start of better hub integration for users
This commit is contained in:
@@ -4,6 +4,9 @@ public final class HubPermissionsNodes {
|
||||
|
||||
public static final String ROOT = "networkhub";
|
||||
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() {
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
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.server.core.HytaleServer;
|
||||
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.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.JavaPluginInit;
|
||||
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.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.MenuCommand;
|
||||
import net.kewwbec.hub.command.RewardsCommand;
|
||||
import net.kewwbec.hub.command.UnhideHubUICommand;
|
||||
import net.kewwbec.hub.config.HubConfig;
|
||||
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.HubMessageListener;
|
||||
import net.kewwbec.hub.protection.CancelBreakBlockSystem;
|
||||
import net.kewwbec.hub.protection.CancelDamageBlockSystem;
|
||||
import net.kewwbec.hub.protection.CancelPlaceBlockSystem;
|
||||
import net.kewwbec.hub.protection.HubWorldFilter;
|
||||
import net.kewwbec.hub.listener.MenuItemActivateSystem;
|
||||
import net.kewwbec.hub.protection.*;
|
||||
import net.kewwbec.hub.service.BuildModeService;
|
||||
import net.kewwbec.hub.service.DailyRewardService;
|
||||
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 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;
|
||||
|
||||
public final class HubPlugin extends JavaPlugin {
|
||||
@@ -34,6 +63,14 @@ public final class HubPlugin extends JavaPlugin {
|
||||
private BuildModeService buildMode;
|
||||
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) {
|
||||
super(init);
|
||||
}
|
||||
@@ -56,15 +93,29 @@ public final class HubPlugin extends JavaPlugin {
|
||||
this.worldService = new HubWorldService(config);
|
||||
HubJoinListener joinListener = new HubJoinListener(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());
|
||||
worldService.applyToConfiguredWorlds();
|
||||
|
||||
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(RemovedPlayerFromWorldEvent.class, messageListener::onRemove);
|
||||
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) {
|
||||
getEntityStoreRegistry().registerSystem(new CancelBreakBlockSystem(filter, buildMode));
|
||||
}
|
||||
@@ -74,19 +125,161 @@ public final class HubPlugin extends JavaPlugin {
|
||||
if (config.protection.disable_block_damage) {
|
||||
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");
|
||||
}
|
||||
|
||||
private void onDisconnect(@Nonnull PlayerDisconnectEvent event) {
|
||||
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
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.storage.EntityStore;
|
||||
import net.kewwbec.hub.HubPermissionsNodes;
|
||||
import net.kewwbec.hub.koth.KothService;
|
||||
import net.kewwbec.hub.listener.HubHudVisibility;
|
||||
import net.kewwbec.hub.service.BuildModeService;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
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 {
|
||||
|
||||
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");
|
||||
this.buildMode = buildMode;
|
||||
this.hudVisibility = hudVisibility;
|
||||
this.koth = koth;
|
||||
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) {
|
||||
boolean nowOn = buildMode.toggle(sender.getUuid());
|
||||
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 {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public final class HubConfig {
|
||||
@@ -9,6 +10,9 @@ public final class HubConfig {
|
||||
public ProtectionSection protection = new ProtectionSection();
|
||||
public MessagesSection messages = new MessagesSection();
|
||||
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 {
|
||||
/**
|
||||
@@ -24,6 +28,12 @@ public final class HubConfig {
|
||||
public boolean disable_block_place = true;
|
||||
/** Cancel DamageBlockEvent in hub worlds (mining progress). */
|
||||
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. */
|
||||
public boolean disable_pvp = true;
|
||||
/** WorldConfig.isFallDamageEnabled = false on hub worlds. */
|
||||
@@ -37,18 +47,34 @@ public final class HubConfig {
|
||||
|
||||
public static final class MessagesSection {
|
||||
/**
|
||||
* Private welcome shown only to the joining player. {player} is substituted.
|
||||
* Empty/null disables.
|
||||
* Private welcome lines shown only to the joining player, one chat message per line.
|
||||
* 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.
|
||||
* Empty/null suppresses the join broadcast entirely.
|
||||
* Replaces Hytale's default join broadcast on hub worlds. Supports the same placeholders
|
||||
* 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.
|
||||
* Empty/null suppresses the leave broadcast entirely.
|
||||
* Replaces Hytale's default leave broadcast on hub worlds. Empty/null suppresses it.
|
||||
*/
|
||||
public String leave_announcement = "";
|
||||
}
|
||||
@@ -59,4 +85,41 @@ public final class HubConfig {
|
||||
*/
|
||||
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.protection == null) parsed.protection = new HubConfig.ProtectionSection();
|
||||
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.rewards == null) parsed.rewards = new HubConfig.RewardsSection();
|
||||
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.logger.HytaleLogger;
|
||||
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.modules.entity.teleport.Teleport;
|
||||
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 net.kewwbec.hub.config.HubConfig;
|
||||
import net.kewwbec.hub.protection.HubWorldFilter;
|
||||
import net.kewwbec.hub.util.HubMessageFormatter;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
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
|
||||
* join/leave messages on AddPlayerToWorldEvent / RemovedPlayerFromWorldEvent).
|
||||
* Lines run through {@link HubMessageFormatter}, which resolves %player% / %prefix% / %color% /
|
||||
* %server% placeholders and parses inline &#RRGGBB color codes into composed messages.
|
||||
*
|
||||
* Join/leave broadcasts are handled by HubMessageListener (AddPlayerToWorldEvent /
|
||||
* RemovedPlayerFromWorldEvent).
|
||||
*/
|
||||
public final class HubJoinListener {
|
||||
|
||||
@@ -63,10 +66,12 @@ public final class HubJoinListener {
|
||||
}
|
||||
}
|
||||
|
||||
if (config.messages.welcome != null && !config.messages.welcome.isBlank()) {
|
||||
String welcome = config.messages.welcome.replace("{player}", playerRef.getUsername());
|
||||
List<String> lines = config.messages.welcome_lines;
|
||||
if (lines == null || lines.isEmpty()) return;
|
||||
for (String line : lines) {
|
||||
if (line == null || line.isBlank()) continue;
|
||||
try {
|
||||
playerRef.sendMessage(Message.raw(welcome).color(Color.YELLOW));
|
||||
playerRef.sendMessage(HubMessageFormatter.format(line, playerRef.getUsername(), playerRef.getUuid()));
|
||||
} 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.RemovedPlayerFromWorldEvent;
|
||||
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 net.kewwbec.hub.util.HubMessageFormatter;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Replaces (or suppresses) Hytale's default join/leave broadcasts on hub worlds.
|
||||
*
|
||||
* AddPlayerToWorldEvent / RemovedPlayerFromWorldEvent are fired by Hytale's player-add/remove
|
||||
* systems with a default joinMessage / leaveMessage. The systems check shouldBroadcastJoinMessage
|
||||
* (or leave) after dispatch and broadcast accordingly. We rewrite the message text here, or
|
||||
* suppress entirely if the configured format is blank.
|
||||
* Templates run through {@link HubMessageFormatter}: %player%, %prefix%, %color%, %server%
|
||||
* placeholders and inline &#RRGGBB color codes. Empty template suppresses the broadcast.
|
||||
*/
|
||||
public final class HubMessageListener {
|
||||
|
||||
private static final UUID NIL = new UUID(0L, 0L);
|
||||
|
||||
private final HubConfig config;
|
||||
private final HubWorldFilter filter;
|
||||
|
||||
@@ -33,32 +34,35 @@ public final class HubMessageListener {
|
||||
|
||||
public void onAdd(@Nonnull AddPlayerToWorldEvent event) {
|
||||
if (!filter.isHub(event.getWorld())) return;
|
||||
String username = resolveUsername(event.getHolder());
|
||||
String template = config.messages.join_announcement;
|
||||
if (template == null || template.isBlank()) {
|
||||
event.setBroadcastJoinMessage(false);
|
||||
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);
|
||||
}
|
||||
|
||||
public void onRemove(@Nonnull RemovedPlayerFromWorldEvent event) {
|
||||
if (!filter.isHub(event.getWorld())) return;
|
||||
String username = resolveUsername(event.getHolder());
|
||||
String template = config.messages.leave_announcement;
|
||||
if (template == null || template.isBlank()) {
|
||||
event.setBroadcastLeaveMessage(false);
|
||||
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);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static String resolveUsername(@Nullable Holder<EntityStore> holder) {
|
||||
if (holder == null) return "?";
|
||||
private static Subject resolveSubject(@Nullable Holder<EntityStore> holder) {
|
||||
if (holder == null) return new Subject("?", NIL);
|
||||
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.query.Query;
|
||||
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.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.world.World;
|
||||
@@ -38,9 +39,11 @@ public final class CancelBreakBlockSystem extends EntityEventSystem<EntityStore,
|
||||
@Nonnull BreakBlockEvent event) {
|
||||
if (event.isCancelled()) return;
|
||||
World world = store.getExternalData().getWorld();
|
||||
if (world == null || !filter.isHub(world)) return;
|
||||
if (!filter.isHub(world)) return;
|
||||
PlayerRef player = chunk.getComponent(chunkSlot, PlayerRef.getComponentType());
|
||||
if (player != null && buildMode.isOn(player.getUuid())) return;
|
||||
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) {
|
||||
if (event.isCancelled()) return;
|
||||
World world = store.getExternalData().getWorld();
|
||||
if (world == null || !filter.isHub(world)) return;
|
||||
if (!filter.isHub(world)) return;
|
||||
PlayerRef player = chunk.getComponent(chunkSlot, PlayerRef.getComponentType());
|
||||
if (player != null && buildMode.isOn(player.getUuid())) return;
|
||||
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) {
|
||||
if (event.isCancelled()) return;
|
||||
World world = store.getExternalData().getWorld();
|
||||
if (world == null || !filter.isHub(world)) return;
|
||||
if (!filter.isHub(world)) return;
|
||||
PlayerRef player = chunk.getComponent(chunkSlot, PlayerRef.getComponentType());
|
||||
if (player != null && buildMode.isOn(player.getUuid())) return;
|
||||
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",
|
||||
"Name": "Hub",
|
||||
"Version": "0.1.0",
|
||||
"Description": "Lobby/hub behavior: protection, welcome message, spawn-on-join",
|
||||
"IncludesAssetPack": false,
|
||||
"Description": "Lobby/hub behavior: protection, welcome message, spawn-on-join, daily rewards",
|
||||
"IncludesAssetPack": true,
|
||||
"Authors": [
|
||||
{"Name": "kewwbec"}
|
||||
],
|
||||
"ServerVersion": "*",
|
||||
"Dependencies": {
|
||||
"kewwbec:NetworkCore": "*"
|
||||
"kewwbec:NetworkCore": "*",
|
||||
"kewwbec:CoreUI": "*"
|
||||
},
|
||||
"OptionalDependencies": {},
|
||||
"DisabledByDefault": false,
|
||||
|
||||
Reference in New Issue
Block a user