start of better hub integration for users
This commit is contained in:
@@ -10,7 +10,7 @@
|
|||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<name>Hub</name>
|
<name>Hub</name>
|
||||||
<description>Lobby/hub behavior: protection, welcome message, spawn-on-join</description>
|
<description>Lobby/hub behavior: protection, welcome message, spawn-on-join, daily rewards</description>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<hytale.server.jar>${user.home}/AppData/Roaming/Hytale/install/release/package/game/latest/Server/HytaleServer.jar</hytale.server.jar>
|
<hytale.server.jar>${user.home}/AppData/Roaming/Hytale/install/release/package/game/latest/Server/HytaleServer.jar</hytale.server.jar>
|
||||||
<networkcore.jar>${project.basedir}/../Core/target/NetworkCore-0.1.0.jar</networkcore.jar>
|
<networkcore.jar>${project.basedir}/../Core/target/NetworkCore-0.1.0.jar</networkcore.jar>
|
||||||
|
<coreui.jar>${project.basedir}/../CoreUI/target/CoreUI-0.1.0.jar</coreui.jar>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
@@ -37,6 +38,14 @@
|
|||||||
<systemPath>${networkcore.jar}</systemPath>
|
<systemPath>${networkcore.jar}</systemPath>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.kewwbec</groupId>
|
||||||
|
<artifactId>coreui</artifactId>
|
||||||
|
<version>0.1.0</version>
|
||||||
|
<scope>system</scope>
|
||||||
|
<systemPath>${coreui.jar}</systemPath>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.code.findbugs</groupId>
|
<groupId>com.google.code.findbugs</groupId>
|
||||||
<artifactId>jsr305</artifactId>
|
<artifactId>jsr305</artifactId>
|
||||||
@@ -47,5 +56,43 @@
|
|||||||
|
|
||||||
<build>
|
<build>
|
||||||
<finalName>${project.artifactId}-${project.version}</finalName>
|
<finalName>${project.artifactId}-${project.version}</finalName>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.5.1</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||||
|
<minimizeJar>false</minimizeJar>
|
||||||
|
<artifactSet>
|
||||||
|
<excludes>
|
||||||
|
<exclude>com.hypixel.hytale:*</exclude>
|
||||||
|
<exclude>net.kewwbec:networkcore</exclude>
|
||||||
|
<exclude>net.kewwbec:coreui</exclude>
|
||||||
|
<exclude>com.google.code.findbugs:*</exclude>
|
||||||
|
</excludes>
|
||||||
|
</artifactSet>
|
||||||
|
<filters>
|
||||||
|
<filter>
|
||||||
|
<artifact>*:*</artifact>
|
||||||
|
<excludes>
|
||||||
|
<exclude>META-INF/*.SF</exclude>
|
||||||
|
<exclude>META-INF/*.DSA</exclude>
|
||||||
|
<exclude>META-INF/*.RSA</exclude>
|
||||||
|
<exclude>module-info.class</exclude>
|
||||||
|
</excludes>
|
||||||
|
</filter>
|
||||||
|
</filters>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ public final class HubPermissionsNodes {
|
|||||||
|
|
||||||
public static final String ROOT = "networkhub";
|
public static final String ROOT = "networkhub";
|
||||||
public static final String BUILD = ROOT + ".build";
|
public static final String BUILD = ROOT + ".build";
|
||||||
|
public static final String REWARDS_USE = ROOT + ".rewards.use";
|
||||||
|
public static final String KOTH_ADMIN = ROOT + ".koth.admin";
|
||||||
|
public static final String UI_ADMIN = ROOT + ".ui.admin";
|
||||||
|
|
||||||
private HubPermissionsNodes() {
|
private HubPermissionsNodes() {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package net.kewwbec.hub;
|
package net.kewwbec.hub;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.event.TriggerVolumeEvent;
|
||||||
|
import com.hypixel.hytale.component.Ref;
|
||||||
|
import com.hypixel.hytale.component.Store;
|
||||||
import com.hypixel.hytale.logger.HytaleLogger;
|
import com.hypixel.hytale.logger.HytaleLogger;
|
||||||
|
import com.hypixel.hytale.server.core.HytaleServer;
|
||||||
import com.hypixel.hytale.server.core.event.events.player.AddPlayerToWorldEvent;
|
import com.hypixel.hytale.server.core.event.events.player.AddPlayerToWorldEvent;
|
||||||
import com.hypixel.hytale.server.core.event.events.player.PlayerDisconnectEvent;
|
import com.hypixel.hytale.server.core.event.events.player.PlayerDisconnectEvent;
|
||||||
import com.hypixel.hytale.server.core.event.events.player.PlayerReadyEvent;
|
import com.hypixel.hytale.server.core.event.events.player.PlayerReadyEvent;
|
||||||
@@ -8,21 +12,46 @@ import com.hypixel.hytale.server.core.event.events.player.RemovedPlayerFromWorld
|
|||||||
import com.hypixel.hytale.server.core.plugin.JavaPlugin;
|
import com.hypixel.hytale.server.core.plugin.JavaPlugin;
|
||||||
import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
|
import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
|
||||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.World;
|
||||||
import com.hypixel.hytale.server.core.universe.world.events.AllWorldsLoadedEvent;
|
import com.hypixel.hytale.server.core.universe.world.events.AllWorldsLoadedEvent;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
|
import net.kewwbec.coreui.api.AvatarRenderType;
|
||||||
|
import net.kewwbec.coreui.api.AvatarRequest;
|
||||||
|
import net.kewwbec.coreui.api.Avatars;
|
||||||
import net.kewwbec.hub.command.BuildCommand;
|
import net.kewwbec.hub.command.BuildCommand;
|
||||||
|
import net.kewwbec.hub.command.MenuCommand;
|
||||||
|
import net.kewwbec.hub.command.RewardsCommand;
|
||||||
|
import net.kewwbec.hub.command.UnhideHubUICommand;
|
||||||
import net.kewwbec.hub.config.HubConfig;
|
import net.kewwbec.hub.config.HubConfig;
|
||||||
import net.kewwbec.hub.config.HubConfigLoader;
|
import net.kewwbec.hub.config.HubConfigLoader;
|
||||||
|
import net.kewwbec.hub.db.DailyRewardRepository;
|
||||||
|
import net.kewwbec.hub.db.HubSchemaBootstrap;
|
||||||
|
import net.kewwbec.hub.koth.KothCommand;
|
||||||
|
import net.kewwbec.hub.koth.KothListener;
|
||||||
|
import net.kewwbec.hub.koth.KothService;
|
||||||
|
import net.kewwbec.hub.listener.HubHudVisibility;
|
||||||
|
import net.kewwbec.hub.listener.HubInventoryListener;
|
||||||
import net.kewwbec.hub.listener.HubJoinListener;
|
import net.kewwbec.hub.listener.HubJoinListener;
|
||||||
import net.kewwbec.hub.listener.HubMessageListener;
|
import net.kewwbec.hub.listener.HubMessageListener;
|
||||||
import net.kewwbec.hub.protection.CancelBreakBlockSystem;
|
import net.kewwbec.hub.listener.MenuItemActivateSystem;
|
||||||
import net.kewwbec.hub.protection.CancelDamageBlockSystem;
|
import net.kewwbec.hub.protection.*;
|
||||||
import net.kewwbec.hub.protection.CancelPlaceBlockSystem;
|
|
||||||
import net.kewwbec.hub.protection.HubWorldFilter;
|
|
||||||
import net.kewwbec.hub.service.BuildModeService;
|
import net.kewwbec.hub.service.BuildModeService;
|
||||||
|
import net.kewwbec.hub.service.DailyRewardService;
|
||||||
import net.kewwbec.hub.service.HubWorldService;
|
import net.kewwbec.hub.service.HubWorldService;
|
||||||
|
import net.kewwbec.hub.ui.MenuHintHud;
|
||||||
|
import net.kewwbec.hub.ui.NetworkXpHud;
|
||||||
|
import net.kewwbec.networkcore.NetworkCore;
|
||||||
|
import net.kewwbec.networkcore.api.DatabaseService;
|
||||||
|
import net.kewwbec.networkcore.api.xp.NetworkXp;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ScheduledFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
public final class HubPlugin extends JavaPlugin {
|
public final class HubPlugin extends JavaPlugin {
|
||||||
@@ -34,6 +63,14 @@ public final class HubPlugin extends JavaPlugin {
|
|||||||
private BuildModeService buildMode;
|
private BuildModeService buildMode;
|
||||||
private HubWorldService worldService;
|
private HubWorldService worldService;
|
||||||
|
|
||||||
|
private final Map<UUID, NetworkXpHud> xpHuds = new ConcurrentHashMap<>();
|
||||||
|
private final Map<UUID, MenuHintHud> menuHintHuds = new ConcurrentHashMap<>();
|
||||||
|
@javax.annotation.Nullable private ScheduledFuture<?> xpHudTask;
|
||||||
|
|
||||||
|
@javax.annotation.Nullable private KothService koth;
|
||||||
|
@javax.annotation.Nullable private MenuItemActivateSystem menuItemSystem;
|
||||||
|
@javax.annotation.Nullable private HubHudVisibility hudVisibility;
|
||||||
|
|
||||||
public HubPlugin(@Nonnull JavaPluginInit init) {
|
public HubPlugin(@Nonnull JavaPluginInit init) {
|
||||||
super(init);
|
super(init);
|
||||||
}
|
}
|
||||||
@@ -56,15 +93,29 @@ public final class HubPlugin extends JavaPlugin {
|
|||||||
this.worldService = new HubWorldService(config);
|
this.worldService = new HubWorldService(config);
|
||||||
HubJoinListener joinListener = new HubJoinListener(config, filter);
|
HubJoinListener joinListener = new HubJoinListener(config, filter);
|
||||||
HubMessageListener messageListener = new HubMessageListener(config, filter);
|
HubMessageListener messageListener = new HubMessageListener(config, filter);
|
||||||
|
this.hudVisibility = new HubHudVisibility(config.inventory.hidden_hud_components);
|
||||||
|
hudVisibility.setChangeListener(this::onHudVisibilityChange);
|
||||||
|
HubInventoryListener inventoryListener = new HubInventoryListener(config.inventory, filter, hudVisibility);
|
||||||
|
|
||||||
getEventRegistry().registerGlobal(AllWorldsLoadedEvent.class, e -> worldService.applyToConfiguredWorlds());
|
getEventRegistry().registerGlobal(AllWorldsLoadedEvent.class, e -> worldService.applyToConfiguredWorlds());
|
||||||
worldService.applyToConfiguredWorlds();
|
worldService.applyToConfiguredWorlds();
|
||||||
|
|
||||||
getEventRegistry().registerGlobal(PlayerReadyEvent.class, joinListener::onReady);
|
getEventRegistry().registerGlobal(PlayerReadyEvent.class, joinListener::onReady);
|
||||||
|
getEventRegistry().registerGlobal(PlayerReadyEvent.class, inventoryListener::onReady);
|
||||||
|
getEventRegistry().registerGlobal(PlayerReadyEvent.class, this::prefetchAvatar);
|
||||||
|
getEventRegistry().registerGlobal(PlayerReadyEvent.class, this::showXpHud);
|
||||||
|
|
||||||
getEventRegistry().registerGlobal(AddPlayerToWorldEvent.class, messageListener::onAdd);
|
getEventRegistry().registerGlobal(AddPlayerToWorldEvent.class, messageListener::onAdd);
|
||||||
getEventRegistry().registerGlobal(RemovedPlayerFromWorldEvent.class, messageListener::onRemove);
|
getEventRegistry().registerGlobal(RemovedPlayerFromWorldEvent.class, messageListener::onRemove);
|
||||||
getEventRegistry().registerGlobal(PlayerDisconnectEvent.class, this::onDisconnect);
|
getEventRegistry().registerGlobal(PlayerDisconnectEvent.class, this::onDisconnect);
|
||||||
|
|
||||||
|
if (config.koth.enabled) {
|
||||||
|
this.koth = new KothService(config.koth, NetworkCore.getInstance().getNetworkXp(), hudVisibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.menuItemSystem = new MenuItemActivateSystem(config.inventory, filter, buildMode, hudVisibility, koth);
|
||||||
|
getEntityStoreRegistry().registerSystem(menuItemSystem);
|
||||||
|
|
||||||
if (config.protection.disable_block_break) {
|
if (config.protection.disable_block_break) {
|
||||||
getEntityStoreRegistry().registerSystem(new CancelBreakBlockSystem(filter, buildMode));
|
getEntityStoreRegistry().registerSystem(new CancelBreakBlockSystem(filter, buildMode));
|
||||||
}
|
}
|
||||||
@@ -74,19 +125,161 @@ public final class HubPlugin extends JavaPlugin {
|
|||||||
if (config.protection.disable_block_damage) {
|
if (config.protection.disable_block_damage) {
|
||||||
getEntityStoreRegistry().registerSystem(new CancelDamageBlockSystem(filter, buildMode));
|
getEntityStoreRegistry().registerSystem(new CancelDamageBlockSystem(filter, buildMode));
|
||||||
}
|
}
|
||||||
|
if (config.protection.disable_damage) {
|
||||||
|
getEntityStoreRegistry().registerSystem(new CancelDamageSystem(filter, koth));
|
||||||
|
}
|
||||||
|
if (config.protection.disable_stamina_drain) {
|
||||||
|
getEntityStoreRegistry().registerSystem(new RefillStaminaSystem(filter));
|
||||||
|
}
|
||||||
|
if (config.protection.disable_item_drop) {
|
||||||
|
getEntityStoreRegistry().registerSystem(new CancelDropItemSystem(filter));
|
||||||
|
}
|
||||||
|
|
||||||
getCommandRegistry().registerCommand(new BuildCommand(buildMode));
|
getCommandRegistry().registerCommand(new BuildCommand(buildMode, hudVisibility, koth));
|
||||||
|
getCommandRegistry().registerCommand(new MenuCommand());
|
||||||
|
getCommandRegistry().registerCommand(new UnhideHubUICommand(hudVisibility, filter, buildMode, koth));
|
||||||
|
|
||||||
|
if (config.rewards.enabled) {
|
||||||
|
NetworkCore core = NetworkCore.getInstance();
|
||||||
|
DatabaseService db = core.getDatabase();
|
||||||
|
if (db == null || !db.isHealthy()) {
|
||||||
|
LOGGER.at(Level.WARNING).log("Hub rewards require NetworkCore MySQL; skipping (rewards.enabled=true but mysql is unavailable)");
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
HubSchemaBootstrap.apply(db.getDataSource(), config.rewards);
|
||||||
|
DailyRewardRepository rewardRepo = new DailyRewardRepository(db.getDataSource(), config.rewards.table);
|
||||||
|
NetworkXp xp = core.getNetworkXp();
|
||||||
|
if (xp == null) {
|
||||||
|
LOGGER.at(Level.WARNING).log("NetworkXp service unavailable; rewards will record claims but not award XP");
|
||||||
|
}
|
||||||
|
DailyRewardService rewards = new DailyRewardService(rewardRepo, xp, config.rewards);
|
||||||
|
getCommandRegistry().registerCommand(new RewardsCommand(rewards));
|
||||||
|
LOGGER.at(Level.INFO).log("Hub rewards ready (table=%s, %d XP per claim)", config.rewards.table, config.rewards.xp_per_claim);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
((HytaleLogger.Api) LOGGER.at(Level.SEVERE).withCause(e)).log("Failed to bootstrap Hub rewards schema; rewards disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xpHudTask = HytaleServer.SCHEDULED_EXECUTOR.scheduleAtFixedRate(
|
||||||
|
this::refreshAllXpHuds, 10L, 10L, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
if (koth != null) {
|
||||||
|
koth.start();
|
||||||
|
KothListener kothListener = new KothListener(koth, config.koth);
|
||||||
|
getEventRegistry().registerGlobal(TriggerVolumeEvent.class, kothListener::onTrigger);
|
||||||
|
getCommandRegistry().registerCommand(new KothCommand(koth));
|
||||||
|
LOGGER.at(Level.INFO).log("KOTH ready (arena_tag=%s, capture_tag=%s, round=%ds, intermission=%ds, xp=%d/%d/%d (+%d), kb=%.1fx)",
|
||||||
|
config.koth.arena_tag, config.koth.capture_tag, config.koth.round_seconds,
|
||||||
|
config.koth.intermission_seconds,
|
||||||
|
config.koth.xp_first, config.koth.xp_second, config.koth.xp_third, config.koth.xp_participation,
|
||||||
|
config.koth.knockback_multiplier);
|
||||||
|
}
|
||||||
|
|
||||||
LOGGER.at(Level.INFO).log("Hub started");
|
LOGGER.at(Level.INFO).log("Hub started");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onDisconnect(@Nonnull PlayerDisconnectEvent event) {
|
private void onDisconnect(@Nonnull PlayerDisconnectEvent event) {
|
||||||
PlayerRef ref = event.getPlayerRef();
|
PlayerRef ref = event.getPlayerRef();
|
||||||
if (ref != null) buildMode.clear(ref.getUuid());
|
if (ref == null) return;
|
||||||
|
buildMode.clear(ref.getUuid());
|
||||||
|
xpHuds.remove(ref.getUuid());
|
||||||
|
menuHintHuds.remove(ref.getUuid());
|
||||||
|
if (koth != null) koth.onDisconnect(ref);
|
||||||
|
if (menuItemSystem != null) menuItemSystem.clearPlayer(ref.getUuid());
|
||||||
|
if (hudVisibility != null) hudVisibility.clearPlayer(ref.getUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onHudVisibilityChange(@Nonnull PlayerRef ref) {
|
||||||
|
Ref<EntityStore> r = ref.getReference();
|
||||||
|
if (r == null || !r.isValid()) return;
|
||||||
|
World world = r.getStore().getExternalData().getWorld();
|
||||||
|
if (world == null) return;
|
||||||
|
UUID uuid = ref.getUuid();
|
||||||
|
boolean isHidden = hudVisibility != null && hudVisibility.isHidden(uuid);
|
||||||
|
|
||||||
|
NetworkXpHud xpHud = xpHuds.get(uuid);
|
||||||
|
if (xpHud != null) {
|
||||||
|
world.execute(() -> { try { xpHud.refresh(); } catch (RuntimeException ignored) {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuHintHud hint = menuHintHuds.get(uuid);
|
||||||
|
if (hint != null) {
|
||||||
|
world.execute(() -> {
|
||||||
|
try {
|
||||||
|
if (isHidden) hint.show();
|
||||||
|
else hint.clear();
|
||||||
|
} catch (RuntimeException ignored) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showXpHud(@Nonnull PlayerReadyEvent event) {
|
||||||
|
Ref<EntityStore> ref = event.getPlayerRef();
|
||||||
|
if (ref == null || !ref.isValid()) return;
|
||||||
|
Store<EntityStore> store = ref.getStore();
|
||||||
|
PlayerRef playerRef = store.getComponent(ref, PlayerRef.getComponentType());
|
||||||
|
if (playerRef == null) return;
|
||||||
|
World world = store.getExternalData().getWorld();
|
||||||
|
if (world == null || !filter.isHub(world)) return;
|
||||||
|
world.execute(() -> {
|
||||||
|
NetworkXp xp = NetworkCore.getInstance().getNetworkXp();
|
||||||
|
NetworkXpHud hud = new NetworkXpHud(playerRef, xp, hudVisibility);
|
||||||
|
xpHuds.put(playerRef.getUuid(), hud);
|
||||||
|
hud.show();
|
||||||
|
|
||||||
|
MenuHintHud hint = new MenuHintHud(playerRef);
|
||||||
|
menuHintHuds.put(playerRef.getUuid(), hint);
|
||||||
|
hint.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshAllXpHuds() {
|
||||||
|
for (NetworkXpHud hud : xpHuds.values()) {
|
||||||
|
PlayerRef pr = hud.getPlayerRef();
|
||||||
|
if (!pr.isValid()) continue;
|
||||||
|
Ref<EntityStore> ref = pr.getReference();
|
||||||
|
if (ref == null || !ref.isValid()) continue;
|
||||||
|
World world = ref.getStore().getExternalData().getWorld();
|
||||||
|
if (world == null) continue;
|
||||||
|
world.execute(() -> {
|
||||||
|
try { hud.refresh(); } catch (RuntimeException ignored) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prefetchAvatar(@Nonnull PlayerReadyEvent event) {
|
||||||
|
Ref<EntityStore> ref = event.getPlayerRef();
|
||||||
|
if (ref == null || !ref.isValid()) return;
|
||||||
|
Store<EntityStore> store = ref.getStore();
|
||||||
|
PlayerRef playerRef = store.getComponent(ref, PlayerRef.getComponentType());
|
||||||
|
if (playerRef == null) return;
|
||||||
|
World world = store.getExternalData().getWorld();
|
||||||
|
if (world == null || !filter.isHub(world)) return;
|
||||||
|
Avatars avatars = NetworkCore.getInstance().findService(Avatars.class);
|
||||||
|
if (avatars == null) return;
|
||||||
|
AvatarRequest req = AvatarRequest.forUsername(playerRef.getUsername())
|
||||||
|
.renderType(AvatarRenderType.HEAD).size(256).rotate(25);
|
||||||
|
avatars.installAsync(playerRef, req, world::execute)
|
||||||
|
.exceptionally(t -> {
|
||||||
|
LOGGER.at(Level.WARNING).log("Hub avatar prefetch failed for %s: %s",
|
||||||
|
playerRef.getUsername(), t.getMessage());
|
||||||
|
return null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void shutdown() {
|
protected void shutdown() {
|
||||||
|
if (xpHudTask != null) {
|
||||||
|
try { xpHudTask.cancel(false); } catch (RuntimeException ignored) {}
|
||||||
|
xpHudTask = null;
|
||||||
|
}
|
||||||
|
xpHuds.clear();
|
||||||
|
menuHintHuds.clear();
|
||||||
|
if (koth != null) {
|
||||||
|
try { koth.stop(); } catch (RuntimeException ignored) {}
|
||||||
|
koth = null;
|
||||||
|
}
|
||||||
LOGGER.at(Level.INFO).log("Hub shutdown complete");
|
LOGGER.at(Level.INFO).log("Hub shutdown complete");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,23 +9,27 @@ import com.hypixel.hytale.server.core.universe.PlayerRef;
|
|||||||
import com.hypixel.hytale.server.core.universe.world.World;
|
import com.hypixel.hytale.server.core.universe.world.World;
|
||||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
import net.kewwbec.hub.HubPermissionsNodes;
|
import net.kewwbec.hub.HubPermissionsNodes;
|
||||||
|
import net.kewwbec.hub.koth.KothService;
|
||||||
|
import net.kewwbec.hub.listener.HubHudVisibility;
|
||||||
import net.kewwbec.hub.service.BuildModeService;
|
import net.kewwbec.hub.service.BuildModeService;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
|
|
||||||
/**
|
|
||||||
* /build - toggle build mode for the caller. Bypasses hub block break/place/damage protection
|
|
||||||
* while ON. World-level flags (PvP, fall damage) are not affected; those are server-wide and not
|
|
||||||
* per-player. Cleared on disconnect.
|
|
||||||
*/
|
|
||||||
public final class BuildCommand extends AbstractPlayerCommand {
|
public final class BuildCommand extends AbstractPlayerCommand {
|
||||||
|
|
||||||
private final BuildModeService buildMode;
|
private final BuildModeService buildMode;
|
||||||
|
@Nullable private final HubHudVisibility hudVisibility;
|
||||||
|
@Nullable private final KothService koth;
|
||||||
|
|
||||||
public BuildCommand(@Nonnull BuildModeService buildMode) {
|
public BuildCommand(@Nonnull BuildModeService buildMode,
|
||||||
|
@Nullable HubHudVisibility hudVisibility,
|
||||||
|
@Nullable KothService koth) {
|
||||||
super("build", "Toggle build mode - bypass hub block protection for yourself");
|
super("build", "Toggle build mode - bypass hub block protection for yourself");
|
||||||
this.buildMode = buildMode;
|
this.buildMode = buildMode;
|
||||||
|
this.hudVisibility = hudVisibility;
|
||||||
|
this.koth = koth;
|
||||||
requirePermission(HubPermissionsNodes.BUILD);
|
requirePermission(HubPermissionsNodes.BUILD);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,8 +39,13 @@ public final class BuildCommand extends AbstractPlayerCommand {
|
|||||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
||||||
boolean nowOn = buildMode.toggle(sender.getUuid());
|
boolean nowOn = buildMode.toggle(sender.getUuid());
|
||||||
if (nowOn) {
|
if (nowOn) {
|
||||||
ctx.sendMessage(Message.raw("Build mode ON. You can break, place, and damage blocks in hub worlds. Run /build again to turn off.").color(Color.GREEN));
|
if (hudVisibility != null) hudVisibility.showAll(sender);
|
||||||
|
ctx.sendMessage(Message.raw("Build mode ON. Hub HUD is now visible. Run /build again to turn off.").color(Color.GREEN));
|
||||||
} else {
|
} else {
|
||||||
|
if (hudVisibility != null) {
|
||||||
|
boolean stayUnhidden = koth != null && koth.isInActiveArena(sender.getUuid());
|
||||||
|
if (!stayUnhidden) hudVisibility.applyHidden(sender);
|
||||||
|
}
|
||||||
ctx.sendMessage(Message.raw("Build mode OFF. Hub protection is back to normal for you.").color(Color.YELLOW));
|
ctx.sendMessage(Message.raw("Build mode OFF. Hub protection is back to normal for you.").color(Color.YELLOW));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package net.kewwbec.hub.command;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.component.Ref;
|
||||||
|
import com.hypixel.hytale.component.Store;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
|
||||||
|
import com.hypixel.hytale.server.core.entity.entities.Player;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.World;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
|
import net.kewwbec.hub.ui.HubMenuPage;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /menu - open the hub main menu (sidebar + 1x3 card grid).
|
||||||
|
*/
|
||||||
|
public final class MenuCommand extends AbstractPlayerCommand {
|
||||||
|
|
||||||
|
public MenuCommand() {
|
||||||
|
super("menu", "Open the hub main menu");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected boolean canGeneratePermission() { return false; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void execute(@Nonnull CommandContext ctx,
|
||||||
|
@Nonnull Store<EntityStore> store,
|
||||||
|
@Nonnull Ref<EntityStore> ref,
|
||||||
|
@Nonnull PlayerRef sender,
|
||||||
|
@Nonnull World world) {
|
||||||
|
Player player = store.getComponent(ref, Player.getComponentType());
|
||||||
|
if (player == null) return;
|
||||||
|
player.getPageManager().openCustomPage(ref, store, new HubMenuPage(sender));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package net.kewwbec.hub.command;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.component.Ref;
|
||||||
|
import com.hypixel.hytale.component.Store;
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
|
||||||
|
import com.hypixel.hytale.server.core.entity.entities.Player;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.World;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
|
import net.kewwbec.hub.HubPermissionsNodes;
|
||||||
|
import net.kewwbec.hub.service.DailyRewardService;
|
||||||
|
import net.kewwbec.hub.ui.DailyRewardsPage;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.awt.Color;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /rewards - opens the daily rewards page. Functions as the testing entry point until an NPC
|
||||||
|
* with an OpenCustomUIInteraction is wired up.
|
||||||
|
*/
|
||||||
|
public final class RewardsCommand extends AbstractPlayerCommand {
|
||||||
|
|
||||||
|
private final DailyRewardService rewards;
|
||||||
|
|
||||||
|
public RewardsCommand(@Nonnull DailyRewardService rewards) {
|
||||||
|
super("rewards", "Open the daily rewards menu");
|
||||||
|
this.rewards = rewards;
|
||||||
|
requirePermission(HubPermissionsNodes.REWARDS_USE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected boolean canGeneratePermission() { return false; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void execute(@Nonnull CommandContext ctx,
|
||||||
|
@Nonnull Store<EntityStore> store,
|
||||||
|
@Nonnull Ref<EntityStore> ref,
|
||||||
|
@Nonnull PlayerRef sender,
|
||||||
|
@Nonnull World world) {
|
||||||
|
Player player = store.getComponent(ref, Player.getComponentType());
|
||||||
|
if (player == null) {
|
||||||
|
ctx.sendMessage(Message.raw("Could not open rewards page.").color(Color.RED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
player.getPageManager().openCustomPage(ref, store, new DailyRewardsPage(sender, rewards));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package net.kewwbec.hub.command;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.component.Ref;
|
||||||
|
import com.hypixel.hytale.component.Store;
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import com.hypixel.hytale.server.core.universe.Universe;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.World;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
|
import net.kewwbec.hub.HubPermissionsNodes;
|
||||||
|
import net.kewwbec.hub.koth.KothService;
|
||||||
|
import net.kewwbec.hub.listener.HubHudVisibility;
|
||||||
|
import net.kewwbec.hub.protection.HubWorldFilter;
|
||||||
|
import net.kewwbec.hub.service.BuildModeService;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
public final class UnhideHubUICommand extends AbstractPlayerCommand {
|
||||||
|
|
||||||
|
private final HubHudVisibility hudVisibility;
|
||||||
|
private final HubWorldFilter filter;
|
||||||
|
private final BuildModeService buildMode;
|
||||||
|
@Nullable private final KothService koth;
|
||||||
|
|
||||||
|
public UnhideHubUICommand(@Nonnull HubHudVisibility hudVisibility,
|
||||||
|
@Nonnull HubWorldFilter filter,
|
||||||
|
@Nonnull BuildModeService buildMode,
|
||||||
|
@Nullable KothService koth) {
|
||||||
|
super("unhideHubUI", "Globally toggle hub HUD visibility for everyone");
|
||||||
|
this.hudVisibility = hudVisibility;
|
||||||
|
this.filter = filter;
|
||||||
|
this.buildMode = buildMode;
|
||||||
|
this.koth = koth;
|
||||||
|
requirePermission(HubPermissionsNodes.UI_ADMIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected boolean canGeneratePermission() { return false; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void execute(@Nonnull CommandContext ctx,
|
||||||
|
@Nonnull Store<EntityStore> store,
|
||||||
|
@Nonnull Ref<EntityStore> ref,
|
||||||
|
@Nonnull PlayerRef sender,
|
||||||
|
@Nonnull World world) {
|
||||||
|
boolean turningOn = !hudVisibility.isGlobalForceShow();
|
||||||
|
hudVisibility.setGlobalForceShow(turningOn);
|
||||||
|
|
||||||
|
Universe u = Universe.get();
|
||||||
|
int affected = 0;
|
||||||
|
if (u != null) {
|
||||||
|
Collection<PlayerRef> players = u.getPlayers();
|
||||||
|
if (players != null) {
|
||||||
|
for (PlayerRef p : players) {
|
||||||
|
if (p == null) continue;
|
||||||
|
Ref<EntityStore> r = p.getReference();
|
||||||
|
if (r == null || !r.isValid()) continue;
|
||||||
|
World w = r.getStore().getExternalData().getWorld();
|
||||||
|
if (w == null || !filter.isHub(w)) continue;
|
||||||
|
if (turningOn) {
|
||||||
|
hudVisibility.showAll(p);
|
||||||
|
} else {
|
||||||
|
boolean keepShown = buildMode.isOn(p.getUuid())
|
||||||
|
|| (koth != null && koth.isInActiveArena(p.getUuid()));
|
||||||
|
if (keepShown) {
|
||||||
|
hudVisibility.showAll(p);
|
||||||
|
} else {
|
||||||
|
hudVisibility.applyHidden(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
affected++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (turningOn) {
|
||||||
|
ctx.sendMessage(Message.raw("Hub HUD globally UNHIDDEN for " + affected + " player(s). Re-run to revert.").color(Color.GREEN));
|
||||||
|
} else {
|
||||||
|
ctx.sendMessage(Message.raw("Hub HUD restored to default for " + affected + " player(s).").color(Color.YELLOW));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package net.kewwbec.hub.config;
|
package net.kewwbec.hub.config;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public final class HubConfig {
|
public final class HubConfig {
|
||||||
@@ -9,6 +10,9 @@ public final class HubConfig {
|
|||||||
public ProtectionSection protection = new ProtectionSection();
|
public ProtectionSection protection = new ProtectionSection();
|
||||||
public MessagesSection messages = new MessagesSection();
|
public MessagesSection messages = new MessagesSection();
|
||||||
public SpawnSection spawn = new SpawnSection();
|
public SpawnSection spawn = new SpawnSection();
|
||||||
|
public RewardsSection rewards = new RewardsSection();
|
||||||
|
public KothSection koth = new KothSection();
|
||||||
|
public InventorySection inventory = new InventorySection();
|
||||||
|
|
||||||
public static final class WorldsSection {
|
public static final class WorldsSection {
|
||||||
/**
|
/**
|
||||||
@@ -24,6 +28,12 @@ public final class HubConfig {
|
|||||||
public boolean disable_block_place = true;
|
public boolean disable_block_place = true;
|
||||||
/** Cancel DamageBlockEvent in hub worlds (mining progress). */
|
/** Cancel DamageBlockEvent in hub worlds (mining progress). */
|
||||||
public boolean disable_block_damage = true;
|
public boolean disable_block_damage = true;
|
||||||
|
/** Cancel every {@code Damage} event in hub worlds (all entities, all sources). */
|
||||||
|
public boolean disable_damage = true;
|
||||||
|
/** Refill player stamina to max every tick in hub worlds (sprint, swing, etc never drain). */
|
||||||
|
public boolean disable_stamina_drain = true;
|
||||||
|
/** Cancel DropItemEvent in hub worlds (Q/throw is suppressed for every player). */
|
||||||
|
public boolean disable_item_drop = true;
|
||||||
/** WorldConfig.isPvpEnabled = false on hub worlds. */
|
/** WorldConfig.isPvpEnabled = false on hub worlds. */
|
||||||
public boolean disable_pvp = true;
|
public boolean disable_pvp = true;
|
||||||
/** WorldConfig.isFallDamageEnabled = false on hub worlds. */
|
/** WorldConfig.isFallDamageEnabled = false on hub worlds. */
|
||||||
@@ -37,18 +47,34 @@ public final class HubConfig {
|
|||||||
|
|
||||||
public static final class MessagesSection {
|
public static final class MessagesSection {
|
||||||
/**
|
/**
|
||||||
* Private welcome shown only to the joining player. {player} is substituted.
|
* Private welcome lines shown only to the joining player, one chat message per line.
|
||||||
* Empty/null disables.
|
* Each line supports inline color codes and placeholders (see below). Empty list disables.
|
||||||
|
*
|
||||||
|
* Placeholders:
|
||||||
|
* %player% - the joining player's username
|
||||||
|
* %prefix% - their primary rank prefix (empty if no rank or RankLookup unavailable)
|
||||||
|
* %color% - their primary rank color as a &#RRGGBB code (empty if no color)
|
||||||
|
* %server% - this server's id
|
||||||
|
*
|
||||||
|
* Color codes (anywhere in a line):
|
||||||
|
* &#RRGGBB - switch to this hex color until the next code or end of line
|
||||||
|
* &r - reset to default color
|
||||||
|
*
|
||||||
|
* Example: "&#FFAA00Welcome %prefix%%player%! &rType /help to get started."
|
||||||
*/
|
*/
|
||||||
public String welcome = "Welcome to the hub, {player}!";
|
public List<String> welcome_lines = new ArrayList<>(Arrays.asList(
|
||||||
|
"&#FFAA00Welcome to the hub, %prefix%%player%&#FFAA00!",
|
||||||
|
"&#AAAAAARun /help any time. Have fun."
|
||||||
|
));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replaces Hytale's default join broadcast on hub worlds. {player} is substituted.
|
* Replaces Hytale's default join broadcast on hub worlds. Supports the same placeholders
|
||||||
* Empty/null suppresses the join broadcast entirely.
|
* and color codes as welcome_lines. Empty/null suppresses the broadcast entirely.
|
||||||
*/
|
*/
|
||||||
public String join_announcement = "{player} joined the hub.";
|
public String join_announcement = "7FF55+ %prefix%%player% 7FF55joined the hub.";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replaces Hytale's default leave broadcast on hub worlds. {player} is substituted.
|
* Replaces Hytale's default leave broadcast on hub worlds. Empty/null suppresses it.
|
||||||
* Empty/null suppresses the leave broadcast entirely.
|
|
||||||
*/
|
*/
|
||||||
public String leave_announcement = "";
|
public String leave_announcement = "";
|
||||||
}
|
}
|
||||||
@@ -59,4 +85,41 @@ public final class HubConfig {
|
|||||||
*/
|
*/
|
||||||
public boolean teleport_on_join = true;
|
public boolean teleport_on_join = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static final class InventorySection {
|
||||||
|
public boolean clear_on_join = true;
|
||||||
|
public boolean open_menu_on_slot_switch = true;
|
||||||
|
public int resting_slot = 6;
|
||||||
|
public int menu_trigger_slot = 0;
|
||||||
|
public List<String> hidden_hud_components = new ArrayList<>(Arrays.asList("Hotbar", "InputBindings"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class KothSection {
|
||||||
|
public boolean enabled = true;
|
||||||
|
public String arena_tag = "koth_arena";
|
||||||
|
public String capture_tag = "koth_capture";
|
||||||
|
public long round_seconds = 300L;
|
||||||
|
public long intermission_seconds = 300L;
|
||||||
|
public long xp_first = 100L;
|
||||||
|
public long xp_second = 60L;
|
||||||
|
public long xp_third = 30L;
|
||||||
|
public long xp_participation = 10L;
|
||||||
|
public String sword_item_id = "Weapon_Sword_Wood";
|
||||||
|
public int sword_slot = 6;
|
||||||
|
public double knockback_multiplier = 4.0;
|
||||||
|
public boolean debug_chat = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class RewardsSection {
|
||||||
|
/** Enable the daily-rewards feature. Requires NetworkCore MySQL + NetworkXp. */
|
||||||
|
public boolean enabled = true;
|
||||||
|
/** Table name for per-player reward state. */
|
||||||
|
public String table = "hub_daily_rewards";
|
||||||
|
/** Network XP awarded per claim. */
|
||||||
|
public long xp_per_claim = 10L;
|
||||||
|
/** Cooldown between claims, in seconds. Default 24h. */
|
||||||
|
public long cooldown_seconds = 86400L;
|
||||||
|
/** If a player claims within this many seconds AFTER cooldown, streak continues. */
|
||||||
|
public long streak_grace_seconds = 86400L;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ public final class HubConfigLoader {
|
|||||||
if (parsed.worlds == null) parsed.worlds = new HubConfig.WorldsSection();
|
if (parsed.worlds == null) parsed.worlds = new HubConfig.WorldsSection();
|
||||||
if (parsed.protection == null) parsed.protection = new HubConfig.ProtectionSection();
|
if (parsed.protection == null) parsed.protection = new HubConfig.ProtectionSection();
|
||||||
if (parsed.messages == null) parsed.messages = new HubConfig.MessagesSection();
|
if (parsed.messages == null) parsed.messages = new HubConfig.MessagesSection();
|
||||||
|
if (parsed.messages.welcome_lines == null) parsed.messages.welcome_lines = new HubConfig.MessagesSection().welcome_lines;
|
||||||
if (parsed.spawn == null) parsed.spawn = new HubConfig.SpawnSection();
|
if (parsed.spawn == null) parsed.spawn = new HubConfig.SpawnSection();
|
||||||
|
if (parsed.rewards == null) parsed.rewards = new HubConfig.RewardsSection();
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package net.kewwbec.hub.db;
|
||||||
|
|
||||||
|
import net.kewwbec.hub.model.DailyReward;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class DailyRewardRepository {
|
||||||
|
|
||||||
|
private final DataSource ds;
|
||||||
|
private final String table;
|
||||||
|
|
||||||
|
public DailyRewardRepository(@Nonnull DataSource ds, @Nonnull String table) {
|
||||||
|
this.ds = ds;
|
||||||
|
this.table = table;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public Optional<DailyReward> find(@Nonnull UUID uuid) throws SQLException {
|
||||||
|
String sql = "SELECT * FROM " + table + " WHERE uuid = ?";
|
||||||
|
try (Connection c = ds.getConnection(); PreparedStatement p = c.prepareStatement(sql)) {
|
||||||
|
p.setString(1, uuid.toString());
|
||||||
|
try (ResultSet rs = p.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
return Optional.of(new DailyReward(
|
||||||
|
UUID.fromString(rs.getString("uuid")),
|
||||||
|
rs.getLong("last_claim_at"),
|
||||||
|
rs.getInt("streak")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void upsert(@Nonnull UUID uuid, long lastClaimAt, int streak) throws SQLException {
|
||||||
|
String sql = "INSERT INTO " + table + " (uuid, last_claim_at, streak) VALUES (?, ?, ?) " +
|
||||||
|
"ON DUPLICATE KEY UPDATE last_claim_at = VALUES(last_claim_at), streak = VALUES(streak)";
|
||||||
|
try (Connection c = ds.getConnection(); PreparedStatement p = c.prepareStatement(sql)) {
|
||||||
|
p.setString(1, uuid.toString());
|
||||||
|
p.setLong(2, lastClaimAt);
|
||||||
|
p.setInt(3, streak);
|
||||||
|
p.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package net.kewwbec.hub.db;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.logger.HytaleLogger;
|
||||||
|
import net.kewwbec.hub.config.HubConfig;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
public final class HubSchemaBootstrap {
|
||||||
|
|
||||||
|
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||||
|
|
||||||
|
private HubSchemaBootstrap() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void apply(@Nonnull DataSource ds, @Nonnull HubConfig.RewardsSection cfg) throws SQLException {
|
||||||
|
try (Connection c = ds.getConnection(); Statement s = c.createStatement()) {
|
||||||
|
s.executeUpdate("""
|
||||||
|
CREATE TABLE IF NOT EXISTS %s (
|
||||||
|
uuid CHAR(36) NOT NULL,
|
||||||
|
last_claim_at BIGINT NOT NULL DEFAULT 0,
|
||||||
|
streak INT NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (uuid)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||||
|
""".formatted(cfg.table));
|
||||||
|
LOGGER.at(Level.INFO).log("Hub schema bootstrap complete (%s)", cfg.table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package net.kewwbec.hub.koth;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.component.Ref;
|
||||||
|
import com.hypixel.hytale.component.Store;
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection;
|
||||||
|
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.World;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
|
import net.kewwbec.hub.HubPermissionsNodes;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.awt.Color;
|
||||||
|
|
||||||
|
public final class KothCommand extends AbstractCommandCollection {
|
||||||
|
|
||||||
|
public KothCommand(@Nonnull KothService service) {
|
||||||
|
super("koth", "KOTH admin commands");
|
||||||
|
addSubCommand(new StartSub(service));
|
||||||
|
addSubCommand(new EndSub(service));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected boolean canGeneratePermission() { return false; }
|
||||||
|
|
||||||
|
public static final class StartSub extends AbstractPlayerCommand {
|
||||||
|
|
||||||
|
private final KothService service;
|
||||||
|
|
||||||
|
public StartSub(@Nonnull KothService service) {
|
||||||
|
super("start", "Force-start a KOTH round");
|
||||||
|
this.service = service;
|
||||||
|
requirePermission(HubPermissionsNodes.KOTH_ADMIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected boolean canGeneratePermission() { return false; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void execute(@Nonnull CommandContext ctx,
|
||||||
|
@Nonnull Store<EntityStore> store,
|
||||||
|
@Nonnull Ref<EntityStore> ref,
|
||||||
|
@Nonnull PlayerRef sender,
|
||||||
|
@Nonnull World world) {
|
||||||
|
if (service.getPhase() == KothService.Phase.ACTIVE) {
|
||||||
|
ctx.sendMessage(Message.raw("KOTH is already active.").color(Color.YELLOW));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
service.forceStart();
|
||||||
|
ctx.sendMessage(Message.raw("KOTH round started.").color(Color.GREEN));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class EndSub extends AbstractPlayerCommand {
|
||||||
|
|
||||||
|
private final KothService service;
|
||||||
|
|
||||||
|
public EndSub(@Nonnull KothService service) {
|
||||||
|
super("end", "Force-end the current KOTH round");
|
||||||
|
this.service = service;
|
||||||
|
requirePermission(HubPermissionsNodes.KOTH_ADMIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected boolean canGeneratePermission() { return false; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void execute(@Nonnull CommandContext ctx,
|
||||||
|
@Nonnull Store<EntityStore> store,
|
||||||
|
@Nonnull Ref<EntityStore> ref,
|
||||||
|
@Nonnull PlayerRef sender,
|
||||||
|
@Nonnull World world) {
|
||||||
|
if (service.getPhase() == KothService.Phase.INTERMISSION) {
|
||||||
|
ctx.sendMessage(Message.raw("KOTH is already in intermission.").color(Color.YELLOW));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
service.forceEnd();
|
||||||
|
ctx.sendMessage(Message.raw("KOTH round ended.").color(Color.GREEN));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package net.kewwbec.hub.koth;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.entity.entities.player.hud.CustomUIHud;
|
||||||
|
import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import net.kewwbec.hub.koth.KothService.Phase;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class KothHud extends CustomUIHud {
|
||||||
|
|
||||||
|
private static final String UI_FILE = "KweebecNet_Hub/Hud/KothHud.ui";
|
||||||
|
|
||||||
|
public KothHud(@Nonnull PlayerRef playerRef) {
|
||||||
|
super(playerRef, "KweebecNetKothHud", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void render(@Nullable Snapshot snap) {
|
||||||
|
UICommandBuilder ui = new UICommandBuilder();
|
||||||
|
build(ui);
|
||||||
|
if (snap != null) {
|
||||||
|
String timerText = snap.phase() == Phase.INTERMISSION
|
||||||
|
? "Next round in " + formatSeconds(snap.secondsRemaining())
|
||||||
|
: "Round ends in " + formatSeconds(snap.secondsRemaining());
|
||||||
|
ui.set("#KothTimer.Text", timerText);
|
||||||
|
|
||||||
|
String kingText;
|
||||||
|
if (snap.phase() == Phase.INTERMISSION) {
|
||||||
|
kingText = "INTERMISSION";
|
||||||
|
} else if (snap.onHill() == 0) {
|
||||||
|
kingText = "Hill is empty";
|
||||||
|
} else if (snap.onHill() == 1) {
|
||||||
|
kingText = "1 on hill";
|
||||||
|
} else {
|
||||||
|
kingText = snap.onHill() + " on hill";
|
||||||
|
}
|
||||||
|
ui.set("#KothKing.Text", kingText);
|
||||||
|
|
||||||
|
renderBoardLine(ui, "#KothBoardLine1", snap.board(), 0, "1. ");
|
||||||
|
renderBoardLine(ui, "#KothBoardLine2", snap.board(), 1, "2. ");
|
||||||
|
renderBoardLine(ui, "#KothBoardLine3", snap.board(), 2, "3. ");
|
||||||
|
|
||||||
|
if (snap.yourRank() == null) {
|
||||||
|
ui.set("#KothYourStats.Text", "");
|
||||||
|
} else {
|
||||||
|
long seconds = snap.yourTimeMs() == null ? 0L : snap.yourTimeMs() / 1000L;
|
||||||
|
ui.set("#KothYourStats.Text", "You: #" + snap.yourRank() + " " + seconds + "s");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
update(true, ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
update(true, new UICommandBuilder());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void build(@Nonnull UICommandBuilder ui) {
|
||||||
|
ui.append(UI_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void renderBoardLine(@Nonnull UICommandBuilder ui,
|
||||||
|
@Nonnull String selector,
|
||||||
|
@Nonnull List<BoardEntry> board,
|
||||||
|
int idx,
|
||||||
|
@Nonnull String prefix) {
|
||||||
|
if (idx >= board.size()) {
|
||||||
|
ui.set(selector + ".Text", "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BoardEntry e = board.get(idx);
|
||||||
|
ui.set(selector + ".Text", prefix + e.name() + " - " + (e.timeMs() / 1000) + "s");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private static String formatSeconds(long s) {
|
||||||
|
if (s <= 0) return "0s";
|
||||||
|
long m = s / 60;
|
||||||
|
long r = s % 60;
|
||||||
|
if (m > 0) return m + "m " + String.format("%02d", r) + "s";
|
||||||
|
return r + "s";
|
||||||
|
}
|
||||||
|
|
||||||
|
public record BoardEntry(@Nonnull UUID uuid, @Nonnull String name, long timeMs) {}
|
||||||
|
|
||||||
|
public record Snapshot(@Nonnull Phase phase,
|
||||||
|
int onHill,
|
||||||
|
@Nonnull List<BoardEntry> board,
|
||||||
|
@Nullable Integer yourRank,
|
||||||
|
@Nullable Long yourTimeMs,
|
||||||
|
long secondsRemaining) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package net.kewwbec.hub.koth;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.TriggerVolumesPlugin;
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.effect.TriggerEventType;
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.event.TriggerVolumeEvent;
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.manager.TriggerVolumeManager;
|
||||||
|
import com.hypixel.hytale.builtin.triggervolumes.manager.VolumeEntry;
|
||||||
|
import com.hypixel.hytale.component.Ref;
|
||||||
|
import com.hypixel.hytale.component.Store;
|
||||||
|
import com.hypixel.hytale.logger.HytaleLogger;
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import com.hypixel.hytale.server.core.universe.Universe;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.World;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
|
import net.kewwbec.hub.config.HubConfig;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
public final class KothListener {
|
||||||
|
|
||||||
|
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static final Method GET_MANAGER;
|
||||||
|
static {
|
||||||
|
Method m = null;
|
||||||
|
try {
|
||||||
|
m = TriggerVolumesPlugin.class.getDeclaredMethod("getManager", World.class);
|
||||||
|
m.setAccessible(true);
|
||||||
|
} catch (NoSuchMethodException e) {
|
||||||
|
LOGGER.at(Level.SEVERE).log("KOTH: TriggerVolumesPlugin.getManager(World) not found, KOTH disabled");
|
||||||
|
}
|
||||||
|
GET_MANAGER = m;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final KothService service;
|
||||||
|
private final HubConfig.KothSection cfg;
|
||||||
|
|
||||||
|
public KothListener(@Nonnull KothService service, @Nonnull HubConfig.KothSection cfg) {
|
||||||
|
this.service = service;
|
||||||
|
this.cfg = cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onTrigger(@Nonnull TriggerVolumeEvent event) {
|
||||||
|
TriggerEventType type = event.getTriggerEventType();
|
||||||
|
|
||||||
|
Ref<EntityStore> entityRef = event.getEntityRef();
|
||||||
|
if (entityRef == null || !entityRef.isValid()) {
|
||||||
|
debugLog("KOTH: event " + type + " volume=" + event.getVolumeId() + " no entityRef");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Store<EntityStore> store = entityRef.getStore();
|
||||||
|
PlayerRef playerRef = store.getComponent(entityRef, PlayerRef.getComponentType());
|
||||||
|
if (playerRef == null) {
|
||||||
|
debugLog("KOTH: event " + type + " volume=" + event.getVolumeId() + " entity is not a player");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type != TriggerEventType.ENTER && type != TriggerEventType.EXIT) {
|
||||||
|
debug(playerRef, "KOTH event " + type + " (ignored) volume=" + event.getVolumeId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String volumeId = event.getVolumeId();
|
||||||
|
Map<String, String> tags = lookupTags(event.getWorldName(), volumeId);
|
||||||
|
boolean isArena = cfg.arena_tag.equals(volumeId) || (tags != null && tags.containsKey(cfg.arena_tag));
|
||||||
|
boolean isCapture = cfg.capture_tag.equals(volumeId) || (tags != null && tags.containsKey(cfg.capture_tag));
|
||||||
|
debug(playerRef, "KOTH " + type + " volume=" + volumeId
|
||||||
|
+ " tags=" + (tags == null ? "<unreachable>" : tags.keySet())
|
||||||
|
+ " arena=" + isArena + " capture=" + isCapture);
|
||||||
|
|
||||||
|
if (!isArena && !isCapture) return;
|
||||||
|
|
||||||
|
if (type == TriggerEventType.ENTER) {
|
||||||
|
if (isArena) {
|
||||||
|
service.onEnterArena(playerRef);
|
||||||
|
debug(playerRef, "KOTH -> onEnterArena (sword + HUD pushed)");
|
||||||
|
}
|
||||||
|
if (isCapture) {
|
||||||
|
service.onEnterCapture(playerRef);
|
||||||
|
debug(playerRef, "KOTH -> onEnterCapture (you are king)");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isCapture) {
|
||||||
|
service.onExitCapture(playerRef);
|
||||||
|
debug(playerRef, "KOTH -> onExitCapture");
|
||||||
|
}
|
||||||
|
if (isArena) {
|
||||||
|
service.onExitArena(playerRef);
|
||||||
|
debug(playerRef, "KOTH -> onExitArena (sword + HUD cleared)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private Map<String, String> lookupTags(@Nonnull String worldName, @Nonnull String volumeId) {
|
||||||
|
if (GET_MANAGER == null) return null;
|
||||||
|
Universe u = Universe.get();
|
||||||
|
if (u == null) return null;
|
||||||
|
World w = u.getWorld(worldName);
|
||||||
|
if (w == null) return null;
|
||||||
|
TriggerVolumesPlugin plugin = TriggerVolumesPlugin.get();
|
||||||
|
if (plugin == null) return null;
|
||||||
|
TriggerVolumeManager mgr;
|
||||||
|
try {
|
||||||
|
mgr = (TriggerVolumeManager) GET_MANAGER.invoke(plugin, w);
|
||||||
|
} catch (ReflectiveOperationException e) {
|
||||||
|
((HytaleLogger.Api) LOGGER.at(Level.WARNING).withCause(e)).log("KOTH: TriggerVolumesPlugin.getManager failed");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (mgr == null) return null;
|
||||||
|
VolumeEntry entry = mgr.getVolume(volumeId);
|
||||||
|
if (entry == null) return null;
|
||||||
|
return entry.getRawTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void debug(@Nonnull PlayerRef ref, @Nonnull String text) {
|
||||||
|
if (!cfg.debug_chat) return;
|
||||||
|
try { ref.sendMessage(Message.raw("[koth-debug] " + text).color(new Color(0x8898B5))); }
|
||||||
|
catch (RuntimeException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void debugLog(@Nonnull String text) {
|
||||||
|
if (cfg.debug_chat) LOGGER.at(Level.INFO).log(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,414 @@
|
|||||||
|
package net.kewwbec.hub.koth;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.component.Ref;
|
||||||
|
import com.hypixel.hytale.component.Store;
|
||||||
|
import com.hypixel.hytale.logger.HytaleLogger;
|
||||||
|
import com.hypixel.hytale.server.core.HytaleServer;
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.entity.entities.Player;
|
||||||
|
import com.hypixel.hytale.server.core.inventory.ItemStack;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import com.hypixel.hytale.server.core.universe.Universe;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.World;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
|
import net.kewwbec.hub.config.HubConfig;
|
||||||
|
import net.kewwbec.hub.koth.KothHud.BoardEntry;
|
||||||
|
import net.kewwbec.hub.koth.KothHud.Snapshot;
|
||||||
|
import net.kewwbec.hub.listener.HubHudVisibility;
|
||||||
|
import net.kewwbec.networkcore.api.xp.NetworkXp;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ScheduledFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
public final class KothService {
|
||||||
|
|
||||||
|
public enum Phase { ACTIVE, INTERMISSION }
|
||||||
|
|
||||||
|
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||||
|
private static final int[] ANNOUNCE_THRESHOLDS = {60, 30, 10};
|
||||||
|
|
||||||
|
private final HubConfig.KothSection cfg;
|
||||||
|
@Nullable private final NetworkXp xp;
|
||||||
|
@Nullable private final HubHudVisibility hudVisibility;
|
||||||
|
|
||||||
|
private final Map<UUID, ArenaPlayer> arenaPlayers = new ConcurrentHashMap<>();
|
||||||
|
private final Map<UUID, Long> accumulatedMs = new ConcurrentHashMap<>();
|
||||||
|
private final Set<UUID> currentCapturers = ConcurrentHashMap.newKeySet();
|
||||||
|
private final Set<Integer> announcedThresholds = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
private volatile Phase phase = Phase.INTERMISSION;
|
||||||
|
private volatile long phaseStartMs = System.currentTimeMillis();
|
||||||
|
|
||||||
|
@Nullable private ScheduledFuture<?> tickTask;
|
||||||
|
|
||||||
|
public KothService(@Nonnull HubConfig.KothSection cfg,
|
||||||
|
@Nullable NetworkXp xp,
|
||||||
|
@Nullable HubHudVisibility hudVisibility) {
|
||||||
|
this.cfg = cfg;
|
||||||
|
this.xp = xp;
|
||||||
|
this.hudVisibility = hudVisibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
phase = Phase.INTERMISSION;
|
||||||
|
phaseStartMs = System.currentTimeMillis();
|
||||||
|
announcedThresholds.clear();
|
||||||
|
tickTask = HytaleServer.SCHEDULED_EXECUTOR.scheduleAtFixedRate(
|
||||||
|
this::tick, 1L, 1L, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
if (tickTask != null) {
|
||||||
|
try { tickTask.cancel(false); } catch (RuntimeException ignored) {}
|
||||||
|
tickTask = null;
|
||||||
|
}
|
||||||
|
for (ArenaPlayer ap : arenaPlayers.values()) {
|
||||||
|
safeOnWorld(ap.playerRef, () -> ap.hud.clear());
|
||||||
|
}
|
||||||
|
arenaPlayers.clear();
|
||||||
|
accumulatedMs.clear();
|
||||||
|
currentCapturers.clear();
|
||||||
|
announcedThresholds.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInArena(@Nonnull UUID uuid) {
|
||||||
|
return arenaPlayers.containsKey(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInActiveArena(@Nonnull UUID uuid) {
|
||||||
|
return phase == Phase.ACTIVE && arenaPlayers.containsKey(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public Phase getPhase() {
|
||||||
|
return phase;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void forceStart() {
|
||||||
|
if (phase == Phase.ACTIVE) return;
|
||||||
|
startRound();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void forceEnd() {
|
||||||
|
if (phase == Phase.INTERMISSION) return;
|
||||||
|
endRound();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onEnterArena(@Nonnull PlayerRef playerRef) {
|
||||||
|
UUID uuid = playerRef.getUuid();
|
||||||
|
ArenaPlayer ap = new ArenaPlayer(playerRef, new KothHud(playerRef));
|
||||||
|
arenaPlayers.put(uuid, ap);
|
||||||
|
if (phase == Phase.ACTIVE) {
|
||||||
|
accumulatedMs.computeIfAbsent(uuid, k -> 0L);
|
||||||
|
giveSword(playerRef);
|
||||||
|
if (hudVisibility != null) hudVisibility.showAll(playerRef);
|
||||||
|
}
|
||||||
|
ap.hud.render(buildSnapshot(uuid, computeRanking()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onExitArena(@Nonnull PlayerRef playerRef) {
|
||||||
|
UUID uuid = playerRef.getUuid();
|
||||||
|
ArenaPlayer removed = arenaPlayers.remove(uuid);
|
||||||
|
if (removed != null) removed.hud.clear();
|
||||||
|
if (phase == Phase.ACTIVE) removeSword(playerRef);
|
||||||
|
if (hudVisibility != null) hudVisibility.applyHidden(playerRef);
|
||||||
|
currentCapturers.remove(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onEnterCapture(@Nonnull PlayerRef playerRef) {
|
||||||
|
if (arenaPlayers.containsKey(playerRef.getUuid())) {
|
||||||
|
currentCapturers.add(playerRef.getUuid());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onExitCapture(@Nonnull PlayerRef playerRef) {
|
||||||
|
currentCapturers.remove(playerRef.getUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onDisconnect(@Nonnull PlayerRef ref) {
|
||||||
|
UUID uuid = ref.getUuid();
|
||||||
|
ArenaPlayer removed = arenaPlayers.remove(uuid);
|
||||||
|
if (removed != null) {
|
||||||
|
removed.hud.clear();
|
||||||
|
removeSword(ref);
|
||||||
|
}
|
||||||
|
currentCapturers.remove(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getKnockbackMultiplier() {
|
||||||
|
return cfg.knockback_multiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tick() {
|
||||||
|
try {
|
||||||
|
if (phase == Phase.ACTIVE) {
|
||||||
|
for (UUID id : currentCapturers) {
|
||||||
|
accumulatedMs.merge(id, 1000L, Long::sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<Map.Entry<UUID, Long>> ranking = computeRanking();
|
||||||
|
long remainingSec = secondsRemaining();
|
||||||
|
if (phase == Phase.INTERMISSION) maybeAnnounceCountdown(remainingSec);
|
||||||
|
|
||||||
|
pushHud(ranking);
|
||||||
|
|
||||||
|
if (remainingSec <= 0L) {
|
||||||
|
if (phase == Phase.ACTIVE) endRound();
|
||||||
|
else startRound();
|
||||||
|
}
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
((HytaleLogger.Api) LOGGER.at(Level.WARNING).withCause(e)).log("KOTH tick failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void maybeAnnounceCountdown(long remainingSec) {
|
||||||
|
for (int t : ANNOUNCE_THRESHOLDS) {
|
||||||
|
if (remainingSec > 0L && remainingSec <= t && announcedThresholds.add(t)) {
|
||||||
|
broadcastAll(formatCountdown(t));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private static String formatCountdown(int secs) {
|
||||||
|
if (secs >= 60) {
|
||||||
|
int m = secs / 60;
|
||||||
|
return "KOTH starting in " + m + " minute" + (m == 1 ? "" : "s") + "!";
|
||||||
|
}
|
||||||
|
return "KOTH starting in " + secs + " seconds!";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pushHud(@Nonnull List<Map.Entry<UUID, Long>> ranking) {
|
||||||
|
for (ArenaPlayer ap : arenaPlayers.values()) {
|
||||||
|
Snapshot snap = buildSnapshot(ap.playerRef.getUuid(), ranking);
|
||||||
|
safeOnWorld(ap.playerRef, () -> ap.hud.render(snap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void endRound() {
|
||||||
|
List<Map.Entry<UUID, Long>> ranking = computeRanking();
|
||||||
|
ranking.removeIf(e -> e.getValue() <= 0L);
|
||||||
|
|
||||||
|
if (ranking.isEmpty()) {
|
||||||
|
broadcastArena("KOTH round ended with no participants.");
|
||||||
|
} else if (ranking.size() == 1) {
|
||||||
|
UUID solo = ranking.get(0).getKey();
|
||||||
|
long soloMs = ranking.get(0).getValue();
|
||||||
|
broadcastArena("KOTH round ended. Solo capture by " + lookupName(solo)
|
||||||
|
+ " (" + (soloMs / 1000) + "s) - no XP awarded.");
|
||||||
|
} else {
|
||||||
|
broadcastArena("KOTH round ended!");
|
||||||
|
int participation = 0;
|
||||||
|
long participationAwarded = 0L;
|
||||||
|
for (int i = 0; i < ranking.size(); i++) {
|
||||||
|
UUID uuid = ranking.get(i).getKey();
|
||||||
|
long timeMs = ranking.get(i).getValue();
|
||||||
|
long reward = rewardForRank(i);
|
||||||
|
String tag = rankTag(i);
|
||||||
|
if (xp != null && reward > 0L) {
|
||||||
|
xp.add(uuid, reward, "koth_" + tag);
|
||||||
|
}
|
||||||
|
if (i < 3) {
|
||||||
|
broadcastArena(ordinal(i + 1) + ": " + lookupName(uuid)
|
||||||
|
+ " " + (timeMs / 1000) + "s"
|
||||||
|
+ (reward > 0L ? " +" + reward + " XP" : ""));
|
||||||
|
} else {
|
||||||
|
participation++;
|
||||||
|
participationAwarded = reward;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (participation > 0) {
|
||||||
|
broadcastArena(participation + " more participant"
|
||||||
|
+ (participation == 1 ? "" : "s")
|
||||||
|
+ (participationAwarded > 0L ? " got +" + participationAwarded + " XP each" : ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
accumulatedMs.clear();
|
||||||
|
for (ArenaPlayer ap : arenaPlayers.values()) {
|
||||||
|
removeSword(ap.playerRef);
|
||||||
|
if (hudVisibility != null) hudVisibility.applyHidden(ap.playerRef);
|
||||||
|
}
|
||||||
|
phase = Phase.INTERMISSION;
|
||||||
|
phaseStartMs = System.currentTimeMillis();
|
||||||
|
announcedThresholds.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long rewardForRank(int idx) {
|
||||||
|
return switch (idx) {
|
||||||
|
case 0 -> cfg.xp_first;
|
||||||
|
case 1 -> cfg.xp_second;
|
||||||
|
case 2 -> cfg.xp_third;
|
||||||
|
default -> cfg.xp_participation;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private static String rankTag(int idx) {
|
||||||
|
return switch (idx) {
|
||||||
|
case 0 -> "1st";
|
||||||
|
case 1 -> "2nd";
|
||||||
|
case 2 -> "3rd";
|
||||||
|
default -> "participant";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private static String ordinal(int n) {
|
||||||
|
return switch (n) {
|
||||||
|
case 1 -> "1st";
|
||||||
|
case 2 -> "2nd";
|
||||||
|
case 3 -> "3rd";
|
||||||
|
default -> n + "th";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startRound() {
|
||||||
|
for (ArenaPlayer ap : arenaPlayers.values()) {
|
||||||
|
accumulatedMs.computeIfAbsent(ap.playerRef.getUuid(), k -> 0L);
|
||||||
|
giveSword(ap.playerRef);
|
||||||
|
if (hudVisibility != null) hudVisibility.showAll(ap.playerRef);
|
||||||
|
}
|
||||||
|
broadcastAll("KOTH round started. Capture the hill!");
|
||||||
|
phase = Phase.ACTIVE;
|
||||||
|
phaseStartMs = System.currentTimeMillis();
|
||||||
|
announcedThresholds.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long secondsRemaining() {
|
||||||
|
long elapsedMs = System.currentTimeMillis() - phaseStartMs;
|
||||||
|
long limitSec = phase == Phase.ACTIVE ? cfg.round_seconds : cfg.intermission_seconds;
|
||||||
|
return Math.max(0L, (limitSec * 1000L - elapsedMs) / 1000L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private List<Map.Entry<UUID, Long>> computeRanking() {
|
||||||
|
List<Map.Entry<UUID, Long>> sorted = new ArrayList<>(accumulatedMs.entrySet());
|
||||||
|
sorted.sort(Comparator.comparingLong(Map.Entry<UUID, Long>::getValue).reversed());
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private Snapshot buildSnapshot(@Nonnull UUID viewerUuid, @Nonnull List<Map.Entry<UUID, Long>> ranking) {
|
||||||
|
int onHill = currentCapturers.size();
|
||||||
|
|
||||||
|
List<BoardEntry> board = new ArrayList<>(3);
|
||||||
|
for (int i = 0; i < Math.min(3, ranking.size()); i++) {
|
||||||
|
UUID id = ranking.get(i).getKey();
|
||||||
|
board.add(new BoardEntry(id, lookupName(id), ranking.get(i).getValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer yourRank = null;
|
||||||
|
Long yourTimeMs = null;
|
||||||
|
for (int i = 0; i < ranking.size(); i++) {
|
||||||
|
if (ranking.get(i).getKey().equals(viewerUuid)) {
|
||||||
|
int rankOneBased = i + 1;
|
||||||
|
if (rankOneBased > 3) {
|
||||||
|
yourRank = rankOneBased;
|
||||||
|
yourTimeMs = ranking.get(i).getValue();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (yourRank == null && !arenaPlayers.containsKey(viewerUuid)) {
|
||||||
|
yourRank = null;
|
||||||
|
}
|
||||||
|
if (yourRank == null && arenaPlayers.containsKey(viewerUuid)
|
||||||
|
&& !accumulatedMs.containsKey(viewerUuid)) {
|
||||||
|
yourRank = ranking.size() + 1;
|
||||||
|
yourTimeMs = 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Snapshot(phase, onHill, board, yourRank, yourTimeMs, secondsRemaining());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void giveSword(@Nonnull PlayerRef ref) {
|
||||||
|
short slot = (short) Math.max(0, cfg.sword_slot);
|
||||||
|
runOnPlayerEntity(ref, player ->
|
||||||
|
player.getInventory().getHotbar().addItemStackToSlot(slot, new ItemStack(cfg.sword_item_id, 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeSword(@Nonnull PlayerRef ref) {
|
||||||
|
runOnPlayerEntity(ref, player ->
|
||||||
|
player.getInventory().getHotbar().removeItemStack(new ItemStack(cfg.sword_item_id, 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void runOnPlayerEntity(@Nonnull PlayerRef ref,
|
||||||
|
@Nonnull java.util.function.Consumer<Player> fn) {
|
||||||
|
Ref<EntityStore> r = ref.getReference();
|
||||||
|
if (r == null || !r.isValid()) return;
|
||||||
|
Store<EntityStore> store = r.getStore();
|
||||||
|
World world = store.getExternalData().getWorld();
|
||||||
|
if (world == null) return;
|
||||||
|
world.execute(() -> {
|
||||||
|
if (!r.isValid()) return;
|
||||||
|
Player p = store.getComponent(r, Player.getComponentType());
|
||||||
|
if (p == null) return;
|
||||||
|
try { fn.accept(p); } catch (RuntimeException e) {
|
||||||
|
((HytaleLogger.Api) LOGGER.at(Level.WARNING).withCause(e)).log("KOTH inventory op failed");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void safeOnWorld(@Nonnull PlayerRef ref, @Nonnull Runnable r) {
|
||||||
|
Ref<EntityStore> rf = ref.getReference();
|
||||||
|
if (rf == null || !rf.isValid()) return;
|
||||||
|
World world = rf.getStore().getExternalData().getWorld();
|
||||||
|
if (world == null) return;
|
||||||
|
world.execute(() -> { try { r.run(); } catch (RuntimeException ignored) {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
private void broadcastArena(@Nonnull String text) {
|
||||||
|
for (ArenaPlayer ap : arenaPlayers.values()) {
|
||||||
|
try { ap.playerRef.sendMessage(Message.raw(text).color(new Color(0xD4AF37))); }
|
||||||
|
catch (RuntimeException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void broadcastAll(@Nonnull String text) {
|
||||||
|
Universe u = Universe.get();
|
||||||
|
if (u == null) {
|
||||||
|
broadcastArena(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Collection<PlayerRef> players;
|
||||||
|
try {
|
||||||
|
players = u.getPlayers();
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
broadcastArena(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (players == null) return;
|
||||||
|
Set<UUID> seen = new HashSet<>();
|
||||||
|
for (PlayerRef p : players) {
|
||||||
|
if (p == null) continue;
|
||||||
|
if (!seen.add(p.getUuid())) continue;
|
||||||
|
try { p.sendMessage(Message.raw(text).color(new Color(0xD4AF37))); }
|
||||||
|
catch (RuntimeException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private String lookupName(@Nonnull UUID uuid) {
|
||||||
|
ArenaPlayer ap = arenaPlayers.get(uuid);
|
||||||
|
if (ap != null) {
|
||||||
|
try { return ap.playerRef.getUsername(); } catch (RuntimeException ignored) {}
|
||||||
|
}
|
||||||
|
return uuid.toString().substring(0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ArenaPlayer(@Nonnull PlayerRef playerRef, @Nonnull KothHud hud) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package net.kewwbec.hub.listener;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.logger.HytaleLogger;
|
||||||
|
import com.hypixel.hytale.protocol.packets.interface_.HudComponent;
|
||||||
|
import com.hypixel.hytale.protocol.packets.interface_.UpdateVisibleHudComponents;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
public final class HubHudVisibility {
|
||||||
|
|
||||||
|
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||||
|
|
||||||
|
private static final EnumSet<HudComponent> ALWAYS_HIDDEN =
|
||||||
|
EnumSet.of(HudComponent.InputBindings);
|
||||||
|
|
||||||
|
private final HudComponent[] hiddenLayout;
|
||||||
|
private final HudComponent[] showAllLayout;
|
||||||
|
private final boolean anyHidden;
|
||||||
|
private final Set<UUID> hidden = ConcurrentHashMap.newKeySet();
|
||||||
|
@Nullable private volatile Consumer<PlayerRef> changeListener;
|
||||||
|
private volatile boolean globalForceShow = false;
|
||||||
|
|
||||||
|
public HubHudVisibility(@Nonnull List<String> hiddenNames) {
|
||||||
|
EnumSet<HudComponent> toHide = EnumSet.noneOf(HudComponent.class);
|
||||||
|
for (String name : hiddenNames) {
|
||||||
|
if (name == null || name.isBlank()) continue;
|
||||||
|
try {
|
||||||
|
toHide.add(HudComponent.valueOf(name.trim()));
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
LOGGER.at(Level.WARNING).log("Unknown HudComponent in hidden_hud_components: %s", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toHide.addAll(ALWAYS_HIDDEN);
|
||||||
|
this.anyHidden = !toHide.isEmpty();
|
||||||
|
|
||||||
|
EnumSet<HudComponent> hiddenModeVisible = EnumSet.complementOf(toHide);
|
||||||
|
this.hiddenLayout = hiddenModeVisible.toArray(new HudComponent[0]);
|
||||||
|
|
||||||
|
EnumSet<HudComponent> showAllVisible = EnumSet.complementOf(ALWAYS_HIDDEN);
|
||||||
|
this.showAllLayout = showAllVisible.toArray(new HudComponent[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean anyHidden() {
|
||||||
|
return anyHidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isHidden(@Nonnull UUID uuid) {
|
||||||
|
return hidden.contains(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChangeListener(@Nullable Consumer<PlayerRef> listener) {
|
||||||
|
this.changeListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void applyHidden(@Nonnull PlayerRef ref) {
|
||||||
|
if (!anyHidden) return;
|
||||||
|
if (globalForceShow) {
|
||||||
|
showAll(ref);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean changed = hidden.add(ref.getUuid());
|
||||||
|
try {
|
||||||
|
ref.getPacketHandler().write(new UpdateVisibleHudComponents(hiddenLayout));
|
||||||
|
} catch (RuntimeException ignored) {}
|
||||||
|
if (changed) notifyChange(ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isGlobalForceShow() {
|
||||||
|
return globalForceShow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGlobalForceShow(boolean on) {
|
||||||
|
this.globalForceShow = on;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showAll(@Nonnull PlayerRef ref) {
|
||||||
|
if (!anyHidden) return;
|
||||||
|
boolean changed = hidden.remove(ref.getUuid());
|
||||||
|
try {
|
||||||
|
ref.getPacketHandler().write(new UpdateVisibleHudComponents(showAllLayout));
|
||||||
|
} catch (RuntimeException ignored) {}
|
||||||
|
if (changed) notifyChange(ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearPlayer(@Nonnull UUID uuid) {
|
||||||
|
hidden.remove(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyChange(@Nonnull PlayerRef ref) {
|
||||||
|
Consumer<PlayerRef> l = changeListener;
|
||||||
|
if (l != null) {
|
||||||
|
try { l.accept(ref); } catch (RuntimeException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package net.kewwbec.hub.listener;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.component.Ref;
|
||||||
|
import com.hypixel.hytale.component.Store;
|
||||||
|
import com.hypixel.hytale.logger.HytaleLogger;
|
||||||
|
import com.hypixel.hytale.server.core.entity.entities.Player;
|
||||||
|
import com.hypixel.hytale.server.core.event.events.player.PlayerReadyEvent;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.World;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
|
import net.kewwbec.hub.config.HubConfig;
|
||||||
|
import net.kewwbec.hub.protection.HubWorldFilter;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
public final class HubInventoryListener {
|
||||||
|
|
||||||
|
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||||
|
|
||||||
|
private final HubConfig.InventorySection cfg;
|
||||||
|
private final HubWorldFilter filter;
|
||||||
|
private final HubHudVisibility hudVisibility;
|
||||||
|
|
||||||
|
public HubInventoryListener(@Nonnull HubConfig.InventorySection cfg,
|
||||||
|
@Nonnull HubWorldFilter filter,
|
||||||
|
@Nonnull HubHudVisibility hudVisibility) {
|
||||||
|
this.cfg = cfg;
|
||||||
|
this.filter = filter;
|
||||||
|
this.hudVisibility = hudVisibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onReady(@Nonnull PlayerReadyEvent event) {
|
||||||
|
Ref<EntityStore> ref = event.getPlayerRef();
|
||||||
|
if (ref == null || !ref.isValid()) return;
|
||||||
|
Store<EntityStore> store = ref.getStore();
|
||||||
|
PlayerRef playerRef = store.getComponent(ref, PlayerRef.getComponentType());
|
||||||
|
if (playerRef == null) return;
|
||||||
|
World world = store.getExternalData().getWorld();
|
||||||
|
if (world == null || !filter.isHub(world)) return;
|
||||||
|
|
||||||
|
world.execute(() -> {
|
||||||
|
if (!ref.isValid()) return;
|
||||||
|
Player player = store.getComponent(ref, Player.getComponentType());
|
||||||
|
if (player == null) return;
|
||||||
|
try {
|
||||||
|
if (cfg.clear_on_join) {
|
||||||
|
player.getInventory().clear();
|
||||||
|
}
|
||||||
|
byte resting = (byte) Math.max(0, cfg.resting_slot);
|
||||||
|
player.getInventory().setActiveHotbarSlot(ref, resting, store);
|
||||||
|
hudVisibility.applyHidden(playerRef);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
LOGGER.at(Level.WARNING).log("Hub inventory/HUD setup failed for %s: %s",
|
||||||
|
playerRef.getUsername(), e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import com.hypixel.hytale.component.Ref;
|
|||||||
import com.hypixel.hytale.component.Store;
|
import com.hypixel.hytale.component.Store;
|
||||||
import com.hypixel.hytale.logger.HytaleLogger;
|
import com.hypixel.hytale.logger.HytaleLogger;
|
||||||
import com.hypixel.hytale.math.vector.Transform;
|
import com.hypixel.hytale.math.vector.Transform;
|
||||||
import com.hypixel.hytale.server.core.Message;
|
|
||||||
import com.hypixel.hytale.server.core.event.events.player.PlayerReadyEvent;
|
import com.hypixel.hytale.server.core.event.events.player.PlayerReadyEvent;
|
||||||
import com.hypixel.hytale.server.core.modules.entity.teleport.Teleport;
|
import com.hypixel.hytale.server.core.modules.entity.teleport.Teleport;
|
||||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
@@ -14,16 +13,20 @@ import com.hypixel.hytale.server.core.universe.world.spawn.ISpawnProvider;
|
|||||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
import net.kewwbec.hub.config.HubConfig;
|
import net.kewwbec.hub.config.HubConfig;
|
||||||
import net.kewwbec.hub.protection.HubWorldFilter;
|
import net.kewwbec.hub.protection.HubWorldFilter;
|
||||||
|
import net.kewwbec.hub.util.HubMessageFormatter;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import java.awt.Color;
|
import java.util.List;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* On PlayerReadyEvent in a hub world: teleport to spawn and send a private welcome.
|
* On PlayerReadyEvent in a hub world: teleport to spawn and send each configured welcome line.
|
||||||
*
|
*
|
||||||
* Join/leave broadcasts are handled by HubMessageListener (which replaces Hytale's built-in
|
* Lines run through {@link HubMessageFormatter}, which resolves %player% / %prefix% / %color% /
|
||||||
* join/leave messages on AddPlayerToWorldEvent / RemovedPlayerFromWorldEvent).
|
* %server% placeholders and parses inline &#RRGGBB color codes into composed messages.
|
||||||
|
*
|
||||||
|
* Join/leave broadcasts are handled by HubMessageListener (AddPlayerToWorldEvent /
|
||||||
|
* RemovedPlayerFromWorldEvent).
|
||||||
*/
|
*/
|
||||||
public final class HubJoinListener {
|
public final class HubJoinListener {
|
||||||
|
|
||||||
@@ -63,10 +66,12 @@ public final class HubJoinListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.messages.welcome != null && !config.messages.welcome.isBlank()) {
|
List<String> lines = config.messages.welcome_lines;
|
||||||
String welcome = config.messages.welcome.replace("{player}", playerRef.getUsername());
|
if (lines == null || lines.isEmpty()) return;
|
||||||
|
for (String line : lines) {
|
||||||
|
if (line == null || line.isBlank()) continue;
|
||||||
try {
|
try {
|
||||||
playerRef.sendMessage(Message.raw(welcome).color(Color.YELLOW));
|
playerRef.sendMessage(HubMessageFormatter.format(line, playerRef.getUsername(), playerRef.getUuid()));
|
||||||
} catch (RuntimeException ignored) {}
|
} catch (RuntimeException ignored) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,24 +5,25 @@ import com.hypixel.hytale.server.core.Message;
|
|||||||
import com.hypixel.hytale.server.core.event.events.player.AddPlayerToWorldEvent;
|
import com.hypixel.hytale.server.core.event.events.player.AddPlayerToWorldEvent;
|
||||||
import com.hypixel.hytale.server.core.event.events.player.RemovedPlayerFromWorldEvent;
|
import com.hypixel.hytale.server.core.event.events.player.RemovedPlayerFromWorldEvent;
|
||||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
import com.hypixel.hytale.server.core.universe.world.World;
|
|
||||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
import net.kewwbec.hub.config.HubConfig;
|
import net.kewwbec.hub.config.HubConfig;
|
||||||
import net.kewwbec.hub.protection.HubWorldFilter;
|
import net.kewwbec.hub.protection.HubWorldFilter;
|
||||||
|
import net.kewwbec.hub.util.HubMessageFormatter;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replaces (or suppresses) Hytale's default join/leave broadcasts on hub worlds.
|
* Replaces (or suppresses) Hytale's default join/leave broadcasts on hub worlds.
|
||||||
*
|
*
|
||||||
* AddPlayerToWorldEvent / RemovedPlayerFromWorldEvent are fired by Hytale's player-add/remove
|
* Templates run through {@link HubMessageFormatter}: %player%, %prefix%, %color%, %server%
|
||||||
* systems with a default joinMessage / leaveMessage. The systems check shouldBroadcastJoinMessage
|
* placeholders and inline &#RRGGBB color codes. Empty template suppresses the broadcast.
|
||||||
* (or leave) after dispatch and broadcast accordingly. We rewrite the message text here, or
|
|
||||||
* suppress entirely if the configured format is blank.
|
|
||||||
*/
|
*/
|
||||||
public final class HubMessageListener {
|
public final class HubMessageListener {
|
||||||
|
|
||||||
|
private static final UUID NIL = new UUID(0L, 0L);
|
||||||
|
|
||||||
private final HubConfig config;
|
private final HubConfig config;
|
||||||
private final HubWorldFilter filter;
|
private final HubWorldFilter filter;
|
||||||
|
|
||||||
@@ -33,32 +34,35 @@ public final class HubMessageListener {
|
|||||||
|
|
||||||
public void onAdd(@Nonnull AddPlayerToWorldEvent event) {
|
public void onAdd(@Nonnull AddPlayerToWorldEvent event) {
|
||||||
if (!filter.isHub(event.getWorld())) return;
|
if (!filter.isHub(event.getWorld())) return;
|
||||||
String username = resolveUsername(event.getHolder());
|
|
||||||
String template = config.messages.join_announcement;
|
String template = config.messages.join_announcement;
|
||||||
if (template == null || template.isBlank()) {
|
if (template == null || template.isBlank()) {
|
||||||
event.setBroadcastJoinMessage(false);
|
event.setBroadcastJoinMessage(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.setJoinMessage(Message.raw(template.replace("{player}", username)));
|
Subject s = resolveSubject(event.getHolder());
|
||||||
|
event.setJoinMessage(HubMessageFormatter.format(template, s.name, s.uuid));
|
||||||
event.setBroadcastJoinMessage(true);
|
event.setBroadcastJoinMessage(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onRemove(@Nonnull RemovedPlayerFromWorldEvent event) {
|
public void onRemove(@Nonnull RemovedPlayerFromWorldEvent event) {
|
||||||
if (!filter.isHub(event.getWorld())) return;
|
if (!filter.isHub(event.getWorld())) return;
|
||||||
String username = resolveUsername(event.getHolder());
|
|
||||||
String template = config.messages.leave_announcement;
|
String template = config.messages.leave_announcement;
|
||||||
if (template == null || template.isBlank()) {
|
if (template == null || template.isBlank()) {
|
||||||
event.setBroadcastLeaveMessage(false);
|
event.setBroadcastLeaveMessage(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.setLeaveMessage(Message.raw(template.replace("{player}", username)));
|
Subject s = resolveSubject(event.getHolder());
|
||||||
|
event.setLeaveMessage(HubMessageFormatter.format(template, s.name, s.uuid));
|
||||||
event.setBroadcastLeaveMessage(true);
|
event.setBroadcastLeaveMessage(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
private static String resolveUsername(@Nullable Holder<EntityStore> holder) {
|
private static Subject resolveSubject(@Nullable Holder<EntityStore> holder) {
|
||||||
if (holder == null) return "?";
|
if (holder == null) return new Subject("?", NIL);
|
||||||
PlayerRef ref = holder.getComponent(PlayerRef.getComponentType());
|
PlayerRef ref = holder.getComponent(PlayerRef.getComponentType());
|
||||||
return (ref == null) ? "?" : ref.getUsername();
|
if (ref == null) return new Subject("?", NIL);
|
||||||
|
return new Subject(ref.getUsername(), ref.getUuid());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record Subject(@Nonnull String name, @Nonnull UUID uuid) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package net.kewwbec.hub.listener;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.component.ArchetypeChunk;
|
||||||
|
import com.hypixel.hytale.component.CommandBuffer;
|
||||||
|
import com.hypixel.hytale.component.Ref;
|
||||||
|
import com.hypixel.hytale.component.Store;
|
||||||
|
import com.hypixel.hytale.component.query.Query;
|
||||||
|
import com.hypixel.hytale.component.system.EntityEventSystem;
|
||||||
|
import com.hypixel.hytale.logger.HytaleLogger;
|
||||||
|
import com.hypixel.hytale.server.core.entity.entities.Player;
|
||||||
|
import com.hypixel.hytale.protocol.packets.inventory.SetActiveSlot;
|
||||||
|
import com.hypixel.hytale.server.core.event.events.ecs.InventorySetActiveSlotEvent;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.World;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
|
import net.kewwbec.hub.config.HubConfig;
|
||||||
|
import net.kewwbec.hub.koth.KothService;
|
||||||
|
import net.kewwbec.hub.protection.HubWorldFilter;
|
||||||
|
import net.kewwbec.hub.service.BuildModeService;
|
||||||
|
import net.kewwbec.hub.ui.HubMenuPage;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentMap;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
public final class MenuItemActivateSystem extends EntityEventSystem<EntityStore, InventorySetActiveSlotEvent> {
|
||||||
|
|
||||||
|
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||||
|
private static final long DEBOUNCE_MS = 500L;
|
||||||
|
|
||||||
|
private final HubConfig.InventorySection cfg;
|
||||||
|
private final HubWorldFilter filter;
|
||||||
|
@Nullable private final KothService koth;
|
||||||
|
@Nonnull private final BuildModeService buildMode;
|
||||||
|
@Nullable private final HubHudVisibility hudVisibility;
|
||||||
|
private final ConcurrentMap<UUID, Long> lastOpenMs = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public MenuItemActivateSystem(@Nonnull HubConfig.InventorySection cfg,
|
||||||
|
@Nonnull HubWorldFilter filter,
|
||||||
|
@Nonnull BuildModeService buildMode,
|
||||||
|
@Nullable HubHudVisibility hudVisibility,
|
||||||
|
@Nullable KothService koth) {
|
||||||
|
super(InventorySetActiveSlotEvent.class);
|
||||||
|
this.cfg = cfg;
|
||||||
|
this.filter = filter;
|
||||||
|
this.buildMode = buildMode;
|
||||||
|
this.hudVisibility = hudVisibility;
|
||||||
|
this.koth = koth;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nonnull
|
||||||
|
public Query<EntityStore> getQuery() {
|
||||||
|
return Query.any();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(int chunkSlot,
|
||||||
|
@Nonnull ArchetypeChunk<EntityStore> chunk,
|
||||||
|
@Nonnull Store<EntityStore> store,
|
||||||
|
@Nonnull CommandBuffer<EntityStore> commandBuffer,
|
||||||
|
@Nonnull InventorySetActiveSlotEvent event) {
|
||||||
|
if (!cfg.open_menu_on_slot_switch) return;
|
||||||
|
if (event.getPreviousSlot() == event.getNewSlot()) return;
|
||||||
|
byte resting = (byte) Math.max(0, cfg.resting_slot);
|
||||||
|
if (event.getNewSlot() == resting) return;
|
||||||
|
World world = store.getExternalData().getWorld();
|
||||||
|
if (world == null || !filter.isHub(world)) return;
|
||||||
|
|
||||||
|
PlayerRef playerRef = chunk.getComponent(chunkSlot, PlayerRef.getComponentType());
|
||||||
|
if (playerRef == null) return;
|
||||||
|
if (koth != null && koth.isInActiveArena(playerRef.getUuid())) return;
|
||||||
|
if (buildMode.isOn(playerRef.getUuid())) return;
|
||||||
|
if (hudVisibility != null && hudVisibility.isGlobalForceShow()) return;
|
||||||
|
|
||||||
|
Player player = chunk.getComponent(chunkSlot, Player.getComponentType());
|
||||||
|
if (player == null) return;
|
||||||
|
|
||||||
|
Ref<EntityStore> ref = chunk.getReferenceTo(chunkSlot);
|
||||||
|
if (ref == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
playerRef.getPacketHandler().write(new SetActiveSlot(event.getInventorySectionId(), resting));
|
||||||
|
} catch (RuntimeException ignored) {}
|
||||||
|
world.execute(() -> {
|
||||||
|
if (!ref.isValid()) return;
|
||||||
|
Player p = store.getComponent(ref, Player.getComponentType());
|
||||||
|
if (p == null) return;
|
||||||
|
try {
|
||||||
|
p.getInventory().setActiveHotbarSlot(ref, resting, store);
|
||||||
|
} catch (RuntimeException ignored) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
byte menuTrigger = (byte) Math.max(0, cfg.menu_trigger_slot);
|
||||||
|
if (event.getNewSlot() != menuTrigger) return;
|
||||||
|
|
||||||
|
UUID uuid = playerRef.getUuid();
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
Long prev = lastOpenMs.put(uuid, now);
|
||||||
|
if (prev != null && now - prev < DEBOUNCE_MS) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
player.getPageManager().openCustomPage(ref, store, new HubMenuPage(playerRef));
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
LOGGER.at(Level.WARNING).log("Hub menu open via slot-switch failed for %s: %s",
|
||||||
|
playerRef.getUsername(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearPlayer(@Nonnull UUID uuid) {
|
||||||
|
lastOpenMs.remove(uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package net.kewwbec.hub.model;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record DailyReward(
|
||||||
|
@Nonnull UUID uuid,
|
||||||
|
long lastClaimAt,
|
||||||
|
int streak
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import com.hypixel.hytale.component.CommandBuffer;
|
|||||||
import com.hypixel.hytale.component.Store;
|
import com.hypixel.hytale.component.Store;
|
||||||
import com.hypixel.hytale.component.query.Query;
|
import com.hypixel.hytale.component.query.Query;
|
||||||
import com.hypixel.hytale.component.system.EntityEventSystem;
|
import com.hypixel.hytale.component.system.EntityEventSystem;
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
import com.hypixel.hytale.server.core.event.events.ecs.BreakBlockEvent;
|
import com.hypixel.hytale.server.core.event.events.ecs.BreakBlockEvent;
|
||||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
import com.hypixel.hytale.server.core.universe.world.World;
|
import com.hypixel.hytale.server.core.universe.world.World;
|
||||||
@@ -38,9 +39,11 @@ public final class CancelBreakBlockSystem extends EntityEventSystem<EntityStore,
|
|||||||
@Nonnull BreakBlockEvent event) {
|
@Nonnull BreakBlockEvent event) {
|
||||||
if (event.isCancelled()) return;
|
if (event.isCancelled()) return;
|
||||||
World world = store.getExternalData().getWorld();
|
World world = store.getExternalData().getWorld();
|
||||||
if (world == null || !filter.isHub(world)) return;
|
if (!filter.isHub(world)) return;
|
||||||
PlayerRef player = chunk.getComponent(chunkSlot, PlayerRef.getComponentType());
|
PlayerRef player = chunk.getComponent(chunkSlot, PlayerRef.getComponentType());
|
||||||
if (player != null && buildMode.isOn(player.getUuid())) return;
|
if (player != null && buildMode.isOn(player.getUuid())) return;
|
||||||
event.setCancelled(true);
|
event.setCancelled(true);
|
||||||
|
|
||||||
|
player.sendMessage(Message.raw("you broke " + event.getBlockType().getId()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ public final class CancelDamageBlockSystem extends EntityEventSystem<EntityStore
|
|||||||
@Nonnull DamageBlockEvent event) {
|
@Nonnull DamageBlockEvent event) {
|
||||||
if (event.isCancelled()) return;
|
if (event.isCancelled()) return;
|
||||||
World world = store.getExternalData().getWorld();
|
World world = store.getExternalData().getWorld();
|
||||||
if (world == null || !filter.isHub(world)) return;
|
if (!filter.isHub(world)) return;
|
||||||
PlayerRef player = chunk.getComponent(chunkSlot, PlayerRef.getComponentType());
|
PlayerRef player = chunk.getComponent(chunkSlot, PlayerRef.getComponentType());
|
||||||
if (player != null && buildMode.isOn(player.getUuid())) return;
|
if (player != null && buildMode.isOn(player.getUuid())) return;
|
||||||
event.setCancelled(true);
|
event.setCancelled(true);
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package net.kewwbec.hub.protection;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.component.ArchetypeChunk;
|
||||||
|
import com.hypixel.hytale.component.CommandBuffer;
|
||||||
|
import com.hypixel.hytale.component.Store;
|
||||||
|
import com.hypixel.hytale.component.SystemGroup;
|
||||||
|
import com.hypixel.hytale.component.query.Query;
|
||||||
|
import com.hypixel.hytale.server.core.entity.knockback.KnockbackComponent;
|
||||||
|
import com.hypixel.hytale.server.core.modules.entity.damage.Damage;
|
||||||
|
import com.hypixel.hytale.server.core.modules.entity.damage.DamageEventSystem;
|
||||||
|
import com.hypixel.hytale.server.core.modules.entity.damage.DamageModule;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.World;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
|
import net.kewwbec.hub.koth.KothService;
|
||||||
|
import org.joml.Vector3d;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public final class CancelDamageSystem extends DamageEventSystem {
|
||||||
|
|
||||||
|
private final HubWorldFilter filter;
|
||||||
|
@Nullable private final KothService koth;
|
||||||
|
|
||||||
|
public CancelDamageSystem(@Nonnull HubWorldFilter filter, @Nullable KothService koth) {
|
||||||
|
super();
|
||||||
|
this.filter = filter;
|
||||||
|
this.koth = koth;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public SystemGroup<EntityStore> getGroup() {
|
||||||
|
return DamageModule.get().getFilterDamageGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nonnull
|
||||||
|
public Query<EntityStore> getQuery() {
|
||||||
|
return Query.any();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(int chunkSlot,
|
||||||
|
@Nonnull ArchetypeChunk<EntityStore> chunk,
|
||||||
|
@Nonnull Store<EntityStore> store,
|
||||||
|
@Nonnull CommandBuffer<EntityStore> commandBuffer,
|
||||||
|
@Nonnull Damage event) {
|
||||||
|
if (event.isCancelled()) return;
|
||||||
|
World world = store.getExternalData().getWorld();
|
||||||
|
if (!filter.isHub(world)) return;
|
||||||
|
|
||||||
|
if (koth != null) {
|
||||||
|
PlayerRef victim = chunk.getComponent(chunkSlot, PlayerRef.getComponentType());
|
||||||
|
if (victim != null && koth.isInArena(victim.getUuid())) {
|
||||||
|
event.setAmount(0f);
|
||||||
|
amplifyKnockback(event, koth.getKnockbackMultiplier());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void amplifyKnockback(@Nonnull Damage event, double multiplier) {
|
||||||
|
if (multiplier == 1.0) return;
|
||||||
|
KnockbackComponent kb = event.getIfPresentMetaObject(Damage.KNOCKBACK_COMPONENT);
|
||||||
|
if (kb == null) return;
|
||||||
|
Vector3d v = kb.getVelocity();
|
||||||
|
if (v == null) return;
|
||||||
|
kb.setVelocity(new Vector3d(v.x * multiplier, v.y * multiplier, v.z * multiplier));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package net.kewwbec.hub.protection;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.component.ArchetypeChunk;
|
||||||
|
import com.hypixel.hytale.component.CommandBuffer;
|
||||||
|
import com.hypixel.hytale.component.Store;
|
||||||
|
import com.hypixel.hytale.component.query.Query;
|
||||||
|
import com.hypixel.hytale.component.system.EntityEventSystem;
|
||||||
|
import com.hypixel.hytale.server.core.event.events.ecs.DropItemEvent;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.World;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class CancelDropItemSystem extends EntityEventSystem<EntityStore, DropItemEvent> {
|
||||||
|
|
||||||
|
private final HubWorldFilter filter;
|
||||||
|
|
||||||
|
public CancelDropItemSystem(@Nonnull HubWorldFilter filter) {
|
||||||
|
super(DropItemEvent.class);
|
||||||
|
this.filter = filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nonnull
|
||||||
|
public Query<EntityStore> getQuery() {
|
||||||
|
return Query.any();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(int chunkSlot,
|
||||||
|
@Nonnull ArchetypeChunk<EntityStore> chunk,
|
||||||
|
@Nonnull Store<EntityStore> store,
|
||||||
|
@Nonnull CommandBuffer<EntityStore> commandBuffer,
|
||||||
|
@Nonnull DropItemEvent event) {
|
||||||
|
if (event.isCancelled()) return;
|
||||||
|
World world = store.getExternalData().getWorld();
|
||||||
|
if (!filter.isHub(world)) return;
|
||||||
|
event.setCancelled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ public final class CancelPlaceBlockSystem extends EntityEventSystem<EntityStore,
|
|||||||
@Nonnull PlaceBlockEvent event) {
|
@Nonnull PlaceBlockEvent event) {
|
||||||
if (event.isCancelled()) return;
|
if (event.isCancelled()) return;
|
||||||
World world = store.getExternalData().getWorld();
|
World world = store.getExternalData().getWorld();
|
||||||
if (world == null || !filter.isHub(world)) return;
|
if (!filter.isHub(world)) return;
|
||||||
PlayerRef player = chunk.getComponent(chunkSlot, PlayerRef.getComponentType());
|
PlayerRef player = chunk.getComponent(chunkSlot, PlayerRef.getComponentType());
|
||||||
if (player != null && buildMode.isOn(player.getUuid())) return;
|
if (player != null && buildMode.isOn(player.getUuid())) return;
|
||||||
event.setCancelled(true);
|
event.setCancelled(true);
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package net.kewwbec.hub.protection;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.component.ArchetypeChunk;
|
||||||
|
import com.hypixel.hytale.component.CommandBuffer;
|
||||||
|
import com.hypixel.hytale.component.Store;
|
||||||
|
import com.hypixel.hytale.component.query.Query;
|
||||||
|
import com.hypixel.hytale.component.system.tick.EntityTickingSystem;
|
||||||
|
import com.hypixel.hytale.server.core.modules.entitystats.EntityStatMap;
|
||||||
|
import com.hypixel.hytale.server.core.modules.entitystats.EntityStatValue;
|
||||||
|
import com.hypixel.hytale.server.core.modules.entitystats.asset.DefaultEntityStatTypes;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.World;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tops every player's stamina back to its max each tick while they are in a hub world.
|
||||||
|
* Prevents sprinting / swinging from ever draining the bar visibly.
|
||||||
|
*/
|
||||||
|
public final class RefillStaminaSystem extends EntityTickingSystem<EntityStore> {
|
||||||
|
|
||||||
|
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||||
|
private static final Query<EntityStore> QUERY = (Query<EntityStore>) Query.and(new Query[] {
|
||||||
|
(Query) EntityStatMap.getComponentType(),
|
||||||
|
(Query) PlayerRef.getComponentType()
|
||||||
|
});
|
||||||
|
|
||||||
|
private final HubWorldFilter filter;
|
||||||
|
|
||||||
|
public RefillStaminaSystem(@Nonnull HubWorldFilter filter) {
|
||||||
|
this.filter = filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nonnull
|
||||||
|
public Query<EntityStore> getQuery() {
|
||||||
|
return QUERY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void tick(float dt, int systemIndex, @Nonnull Store<EntityStore> store) {
|
||||||
|
World world = store.getExternalData().getWorld();
|
||||||
|
if (world == null || !filter.isHub(world)) return;
|
||||||
|
super.tick(dt, systemIndex, store);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void tick(float dt,
|
||||||
|
int index,
|
||||||
|
@Nonnull ArchetypeChunk<EntityStore> chunk,
|
||||||
|
@Nonnull Store<EntityStore> store,
|
||||||
|
@Nonnull CommandBuffer<EntityStore> commandBuffer) {
|
||||||
|
EntityStatMap statMap = chunk.getComponent(index, EntityStatMap.getComponentType());
|
||||||
|
if (statMap == null) return;
|
||||||
|
int staminaIndex = DefaultEntityStatTypes.getStamina();
|
||||||
|
EntityStatValue stamina = statMap.get(staminaIndex);
|
||||||
|
if (stamina == null) return;
|
||||||
|
float max = stamina.getMax();
|
||||||
|
if (stamina.get() < max) {
|
||||||
|
statMap.setStatValue(staminaIndex, max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package net.kewwbec.hub.service;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.logger.HytaleLogger;
|
||||||
|
import net.kewwbec.hub.config.HubConfig;
|
||||||
|
import net.kewwbec.hub.db.DailyRewardRepository;
|
||||||
|
import net.kewwbec.hub.model.DailyReward;
|
||||||
|
import net.kewwbec.networkcore.api.xp.NetworkXp;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Daily-reward state + claim logic. Backed by MySQL via NetworkCore. Reward amount and cooldown
|
||||||
|
* are config-driven. A successful claim awards network XP through the NetworkXp service.
|
||||||
|
*
|
||||||
|
* Streak rule: if the player claims within {@code cooldown_seconds + streak_grace_seconds} of
|
||||||
|
* their last claim, the streak increments. Past that window, the streak resets to 1.
|
||||||
|
*/
|
||||||
|
public final class DailyRewardService {
|
||||||
|
|
||||||
|
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||||
|
|
||||||
|
private final DailyRewardRepository repo;
|
||||||
|
private final NetworkXp xp;
|
||||||
|
private final HubConfig.RewardsSection cfg;
|
||||||
|
|
||||||
|
public DailyRewardService(@Nonnull DailyRewardRepository repo,
|
||||||
|
@Nullable NetworkXp xp,
|
||||||
|
@Nonnull HubConfig.RewardsSection cfg) {
|
||||||
|
this.repo = repo;
|
||||||
|
this.xp = xp;
|
||||||
|
this.cfg = cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read-only snapshot of a player's reward state.
|
||||||
|
*/
|
||||||
|
public record Snapshot(int streak, boolean canClaim, long secondsUntilReady, long xpPerClaim) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ClaimStatus {
|
||||||
|
OK,
|
||||||
|
NOT_READY,
|
||||||
|
DB_ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of {@link #tryClaim(UUID)}.
|
||||||
|
*/
|
||||||
|
public record ClaimResult(@Nonnull ClaimStatus status,
|
||||||
|
long xpGained,
|
||||||
|
int newStreak,
|
||||||
|
long secondsUntilNext) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public Snapshot snapshot(@Nonnull UUID uuid) {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
try {
|
||||||
|
Optional<DailyReward> existing = repo.find(uuid);
|
||||||
|
if (existing.isEmpty()) {
|
||||||
|
return new Snapshot(0, true, 0L, cfg.xp_per_claim);
|
||||||
|
}
|
||||||
|
DailyReward r = existing.get();
|
||||||
|
long readyAt = r.lastClaimAt() + cfg.cooldown_seconds * 1000L;
|
||||||
|
if (now >= readyAt) {
|
||||||
|
return new Snapshot(r.streak(), true, 0L, cfg.xp_per_claim);
|
||||||
|
}
|
||||||
|
long secondsLeft = Math.max(0L, (readyAt - now) / 1000L);
|
||||||
|
return new Snapshot(r.streak(), false, secondsLeft, cfg.xp_per_claim);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
LOGGER.at(Level.WARNING).log("Reward snapshot failed for %s: %s", uuid, e.getMessage());
|
||||||
|
return new Snapshot(0, false, cfg.cooldown_seconds, cfg.xp_per_claim);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
public ClaimResult tryClaim(@Nonnull UUID uuid) {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
try {
|
||||||
|
Optional<DailyReward> existing = repo.find(uuid);
|
||||||
|
int newStreak;
|
||||||
|
if (existing.isEmpty()) {
|
||||||
|
newStreak = 1;
|
||||||
|
} else {
|
||||||
|
DailyReward r = existing.get();
|
||||||
|
long readyAt = r.lastClaimAt() + cfg.cooldown_seconds * 1000L;
|
||||||
|
if (now < readyAt) {
|
||||||
|
long secondsLeft = Math.max(0L, (readyAt - now) / 1000L);
|
||||||
|
return new ClaimResult(ClaimStatus.NOT_READY, 0L, r.streak(), secondsLeft);
|
||||||
|
}
|
||||||
|
long graceEnd = readyAt + cfg.streak_grace_seconds * 1000L;
|
||||||
|
newStreak = (now <= graceEnd) ? r.streak() + 1 : 1;
|
||||||
|
}
|
||||||
|
repo.upsert(uuid, now, newStreak);
|
||||||
|
long awarded = cfg.xp_per_claim;
|
||||||
|
if (xp != null) {
|
||||||
|
xp.add(uuid, awarded, "hub.daily_reward");
|
||||||
|
} else {
|
||||||
|
LOGGER.at(Level.WARNING).log("Reward claim recorded but NetworkXp unavailable; XP not granted for %s", uuid);
|
||||||
|
awarded = 0L;
|
||||||
|
}
|
||||||
|
return new ClaimResult(ClaimStatus.OK, awarded, newStreak, cfg.cooldown_seconds);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
LOGGER.at(Level.WARNING).log("Reward claim failed for %s: %s", uuid, e.getMessage());
|
||||||
|
return new ClaimResult(ClaimStatus.DB_ERROR, 0L, 0, 0L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package net.kewwbec.hub.ui;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
import com.hypixel.hytale.component.Ref;
|
||||||
|
import com.hypixel.hytale.component.Store;
|
||||||
|
import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime;
|
||||||
|
import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType;
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage;
|
||||||
|
import com.hypixel.hytale.server.core.ui.builder.EventData;
|
||||||
|
import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder;
|
||||||
|
import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
|
import net.kewwbec.hub.service.DailyRewardService;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.awt.Color;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hub daily-rewards page. Markup at
|
||||||
|
* {@code Common/UI/Custom/KweebecNet_Hub/Pages/DailyRewardsPage.ui} uses CoreUI templates
|
||||||
|
* via {@code KweebecNet/Common.ui}. This driver populates dynamic state and handles the
|
||||||
|
* claim event.
|
||||||
|
*/
|
||||||
|
public final class DailyRewardsPage extends InteractiveCustomUIPage<DailyRewardsPage.Data> {
|
||||||
|
|
||||||
|
private final PlayerRef playerRef;
|
||||||
|
private final DailyRewardService rewards;
|
||||||
|
|
||||||
|
public DailyRewardsPage(@Nonnull PlayerRef playerRef, @Nonnull DailyRewardService rewards) {
|
||||||
|
super(playerRef, CustomPageLifetime.CanDismiss, Data.CODEC);
|
||||||
|
this.playerRef = playerRef;
|
||||||
|
this.rewards = rewards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void build(@Nonnull Ref<EntityStore> ref,
|
||||||
|
@Nonnull UICommandBuilder ui,
|
||||||
|
@Nonnull UIEventBuilder events,
|
||||||
|
@Nonnull Store<EntityStore> store) {
|
||||||
|
ui.append("KweebecNet_Hub/Pages/DailyRewardsPage.ui");
|
||||||
|
|
||||||
|
DailyRewardService.Snapshot snap = rewards.snapshot(playerRef.getUuid());
|
||||||
|
|
||||||
|
ui.set("#Subtitle.Text", "Welcome back, " + playerRef.getUsername() + ".");
|
||||||
|
ui.set("#StreakLabel.Text", "Current streak: " + snap.streak() + " day(s)");
|
||||||
|
ui.set("#RewardLabel.Text", "+" + snap.xpPerClaim() + " Network XP");
|
||||||
|
|
||||||
|
if (snap.canClaim()) {
|
||||||
|
ui.set("#StatusLabel.Text", "Your daily reward is ready.");
|
||||||
|
ui.set("#ClaimBtn.Disabled", false);
|
||||||
|
events.addEventBinding(CustomUIEventBindingType.Activating, "#ClaimBtn",
|
||||||
|
EventData.of(Data.KEY_ACTION, Data.ACTION_CLAIM));
|
||||||
|
} else {
|
||||||
|
ui.set("#StatusLabel.Text", "Next reward in " + formatDuration(snap.secondsUntilReady()));
|
||||||
|
ui.set("#ClaimBtn.Disabled", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleDataEvent(@Nonnull Ref<EntityStore> ref,
|
||||||
|
@Nonnull Store<EntityStore> store,
|
||||||
|
@Nonnull Data data) {
|
||||||
|
super.handleDataEvent(ref, store, data);
|
||||||
|
if (Data.ACTION_CLAIM.equals(data.action)) {
|
||||||
|
DailyRewardService.ClaimResult r = rewards.tryClaim(playerRef.getUuid());
|
||||||
|
switch (r.status()) {
|
||||||
|
case OK -> playerRef.sendMessage(Message.raw(
|
||||||
|
"+" + r.xpGained() + " Network XP (streak: " + r.newStreak() + ")"
|
||||||
|
).color(new Color(0x55FF55)));
|
||||||
|
case NOT_READY -> playerRef.sendMessage(Message.raw(
|
||||||
|
"Not ready yet. Try again in " + formatDuration(r.secondsUntilNext())
|
||||||
|
).color(Color.YELLOW));
|
||||||
|
case DB_ERROR -> playerRef.sendMessage(Message.raw(
|
||||||
|
"Reward system unavailable. Try later."
|
||||||
|
).color(Color.RED));
|
||||||
|
}
|
||||||
|
sendUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private static String formatDuration(long seconds) {
|
||||||
|
if (seconds <= 0L) return "now";
|
||||||
|
long h = seconds / 3600L;
|
||||||
|
long m = (seconds % 3600L) / 60L;
|
||||||
|
long s = seconds % 60L;
|
||||||
|
if (h > 0L) return String.format("%dh %02dm", h, m);
|
||||||
|
if (m > 0L) return String.format("%dm %02ds", m, s);
|
||||||
|
return s + "s";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Data {
|
||||||
|
static final String KEY_ACTION = "Action";
|
||||||
|
static final String ACTION_CLAIM = "claim";
|
||||||
|
|
||||||
|
public static final BuilderCodec<Data> CODEC = BuilderCodec.<Data>builder(Data.class, Data::new)
|
||||||
|
.addField(new KeyedCodec<>(KEY_ACTION, Codec.STRING),
|
||||||
|
(d, v) -> d.action = v,
|
||||||
|
d -> d.action)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
public String action;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
package net.kewwbec.hub.ui;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.codec.Codec;
|
||||||
|
import com.hypixel.hytale.codec.KeyedCodec;
|
||||||
|
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||||
|
import com.hypixel.hytale.component.Ref;
|
||||||
|
import com.hypixel.hytale.component.Store;
|
||||||
|
import com.hypixel.hytale.logger.HytaleLogger;
|
||||||
|
import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime;
|
||||||
|
import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType;
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import com.hypixel.hytale.server.core.entity.entities.player.pages.InteractiveCustomUIPage;
|
||||||
|
import com.hypixel.hytale.server.core.ui.builder.EventData;
|
||||||
|
import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder;
|
||||||
|
import com.hypixel.hytale.server.core.ui.builder.UIEventBuilder;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import com.hypixel.hytale.server.core.universe.Universe;
|
||||||
|
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||||
|
import net.kewwbec.coreui.api.AvatarHandle;
|
||||||
|
import net.kewwbec.coreui.api.AvatarRenderType;
|
||||||
|
import net.kewwbec.coreui.api.AvatarRequest;
|
||||||
|
import net.kewwbec.coreui.api.Avatars;
|
||||||
|
import net.kewwbec.networkcore.NetworkCore;
|
||||||
|
import net.kewwbec.networkcore.api.PlayerPresence;
|
||||||
|
import net.kewwbec.networkcore.api.ServerInfo;
|
||||||
|
import net.kewwbec.networkcore.api.ServerRegistry;
|
||||||
|
import net.kewwbec.networkcore.api.guilds.GuildInfo;
|
||||||
|
import net.kewwbec.networkcore.api.guilds.GuildLookup;
|
||||||
|
import net.kewwbec.networkcore.api.xp.LevelInfo;
|
||||||
|
import net.kewwbec.networkcore.api.xp.NetworkXp;
|
||||||
|
import net.kewwbec.networkcore.network.RefRequestService;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
public final class HubMenuPage extends InteractiveCustomUIPage<HubMenuPage.Data> {
|
||||||
|
|
||||||
|
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||||
|
|
||||||
|
private final PlayerRef playerRef;
|
||||||
|
|
||||||
|
public HubMenuPage(@Nonnull PlayerRef playerRef) {
|
||||||
|
super(playerRef, CustomPageLifetime.CanDismiss, Data.CODEC);
|
||||||
|
this.playerRef = playerRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void build(@Nonnull Ref<EntityStore> ref,
|
||||||
|
@Nonnull UICommandBuilder ui,
|
||||||
|
@Nonnull UIEventBuilder events,
|
||||||
|
@Nonnull Store<EntityStore> store) {
|
||||||
|
ui.append("KweebecNet_Hub/Pages/HubMenuPage.ui");
|
||||||
|
|
||||||
|
Stats stats = readStats(playerRef);
|
||||||
|
ui.set("#Username.Text", playerRef.getUsername());
|
||||||
|
ui.set("#StatPlaytime #RowValue.Text", stats.playtime);
|
||||||
|
ui.set("#StatLevel #RowValue.Text", String.valueOf(stats.level));
|
||||||
|
ui.set("#StatGuild #RowValue.Text", stats.guild);
|
||||||
|
ui.set("#StatWorld #RowValue.Text", stats.worldOnline + " online");
|
||||||
|
ui.set("#StatNetwork #RowValue.Text", stats.networkOnline + " online");
|
||||||
|
|
||||||
|
events.addEventBinding(CustomUIEventBindingType.Activating, "#PlayBtn",
|
||||||
|
EventData.of(Data.KEY_ACTION, Data.ACTION_PLAY_SMP));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private AvatarHandle resolveAvatarHandle() {
|
||||||
|
Avatars avatars = NetworkCore.getInstance().findService(Avatars.class);
|
||||||
|
if (avatars == null) return null;
|
||||||
|
AvatarRequest req = AvatarRequest.forUsername(playerRef.getUsername())
|
||||||
|
.renderType(AvatarRenderType.HEAD)
|
||||||
|
.size(256)
|
||||||
|
.rotate(25);
|
||||||
|
AvatarHandle cached = avatars.getCached(playerRef, req);
|
||||||
|
if (cached != null) return cached;
|
||||||
|
try {
|
||||||
|
return avatars.install(playerRef, req);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
((HytaleLogger.Api) LOGGER.at(Level.WARNING).withCause(e)).log("Hub menu avatar install failed for %s", playerRef.getUsername());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleDataEvent(@Nonnull Ref<EntityStore> ref,
|
||||||
|
@Nonnull Store<EntityStore> store,
|
||||||
|
@Nonnull Data data) {
|
||||||
|
super.handleDataEvent(ref, store, data);
|
||||||
|
if (Data.ACTION_PLAY_SMP.equals(data.action)) {
|
||||||
|
sendToSmp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String SMP_ROLE = "us-smp";
|
||||||
|
|
||||||
|
private void sendToSmp() {
|
||||||
|
NetworkCore core = NetworkCore.getInstance();
|
||||||
|
ServerRegistry registry = core.getServerRegistry();
|
||||||
|
RefRequestService transfer = core.getRefRequestService();
|
||||||
|
if (registry == null || transfer == null) {
|
||||||
|
playerRef.sendMessage(Message.raw("SMP transfer unavailable.").color(Color.RED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Collection<ServerInfo> candidates = registry.getServersByRole(SMP_ROLE);
|
||||||
|
ServerInfo target = candidates.stream()
|
||||||
|
.filter(ServerInfo::isReferrable)
|
||||||
|
.min(Comparator.comparingInt(ServerInfo::players))
|
||||||
|
.orElse(null);
|
||||||
|
if (target == null) {
|
||||||
|
playerRef.sendMessage(Message.raw("No SMP server is online right now.").color(Color.YELLOW));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!transfer.referLocal(playerRef, target)) {
|
||||||
|
playerRef.sendMessage(Message.raw("Couldn't send you to " + target.id() + ".").color(Color.RED));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private static Stats readStats(@Nonnull PlayerRef p) {
|
||||||
|
NetworkCore core = NetworkCore.getInstance();
|
||||||
|
int level = 0;
|
||||||
|
NetworkXp xp = core.getNetworkXp();
|
||||||
|
if (xp != null) {
|
||||||
|
LevelInfo info = xp.getInfo(p.getUuid());
|
||||||
|
level = info.level();
|
||||||
|
}
|
||||||
|
String guildName = "-";
|
||||||
|
GuildLookup gl = core.getGuildLookup();
|
||||||
|
if (gl != null) {
|
||||||
|
GuildInfo g = gl.getGuildOf(p.getUuid());
|
||||||
|
if (g != null) guildName = g.name();
|
||||||
|
}
|
||||||
|
int worldOnline = 0;
|
||||||
|
Universe u = Universe.get();
|
||||||
|
if (u != null) worldOnline = u.getPlayers().size();
|
||||||
|
int netOnline = 0;
|
||||||
|
PlayerPresence presence = core.getPlayerPresence();
|
||||||
|
if (presence != null) netOnline = presence.onlineCount();
|
||||||
|
return new Stats("-", level, guildName, worldOnline, netOnline);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Stats(String playtime, int level, String guild, int worldOnline, int networkOnline) {}
|
||||||
|
|
||||||
|
public static final class Data {
|
||||||
|
static final String KEY_ACTION = "Action";
|
||||||
|
static final String ACTION_PLAY_SMP = "play_smp";
|
||||||
|
|
||||||
|
public static final BuilderCodec<Data> CODEC = BuilderCodec.<Data>builder(Data.class, Data::new)
|
||||||
|
.addField(new KeyedCodec<>(KEY_ACTION, Codec.STRING),
|
||||||
|
(d, v) -> d.action = v,
|
||||||
|
d -> d.action)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
public String action;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package net.kewwbec.hub.ui;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.entity.entities.player.hud.CustomUIHud;
|
||||||
|
import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class MenuHintHud extends CustomUIHud {
|
||||||
|
|
||||||
|
private static final String UI_FILE = "KweebecNet_Hub/Hud/MenuHintHud.ui";
|
||||||
|
|
||||||
|
public MenuHintHud(@Nonnull PlayerRef playerRef) {
|
||||||
|
super(playerRef, "KweebecNetMenuHint", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void build(@Nonnull UICommandBuilder ui) {
|
||||||
|
ui.append(UI_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
update(true, new UICommandBuilder());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package net.kewwbec.hub.ui;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.entity.entities.player.hud.CustomUIHud;
|
||||||
|
import com.hypixel.hytale.server.core.ui.builder.UICommandBuilder;
|
||||||
|
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||||
|
import net.kewwbec.hub.listener.HubHudVisibility;
|
||||||
|
import net.kewwbec.networkcore.api.xp.LevelInfo;
|
||||||
|
import net.kewwbec.networkcore.api.xp.NetworkXp;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public final class NetworkXpHud extends CustomUIHud {
|
||||||
|
|
||||||
|
private static final String UI_FILE_MID = "KweebecNet_Hub/Hud/NetworkXpHud.ui";
|
||||||
|
private static final String UI_FILE_LOW = "KweebecNet_Hub/Hud/NetworkXpHud_Low.ui";
|
||||||
|
|
||||||
|
private final NetworkXp xp;
|
||||||
|
@Nullable private final HubHudVisibility hudVisibility;
|
||||||
|
|
||||||
|
public NetworkXpHud(@Nonnull PlayerRef playerRef,
|
||||||
|
@Nullable NetworkXp xp,
|
||||||
|
@Nullable HubHudVisibility hudVisibility) {
|
||||||
|
super(playerRef, "KweebecNetXpBar", 0);
|
||||||
|
this.xp = xp;
|
||||||
|
this.hudVisibility = hudVisibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void build(@Nonnull UICommandBuilder ui) {
|
||||||
|
ui.append(useLowPosition() ? UI_FILE_LOW : UI_FILE_MID);
|
||||||
|
|
||||||
|
LevelInfo info = (xp != null) ? xp.getInfo(getPlayerRef().getUuid()) : null;
|
||||||
|
int level = info != null ? info.level() : 0;
|
||||||
|
long into = info != null ? info.xpIntoLevel() : 0L;
|
||||||
|
long need = info != null ? info.xpForNextLevel() : 0L;
|
||||||
|
|
||||||
|
int fillWeight = 0;
|
||||||
|
int emptyWeight = 1000;
|
||||||
|
if (need > 0L) {
|
||||||
|
double progress = Math.max(0.0, Math.min(1.0, (double) into / (double) need));
|
||||||
|
fillWeight = (int) Math.round(progress * 1000);
|
||||||
|
emptyWeight = 1000 - fillWeight;
|
||||||
|
}
|
||||||
|
ui.set("#XpFill.FlexWeight", fillWeight);
|
||||||
|
ui.set("#XpEmpty.FlexWeight", emptyWeight);
|
||||||
|
|
||||||
|
ui.set("#XpText.Text", "LVL " + level);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refresh() {
|
||||||
|
UICommandBuilder builder = new UICommandBuilder();
|
||||||
|
build(builder);
|
||||||
|
update(true, builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean useLowPosition() {
|
||||||
|
if (hudVisibility == null) return false;
|
||||||
|
return hudVisibility.isHidden(getPlayerRef().getUuid());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
package net.kewwbec.hub.util;
|
||||||
|
|
||||||
|
import com.hypixel.hytale.server.core.Message;
|
||||||
|
import net.kewwbec.networkcore.NetworkCore;
|
||||||
|
import net.kewwbec.networkcore.api.ranks.RankInfo;
|
||||||
|
import net.kewwbec.networkcore.api.ranks.RankLookup;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves Hub message templates into composed Hytale {@link Message}s.
|
||||||
|
*
|
||||||
|
* Supports placeholders:
|
||||||
|
* %player% the player's username
|
||||||
|
* %prefix% their primary rank prefix (or empty)
|
||||||
|
* %color% their primary rank color as a &#RRGGBB code (or empty)
|
||||||
|
* %server% the local server id (looked up via NetworkCore)
|
||||||
|
*
|
||||||
|
* Supports inline color codes:
|
||||||
|
* &#RRGGBB switch to a hex color until the next code or end of line
|
||||||
|
* &r reset to default color
|
||||||
|
*
|
||||||
|
* Legacy {player} is also accepted for backward compatibility.
|
||||||
|
*
|
||||||
|
* The formatter has no internal state; callers construct one per use or share a singleton.
|
||||||
|
*/
|
||||||
|
public final class HubMessageFormatter {
|
||||||
|
|
||||||
|
private HubMessageFormatter() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a single template string into a {@link Message}. The {@code username} and {@code uuid}
|
||||||
|
* identify the subject for placeholder substitution; the rank lookup runs lazily and is
|
||||||
|
* resilient to a missing RankLookup service.
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
public static Message format(@Nonnull String template,
|
||||||
|
@Nonnull String username,
|
||||||
|
@Nonnull UUID uuid) {
|
||||||
|
String resolved = applyPlaceholders(template, username, uuid);
|
||||||
|
return buildColoredMessage(resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private static String applyPlaceholders(@Nonnull String template,
|
||||||
|
@Nonnull String username,
|
||||||
|
@Nonnull UUID uuid) {
|
||||||
|
RankInfo rank = lookupRank(uuid);
|
||||||
|
String prefix = (rank == null || rank.prefix() == null) ? "" : rank.prefix();
|
||||||
|
String colorCode = (rank == null || rank.color() == null || rank.color().isBlank())
|
||||||
|
? "" : "&" + rank.color();
|
||||||
|
String serverId = serverId();
|
||||||
|
|
||||||
|
return template
|
||||||
|
.replace("%player%", username)
|
||||||
|
.replace("{player}", username)
|
||||||
|
.replace("%prefix%", prefix)
|
||||||
|
.replace("%color%", colorCode)
|
||||||
|
.replace("%server%", serverId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static RankInfo lookupRank(@Nonnull UUID uuid) {
|
||||||
|
try {
|
||||||
|
RankLookup l = NetworkCore.getInstance().getRankLookup();
|
||||||
|
return l == null ? null : l.getPrimaryRank(uuid);
|
||||||
|
} catch (RuntimeException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private static String serverId() {
|
||||||
|
try { return NetworkCore.getInstance().getServerId(); }
|
||||||
|
catch (RuntimeException ignored) { return ""; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a string containing inline color codes ({@code &#RRGGBB} and {@code &r}) and builds a
|
||||||
|
* {@link Message} where each segment carries its own color. Plain text with no codes returns a
|
||||||
|
* single uncolored raw message.
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
public static Message buildColoredMessage(@Nonnull String text) {
|
||||||
|
if (text.isEmpty()) return Message.raw("");
|
||||||
|
|
||||||
|
Message composed = Message.empty();
|
||||||
|
StringBuilder segment = new StringBuilder();
|
||||||
|
Color currentColor = null;
|
||||||
|
boolean anySegmentEmitted = false;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
while (i < text.length()) {
|
||||||
|
char ch = text.charAt(i);
|
||||||
|
// Hex color code: &#RRGGBB
|
||||||
|
if (ch == '&' && i + 8 <= text.length() && text.charAt(i + 1) == '#'
|
||||||
|
&& isHex6(text, i + 2)) {
|
||||||
|
if (segment.length() > 0) {
|
||||||
|
composed = composed.insert(colorize(Message.raw(segment.toString()), currentColor));
|
||||||
|
anySegmentEmitted = true;
|
||||||
|
segment.setLength(0);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
currentColor = new Color(Integer.parseInt(text.substring(i + 2, i + 8), 16));
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
currentColor = null;
|
||||||
|
}
|
||||||
|
i += 8;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Reset: &r
|
||||||
|
if (ch == '&' && i + 2 <= text.length() && text.charAt(i + 1) == 'r') {
|
||||||
|
if (segment.length() > 0) {
|
||||||
|
composed = composed.insert(colorize(Message.raw(segment.toString()), currentColor));
|
||||||
|
anySegmentEmitted = true;
|
||||||
|
segment.setLength(0);
|
||||||
|
}
|
||||||
|
currentColor = null;
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
segment.append(ch);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if (segment.length() > 0) {
|
||||||
|
composed = composed.insert(colorize(Message.raw(segment.toString()), currentColor));
|
||||||
|
anySegmentEmitted = true;
|
||||||
|
}
|
||||||
|
return anySegmentEmitted ? composed : Message.raw(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private static Message colorize(@Nonnull Message m, @Nullable Color c) {
|
||||||
|
return c == null ? m : m.color(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isHex6(@Nonnull String s, int from) {
|
||||||
|
if (from + 6 > s.length()) return false;
|
||||||
|
for (int i = from; i < from + 6; i++) {
|
||||||
|
char ch = s.charAt(i);
|
||||||
|
if (!((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F'))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
Group #KothHud {
|
||||||
|
Anchor: (Top: 80, Right: 20, Width: 260, Height: 200);
|
||||||
|
LayoutMode: Top;
|
||||||
|
Background: #1F2A40;
|
||||||
|
OutlineColor: #D4AF37;
|
||||||
|
OutlineSize: 1;
|
||||||
|
Padding: (Full: 12);
|
||||||
|
|
||||||
|
Label #KothTitle {
|
||||||
|
Anchor: (Height: 22, Bottom: 4);
|
||||||
|
Style: (FontSize: 16, TextColor: #D4AF37, HorizontalAlignment: Center, RenderBold: true, LetterSpacing: 2);
|
||||||
|
Text: "KING OF THE HILL";
|
||||||
|
}
|
||||||
|
|
||||||
|
Label #KothTimer {
|
||||||
|
Anchor: (Height: 16, Bottom: 10);
|
||||||
|
Style: (FontSize: 12, TextColor: #8898B5, HorizontalAlignment: Center, RenderItalics: true);
|
||||||
|
Text: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Group #KothSeparatorTop {
|
||||||
|
Anchor: (Height: 1, Bottom: 8);
|
||||||
|
Background: #2A3A55;
|
||||||
|
}
|
||||||
|
|
||||||
|
Label #KothKing {
|
||||||
|
Anchor: (Height: 18, Bottom: 8);
|
||||||
|
Style: (FontSize: 14, TextColor: #55FF55, HorizontalAlignment: Center, RenderBold: true);
|
||||||
|
Text: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Label #KothBoardLine1 {
|
||||||
|
Anchor: (Height: 14, Bottom: 2);
|
||||||
|
Style: (FontSize: 12, TextColor: #FFFFFF);
|
||||||
|
Text: "";
|
||||||
|
}
|
||||||
|
Label #KothBoardLine2 {
|
||||||
|
Anchor: (Height: 14, Bottom: 2);
|
||||||
|
Style: (FontSize: 12, TextColor: #DDE3EE);
|
||||||
|
Text: "";
|
||||||
|
}
|
||||||
|
Label #KothBoardLine3 {
|
||||||
|
Anchor: (Height: 14, Bottom: 6);
|
||||||
|
Style: (FontSize: 12, TextColor: #BBBBBB);
|
||||||
|
Text: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Group #KothSeparatorBottom {
|
||||||
|
Anchor: (Height: 1, Bottom: 4);
|
||||||
|
Background: #2A3A55;
|
||||||
|
}
|
||||||
|
|
||||||
|
Label #KothYourStats {
|
||||||
|
Anchor: (Height: 14);
|
||||||
|
Style: (FontSize: 12, TextColor: #FFAA55, HorizontalAlignment: Center, RenderBold: true);
|
||||||
|
Text: "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
Group #MenuHintHud {
|
||||||
|
Anchor: (Bottom: 50, Right: 50, Width: 160, Height: 28);
|
||||||
|
LayoutMode: Right;
|
||||||
|
|
||||||
|
ActionButton #MenuHintAction {
|
||||||
|
ActionName: "Menu";
|
||||||
|
KeyBindingLabel: "1";
|
||||||
|
Alignment: Left;
|
||||||
|
HitTestVisible: false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
Group #NetworkXpHud {
|
||||||
|
Anchor: (Bottom: 155, Height: 18, Left: 0, Right: 0);
|
||||||
|
LayoutMode: CenterMiddle;
|
||||||
|
|
||||||
|
Group #XpBar {
|
||||||
|
Anchor: (Width: 360, Height: 18);
|
||||||
|
Background: #1F2A40;
|
||||||
|
OutlineColor: #D4AF37;
|
||||||
|
OutlineSize: 1;
|
||||||
|
|
||||||
|
Group #XpRow {
|
||||||
|
Anchor: (Full: 1);
|
||||||
|
LayoutMode: Left;
|
||||||
|
Group #XpFill {
|
||||||
|
FlexWeight: 0;
|
||||||
|
Background: #D4AF37;
|
||||||
|
}
|
||||||
|
Group #XpEmpty {
|
||||||
|
FlexWeight: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Label #XpText {
|
||||||
|
Anchor: (Full: 0);
|
||||||
|
Style: (FontSize: 12, TextColor: #FFFFFF, HorizontalAlignment: Center, VerticalAlignment: Center, RenderBold: true);
|
||||||
|
Text: "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
Group #NetworkXpHud {
|
||||||
|
Anchor: (Bottom: 50, Height: 18, Left: 0, Right: 0);
|
||||||
|
LayoutMode: CenterMiddle;
|
||||||
|
|
||||||
|
Group #XpBar {
|
||||||
|
Anchor: (Width: 360, Height: 18);
|
||||||
|
Background: #1F2A40;
|
||||||
|
OutlineColor: #D4AF37;
|
||||||
|
OutlineSize: 1;
|
||||||
|
|
||||||
|
Group #XpRow {
|
||||||
|
Anchor: (Full: 1);
|
||||||
|
LayoutMode: Left;
|
||||||
|
Group #XpFill {
|
||||||
|
FlexWeight: 0;
|
||||||
|
Background: #D4AF37;
|
||||||
|
}
|
||||||
|
Group #XpEmpty {
|
||||||
|
FlexWeight: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Label #XpText {
|
||||||
|
Anchor: (Full: 0);
|
||||||
|
Style: (FontSize: 12, TextColor: #FFFFFF, HorizontalAlignment: Center, VerticalAlignment: Center, RenderBold: true);
|
||||||
|
Text: "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
$KW = "../../KweebecNet/Common.ui";
|
||||||
|
|
||||||
|
$KW.@KweebecPageOverlay {
|
||||||
|
Anchor: (Full: 0);
|
||||||
|
|
||||||
|
$KW.@KweebecPanel #Frame {
|
||||||
|
@Text = "HUB REWARDS";
|
||||||
|
@Anchor = Anchor(Width: 560, Height: 420);
|
||||||
|
|
||||||
|
#Content {
|
||||||
|
Group {
|
||||||
|
LayoutMode: Top;
|
||||||
|
FlexWeight: 1;
|
||||||
|
Padding: (Full: 28);
|
||||||
|
|
||||||
|
$KW.@KweebecSubtitle #Subtitle {
|
||||||
|
@Anchor = Anchor(Bottom: 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
Label #StreakLabel {
|
||||||
|
Anchor: (Height: 24, Bottom: 4);
|
||||||
|
Style: (FontSize: 15, TextColor: #FFAA55, HorizontalAlignment: Center, RenderBold: true);
|
||||||
|
Text: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Label #StatusLabel {
|
||||||
|
Anchor: (Height: 20, Bottom: 18);
|
||||||
|
Style: (FontSize: 13, TextColor: #BBBBBB, HorizontalAlignment: Center);
|
||||||
|
Text: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Label #RewardLabel {
|
||||||
|
Anchor: (Height: 36, Bottom: 22);
|
||||||
|
Style: (FontSize: 22, TextColor: #55FF55, HorizontalAlignment: Center, RenderBold: true);
|
||||||
|
Text: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
$KW.@KweebecPrimaryButton #ClaimBtn {
|
||||||
|
@Text = "CLAIM REWARD";
|
||||||
|
@Anchor = Anchor(Horizontal: 60);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
$KW = "../../KweebecNet/Common.ui";
|
||||||
|
|
||||||
|
$KW.@KweebecPageOverlay {
|
||||||
|
Anchor: (Full: 0);
|
||||||
|
|
||||||
|
$KW.@KweebecPanel #Frame {
|
||||||
|
@Text = "KWEEBEC NETWORK";
|
||||||
|
@Anchor = Anchor(Width: 1100, Height: 660);
|
||||||
|
|
||||||
|
#Content {
|
||||||
|
Group {
|
||||||
|
LayoutMode: Left;
|
||||||
|
FlexWeight: 1;
|
||||||
|
|
||||||
|
Group #Sidebar {
|
||||||
|
LayoutMode: Top;
|
||||||
|
Anchor: (Width: 280);
|
||||||
|
Padding: (Full: 20);
|
||||||
|
|
||||||
|
$KW.@KweebecHyvatar #AvatarFrame {
|
||||||
|
@Anchor = Anchor(Height: 220, Bottom: 18);
|
||||||
|
}
|
||||||
|
|
||||||
|
Label #Username {
|
||||||
|
Anchor: (Height: 30, Bottom: 18);
|
||||||
|
Style: (FontSize: 20, TextColor: #FFFFFF, HorizontalAlignment: Center, RenderBold: true);
|
||||||
|
Text: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
$KW.@KweebecSeparator {}
|
||||||
|
|
||||||
|
$KW.@KweebecStatRow #StatPlaytime {
|
||||||
|
@Label = "Playtime";
|
||||||
|
}
|
||||||
|
$KW.@KweebecStatRow #StatLevel {
|
||||||
|
@Label = "Total Level";
|
||||||
|
@ValueColor = #55CCFF;
|
||||||
|
}
|
||||||
|
$KW.@KweebecStatRow #StatGuild {
|
||||||
|
@Label = "Guild";
|
||||||
|
}
|
||||||
|
|
||||||
|
$KW.@KweebecSeparator {}
|
||||||
|
|
||||||
|
$KW.@KweebecStatRow #StatWorld {
|
||||||
|
@Label = "World";
|
||||||
|
}
|
||||||
|
$KW.@KweebecStatRow #StatNetwork {
|
||||||
|
@Label = "Network";
|
||||||
|
}
|
||||||
|
|
||||||
|
$KW.@KweebecSeparator {}
|
||||||
|
|
||||||
|
Label #DiscordLine {
|
||||||
|
Anchor: (Height: 22);
|
||||||
|
Style: (FontSize: 14, TextColor: #7289DA, HorizontalAlignment: Center, RenderBold: true);
|
||||||
|
Text: "discord.gg/kweebec";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Group #CardRow {
|
||||||
|
LayoutMode: Left;
|
||||||
|
FlexWeight: 1;
|
||||||
|
Padding: (Left: 40, Right: 40, Top: 20, Bottom: 20);
|
||||||
|
|
||||||
|
$KW.@KweebecCard #SmpCard {
|
||||||
|
@Anchor = Anchor(Width: 240, Height: 420, Right: 20);
|
||||||
|
|
||||||
|
Group #SmpIconPanel {
|
||||||
|
LayoutMode: CenterMiddle;
|
||||||
|
Anchor: (Height: 180, Bottom: 16);
|
||||||
|
Background: #0E1422;
|
||||||
|
OutlineColor: #2A3A55;
|
||||||
|
OutlineSize: 1;
|
||||||
|
ItemIcon #SmpIcon {
|
||||||
|
Anchor: (Width: 128, Height: 128);
|
||||||
|
ItemId: "Plant_Fruit_Apple";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Label #SmpCardTitle {
|
||||||
|
Anchor: (Height: 28, Bottom: 6);
|
||||||
|
Style: (FontSize: 22, TextColor: #FFFFFF, HorizontalAlignment: Center, RenderBold: true, LetterSpacing: 2);
|
||||||
|
Text: "SMP";
|
||||||
|
}
|
||||||
|
Label #SmpCardDescription {
|
||||||
|
FlexWeight: 1;
|
||||||
|
Style: (FontSize: 13, TextColor: #8898B5, HorizontalAlignment: Center, Wrap: true);
|
||||||
|
Text: "Survival multiplayer.";
|
||||||
|
}
|
||||||
|
$KW.@KweebecPrimaryButton #PlayBtn {
|
||||||
|
@Text = "PLAY";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$KW.@KweebecCard #ComingSoon1 {
|
||||||
|
@Anchor = Anchor(Width: 240, Height: 420, Right: 20);
|
||||||
|
@Background = #161C2C;
|
||||||
|
@OutlineColor = #232E48;
|
||||||
|
|
||||||
|
Group #ComingSoon1IconPanel {
|
||||||
|
LayoutMode: CenterMiddle;
|
||||||
|
Anchor: (Height: 180, Bottom: 16);
|
||||||
|
Background: #0B1020;
|
||||||
|
OutlineColor: #232E48;
|
||||||
|
OutlineSize: 1;
|
||||||
|
Label {
|
||||||
|
Style: (FontSize: 64, TextColor: #3F4A66, HorizontalAlignment: Center, RenderBold: true);
|
||||||
|
Text: "?";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
Anchor: (Height: 28, Bottom: 6);
|
||||||
|
Style: (FontSize: 18, TextColor: #5A6580, HorizontalAlignment: Center, RenderBold: true, LetterSpacing: 2);
|
||||||
|
Text: "COMING SOON";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$KW.@KweebecCard #ComingSoon2 {
|
||||||
|
@Anchor = Anchor(Width: 240, Height: 420);
|
||||||
|
@Background = #161C2C;
|
||||||
|
@OutlineColor = #232E48;
|
||||||
|
|
||||||
|
Group #ComingSoon2IconPanel {
|
||||||
|
LayoutMode: CenterMiddle;
|
||||||
|
Anchor: (Height: 180, Bottom: 16);
|
||||||
|
Background: #0B1020;
|
||||||
|
OutlineColor: #232E48;
|
||||||
|
OutlineSize: 1;
|
||||||
|
Label {
|
||||||
|
Style: (FontSize: 64, TextColor: #3F4A66, HorizontalAlignment: Center, RenderBold: true);
|
||||||
|
Text: "?";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
Anchor: (Height: 28, Bottom: 6);
|
||||||
|
Style: (FontSize: 18, TextColor: #5A6580, HorizontalAlignment: Center, RenderBold: true, LetterSpacing: 2);
|
||||||
|
Text: "COMING SOON";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,14 +2,15 @@
|
|||||||
"Group": "kewwbec",
|
"Group": "kewwbec",
|
||||||
"Name": "Hub",
|
"Name": "Hub",
|
||||||
"Version": "0.1.0",
|
"Version": "0.1.0",
|
||||||
"Description": "Lobby/hub behavior: protection, welcome message, spawn-on-join",
|
"Description": "Lobby/hub behavior: protection, welcome message, spawn-on-join, daily rewards",
|
||||||
"IncludesAssetPack": false,
|
"IncludesAssetPack": true,
|
||||||
"Authors": [
|
"Authors": [
|
||||||
{"Name": "kewwbec"}
|
{"Name": "kewwbec"}
|
||||||
],
|
],
|
||||||
"ServerVersion": "*",
|
"ServerVersion": "*",
|
||||||
"Dependencies": {
|
"Dependencies": {
|
||||||
"kewwbec:NetworkCore": "*"
|
"kewwbec:NetworkCore": "*",
|
||||||
|
"kewwbec:CoreUI": "*"
|
||||||
},
|
},
|
||||||
"OptionalDependencies": {},
|
"OptionalDependencies": {},
|
||||||
"DisabledByDefault": false,
|
"DisabledByDefault": false,
|
||||||
|
|||||||
Reference in New Issue
Block a user