diff --git a/src/main/java/net/kewwbec/networkcore/NetworkCore.java b/src/main/java/net/kewwbec/networkcore/NetworkCore.java
index 7082e73..7ce99e1 100644
--- a/src/main/java/net/kewwbec/networkcore/NetworkCore.java
+++ b/src/main/java/net/kewwbec/networkcore/NetworkCore.java
@@ -13,6 +13,7 @@ import net.kewwbec.networkcore.api.MetricsService;
import net.kewwbec.networkcore.api.PlayerPresence;
import net.kewwbec.networkcore.api.RpcClient;
import net.kewwbec.networkcore.api.ServerRegistry;
+import net.kewwbec.networkcore.api.VanishService;
import net.kewwbec.networkcore.api.events.ServerLeftNetworkEvent;
import net.kewwbec.networkcore.bus.RedisMessageBus;
import net.kewwbec.networkcore.command.NetworkCoreCommand;
@@ -46,6 +47,11 @@ import net.kewwbec.networkcore.xp.XpSchemaBootstrap;
import net.kewwbec.networkcore.presence.PlayerPresenceService;
import net.kewwbec.networkcore.registry.RedisServerRegistry;
import net.kewwbec.networkcore.rpc.RpcClientImpl;
+import net.kewwbec.networkcore.vanish.PlayerListVanishFilter;
+import net.kewwbec.networkcore.vanish.VanishEnforcer;
+import net.kewwbec.networkcore.vanish.VanishServiceImpl;
+
+import com.hypixel.hytale.server.core.event.events.player.PlayerReadyEvent;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -75,6 +81,7 @@ public final class NetworkCore extends JavaPlugin {
@Nullable private RefRequestService refRequestService;
@Nullable private LockService lockService;
@Nullable private ShutdownHandoff shutdownHandoff;
+ @Nullable private VanishServiceImpl vanishService;
public NetworkCore(@Nonnull JavaPluginInit init) {
super(init);
@@ -164,6 +171,13 @@ public final class NetworkCore extends JavaPlugin {
this.alertService = new AlertService(messageBus, serverId);
alertService.start();
+ this.vanishService = new VanishServiceImpl(messageBus, serverId);
+ vanishService.start();
+ services.register(VanishService.class, vanishService);
+ VanishEnforcer vanishEnforcer = new VanishEnforcer(vanishService);
+ getEventRegistry().registerGlobal(PlayerReadyEvent.class, vanishEnforcer::onPlayerReady);
+ new PlayerListVanishFilter(vanishService).register();
+
this.refRequestService = new RefRequestService(messageBus, serverId);
refRequestService.start();
@@ -185,7 +199,7 @@ public final class NetworkCore extends JavaPlugin {
});
getCommandRegistry().registerCommand(new AlertCommand(alertService));
- getCommandRegistry().registerCommand(new GlistCommand(serverRegistry, presenceService));
+ getCommandRegistry().registerCommand(new GlistCommand(serverRegistry, presenceService, vanishService));
getCommandRegistry().registerCommand(new HubCommand(lobbyPicker, refRequestService, serverId));
getCommandRegistry().registerCommand(new SendCommand(serverRegistry, presenceService, refRequestService));
getCommandRegistry().registerCommand(new SendAllCommand(serverRegistry, presenceService, refRequestService, serverId));
@@ -227,6 +241,10 @@ public final class NetworkCore extends JavaPlugin {
try { alertService.stop(); } catch (RuntimeException ignored) {}
alertService = null;
}
+ if (vanishService != null) {
+ try { vanishService.stop(); } catch (RuntimeException ignored) {}
+ vanishService = null;
+ }
if (presenceService != null) {
try { presenceService.stop(); } catch (RuntimeException ignored) {}
presenceService = null;
@@ -288,6 +306,8 @@ public final class NetworkCore extends JavaPlugin {
@Nullable
public NetworkXp getNetworkXp() { return services.get(NetworkXp.class); }
@Nullable
+ public VanishService getVanishService() { return services.get(VanishService.class); }
+ @Nullable
public RefRequestService getRefRequestService() { return refRequestService; }
@Nullable
diff --git a/src/main/java/net/kewwbec/networkcore/api/VanishService.java b/src/main/java/net/kewwbec/networkcore/api/VanishService.java
new file mode 100644
index 0000000..5daefbf
--- /dev/null
+++ b/src/main/java/net/kewwbec/networkcore/api/VanishService.java
@@ -0,0 +1,48 @@
+package net.kewwbec.networkcore.api;
+
+import com.hypixel.hytale.server.core.universe.PlayerRef;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.BiConsumer;
+
+/**
+ * Cross-server vanish state. Source of truth shared via the {@link MessageBus};
+ * each server caches and enforces locally (HiddenPlayersManager + join-broadcast
+ * suppression by consuming plugins).
+ *
+ *
Players with the {@code networkstaff.vanish} permission can see vanished
+ * players regardless. Everyone else sees them as if they weren't there.
+ */
+public interface VanishService {
+
+ boolean isVanished(@Nonnull UUID playerUuid);
+
+ /**
+ * Toggle vanish state for {@code playerUuid}. Persists to DB (if configured)
+ * and broadcasts to other servers on the bus.
+ */
+ void setVanished(@Nonnull UUID playerUuid, boolean vanished);
+
+ /** Snapshot of every UUID currently marked vanished, across the network. */
+ @Nonnull
+ Set vanishedPlayers();
+
+ /**
+ * Convenience check for "should I pretend this target is offline when showing
+ * results to {@code viewer}?" Returns true iff {@code target} is vanished and
+ * {@code viewer} lacks {@code networkstaff.vanish}. Null viewer is treated as
+ * non-staff (defensive — assume hidden).
+ */
+ boolean isHiddenFrom(@Nonnull UUID target, @Nullable PlayerRef viewer);
+
+ /**
+ * Subscribe to vanish state changes. Listener receives {@code (uuid, vanishedNow)}
+ * for every toggle, whether the change originated locally or arrived via the bus
+ * from another server. Fires on the bus-handling thread; consumers that need the
+ * world thread must hop themselves.
+ */
+ void addListener(@Nonnull BiConsumer listener);
+}
diff --git a/src/main/java/net/kewwbec/networkcore/command/network/GlistCommand.java b/src/main/java/net/kewwbec/networkcore/command/network/GlistCommand.java
index 72c1d75..6c0d237 100644
--- a/src/main/java/net/kewwbec/networkcore/command/network/GlistCommand.java
+++ b/src/main/java/net/kewwbec/networkcore/command/network/GlistCommand.java
@@ -15,6 +15,8 @@ import net.kewwbec.networkcore.api.PlayerLocation;
import net.kewwbec.networkcore.api.PlayerPresence;
import net.kewwbec.networkcore.api.ServerInfo;
import net.kewwbec.networkcore.api.ServerRegistry;
+import net.kewwbec.networkcore.api.VanishService;
+import javax.annotation.Nullable;
import javax.annotation.Nonnull;
import java.awt.Color;
@@ -36,12 +38,16 @@ public final class GlistCommand extends AbstractPlayerCommand {
private final ServerRegistry registry;
private final PlayerPresence presence;
+ @Nullable private final VanishService vanish;
private final OptionalArg filterArg = withOptionalArg("filter", "Server id or role to filter by", ArgTypes.STRING);
- public GlistCommand(@Nonnull ServerRegistry registry, @Nonnull PlayerPresence presence) {
+ public GlistCommand(@Nonnull ServerRegistry registry,
+ @Nonnull PlayerPresence presence,
+ @Nullable VanishService vanish) {
super("glist", "List online players, optionally filtered by server id or role");
this.registry = registry;
this.presence = presence;
+ this.vanish = vanish;
requirePermission(NetworkCorePermissions.GLIST);
}
@@ -60,11 +66,13 @@ public final class GlistCommand extends AbstractPlayerCommand {
Set includedIds = new LinkedHashSet<>();
for (ServerInfo s : servers) includedIds.add(s.id());
+ boolean canSeeVanished = sender.hasPermission(net.kewwbec.networkcore.vanish.VanishEnforcer.PERM_SEE);
Map> grouped = new LinkedHashMap<>();
for (ServerInfo s : servers) grouped.put(s.id(), new ArrayList<>());
int filteredOnline = 0;
for (PlayerLocation loc : presence.all()) {
if (!includedIds.contains(loc.serverId())) continue;
+ if (!canSeeVanished && vanish != null && vanish.isVanished(loc.uuid())) continue;
grouped.computeIfAbsent(loc.serverId(), k -> new ArrayList<>()).add(loc);
filteredOnline++;
}
diff --git a/src/main/java/net/kewwbec/networkcore/vanish/PlayerListVanishFilter.java b/src/main/java/net/kewwbec/networkcore/vanish/PlayerListVanishFilter.java
new file mode 100644
index 0000000..19c5574
--- /dev/null
+++ b/src/main/java/net/kewwbec/networkcore/vanish/PlayerListVanishFilter.java
@@ -0,0 +1,85 @@
+package net.kewwbec.networkcore.vanish;
+
+import com.hypixel.hytale.protocol.Packet;
+import com.hypixel.hytale.protocol.packets.interface_.AddToServerPlayerList;
+import com.hypixel.hytale.protocol.packets.interface_.ServerPlayerListPlayer;
+import com.hypixel.hytale.protocol.packets.interface_.ServerPlayerListUpdate;
+import com.hypixel.hytale.protocol.packets.interface_.UpdateServerPlayerList;
+import com.hypixel.hytale.server.core.io.adapter.PacketAdapters;
+import com.hypixel.hytale.server.core.io.adapter.PlayerPacketFilter;
+import com.hypixel.hytale.server.core.universe.PlayerRef;
+import net.kewwbec.networkcore.api.VanishService;
+
+import javax.annotation.Nonnull;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Outbound packet filter that strips vanished players from the client-visible
+ * tab list. Runs for every outbound {@link AddToServerPlayerList} and
+ * {@link UpdateServerPlayerList}; for non-staff recipients, drops any entries
+ * for vanished UUIDs. If the resulting array is empty the packet is cancelled
+ * entirely.
+ *
+ * For mid-session toggles, see {@link VanishEnforcer#onVanishChanged} —
+ * that side pushes explicit {@code RemoveFromServerPlayerList} /
+ * {@code AddToServerPlayerList} to non-staff clients so the list updates
+ * without waiting for the next engine push.
+ */
+public final class PlayerListVanishFilter implements PlayerPacketFilter {
+
+ private final VanishService vanish;
+
+ public PlayerListVanishFilter(@Nonnull VanishService vanish) {
+ this.vanish = vanish;
+ }
+
+ public void register() {
+ PacketAdapters.registerOutbound(this);
+ }
+
+ @Override
+ public boolean test(@Nonnull PlayerRef recipient, @Nonnull Packet packet) {
+ if (recipient.hasPermission(VanishEnforcer.PERM_SEE)) return false;
+
+ if (packet instanceof AddToServerPlayerList add) {
+ return filterAdd(add);
+ }
+ if (packet instanceof UpdateServerPlayerList upd) {
+ return filterUpdate(upd);
+ }
+ return false;
+ }
+
+ private boolean filterAdd(@Nonnull AddToServerPlayerList add) {
+ ServerPlayerListPlayer[] players = add.players;
+ if (players == null || players.length == 0) return false;
+ List kept = new ArrayList<>(players.length);
+ for (ServerPlayerListPlayer p : players) {
+ if (p == null || p.uuid == null) continue;
+ if (vanish.isVanished(p.uuid)) continue;
+ kept.add(p);
+ }
+ if (kept.isEmpty()) return true;
+ if (kept.size() != players.length) {
+ add.players = kept.toArray(new ServerPlayerListPlayer[0]);
+ }
+ return false;
+ }
+
+ private boolean filterUpdate(@Nonnull UpdateServerPlayerList upd) {
+ ServerPlayerListUpdate[] players = upd.players;
+ if (players == null || players.length == 0) return false;
+ List kept = new ArrayList<>(players.length);
+ for (ServerPlayerListUpdate u : players) {
+ if (u == null || u.uuid == null) continue;
+ if (vanish.isVanished(u.uuid)) continue;
+ kept.add(u);
+ }
+ if (kept.isEmpty()) return true;
+ if (kept.size() != players.length) {
+ upd.players = kept.toArray(new ServerPlayerListUpdate[0]);
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/net/kewwbec/networkcore/vanish/VanishChangePayload.java b/src/main/java/net/kewwbec/networkcore/vanish/VanishChangePayload.java
new file mode 100644
index 0000000..372b24b
--- /dev/null
+++ b/src/main/java/net/kewwbec/networkcore/vanish/VanishChangePayload.java
@@ -0,0 +1,19 @@
+package net.kewwbec.networkcore.vanish;
+
+/**
+ * Bus payload broadcast on every vanish toggle. Carries enough state for any
+ * server to update its cache and enforce locally.
+ */
+public final class VanishChangePayload {
+
+ public static final String CHANNEL = "network:vanish";
+
+ /** UUID being vanished/unvanished. */
+ public String uuid;
+ /** New state — true=vanished, false=unvanished. */
+ public boolean vanished;
+ /** Origin server id (for echo suppression). */
+ public String serverId;
+ /** Timestamp (ms since epoch) for staleness checks if we ever add them. */
+ public long ts;
+}
diff --git a/src/main/java/net/kewwbec/networkcore/vanish/VanishEnforcer.java b/src/main/java/net/kewwbec/networkcore/vanish/VanishEnforcer.java
new file mode 100644
index 0000000..1ba47c6
--- /dev/null
+++ b/src/main/java/net/kewwbec/networkcore/vanish/VanishEnforcer.java
@@ -0,0 +1,114 @@
+package net.kewwbec.networkcore.vanish;
+
+import com.hypixel.hytale.logger.HytaleLogger;
+import com.hypixel.hytale.protocol.packets.interface_.AddToServerPlayerList;
+import com.hypixel.hytale.protocol.packets.interface_.RemoveFromServerPlayerList;
+import com.hypixel.hytale.protocol.packets.interface_.ServerPlayerListPlayer;
+import com.hypixel.hytale.server.core.entity.entities.player.HiddenPlayersManager;
+import com.hypixel.hytale.server.core.event.events.player.PlayerReadyEvent;
+import com.hypixel.hytale.server.core.io.PacketHandler;
+import com.hypixel.hytale.server.core.universe.PlayerRef;
+import com.hypixel.hytale.server.core.universe.Universe;
+
+import javax.annotation.Nonnull;
+import java.util.UUID;
+import java.util.logging.Level;
+
+/**
+ * Per-server enforcement of vanish state via {@link HiddenPlayersManager}.
+ * Wired by NetworkCore: on player join we hide existing vanished players from
+ * non-staff arrivals, and on vanish toggles (local or remote via bus) we update
+ * every online player's visibility set.
+ */
+public final class VanishEnforcer {
+
+ /** Anyone with this perm sees vanished players and can use /vanish. */
+ public static final String PERM_SEE = "networkstaff.vanish";
+
+ private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
+
+ private final VanishServiceImpl vanish;
+
+ public VanishEnforcer(@Nonnull VanishServiceImpl vanish) {
+ this.vanish = vanish;
+ vanish.addListener(this::onVanishChanged);
+ }
+
+ /** Called by NetworkCore on every PlayerReadyEvent. */
+ public void onPlayerReady(@Nonnull PlayerReadyEvent event) {
+ com.hypixel.hytale.component.Ref ref =
+ event.getPlayerRef();
+ if (ref == null || !ref.isValid()) return;
+ com.hypixel.hytale.component.Store store =
+ ref.getStore();
+ if (store == null) return;
+ PlayerRef newcomer = store.getComponent(ref, PlayerRef.getComponentType());
+ if (newcomer == null) return;
+
+ UUID newcomerUuid = newcomer.getUuid();
+ boolean newcomerCanSeeVanished = newcomer.hasPermission(PERM_SEE);
+
+ if (!newcomerCanSeeVanished) {
+ HiddenPlayersManager hpm = newcomer.getHiddenPlayersManager();
+ if (hpm != null) {
+ for (UUID vanishedUuid : vanish.vanishedPlayers()) {
+ if (vanishedUuid.equals(newcomerUuid)) continue;
+ hpm.hidePlayer(vanishedUuid);
+ }
+ }
+ }
+
+ if (vanish.isVanished(newcomerUuid)) {
+ Universe universe = Universe.get();
+ if (universe == null) return;
+ for (PlayerRef other : universe.getPlayers()) {
+ if (other.getUuid().equals(newcomerUuid)) continue;
+ if (other.hasPermission(PERM_SEE)) continue;
+ HiddenPlayersManager hpm = other.getHiddenPlayersManager();
+ if (hpm != null) hpm.hidePlayer(newcomerUuid);
+ }
+ }
+ }
+
+ private void onVanishChanged(@Nonnull UUID uuid, boolean vanishedNow) {
+ Universe universe = Universe.get();
+ if (universe == null) return;
+ try {
+ PlayerRef subject = null;
+ for (PlayerRef p : universe.getPlayers()) {
+ if (p.getUuid().equals(uuid)) { subject = p; break; }
+ }
+ for (PlayerRef other : universe.getPlayers()) {
+ if (other.getUuid().equals(uuid)) continue;
+ HiddenPlayersManager hpm = other.getHiddenPlayersManager();
+ if (hpm != null) {
+ if (other.hasPermission(PERM_SEE)) {
+ hpm.showPlayer(uuid);
+ } else if (vanishedNow) {
+ hpm.hidePlayer(uuid);
+ } else {
+ hpm.showPlayer(uuid);
+ }
+ }
+ if (other.hasPermission(PERM_SEE)) continue;
+ PacketHandler ph = other.getPacketHandler();
+ if (ph == null) continue;
+ try {
+ if (vanishedNow) {
+ ph.write(new RemoveFromServerPlayerList(new UUID[]{uuid}));
+ } else if (subject != null) {
+ ServerPlayerListPlayer entry = new ServerPlayerListPlayer(
+ subject.getUuid(),
+ subject.getUsername(),
+ subject.getWorldUuid(),
+ 0);
+ ph.write(new AddToServerPlayerList(new ServerPlayerListPlayer[]{entry}));
+ }
+ } catch (RuntimeException ignored) {
+ }
+ }
+ } catch (RuntimeException e) {
+ LOGGER.at(Level.WARNING).log("VanishEnforcer onVanishChanged threw: %s", e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/net/kewwbec/networkcore/vanish/VanishServiceImpl.java b/src/main/java/net/kewwbec/networkcore/vanish/VanishServiceImpl.java
new file mode 100644
index 0000000..1a1ed88
--- /dev/null
+++ b/src/main/java/net/kewwbec/networkcore/vanish/VanishServiceImpl.java
@@ -0,0 +1,115 @@
+package net.kewwbec.networkcore.vanish;
+
+import com.hypixel.hytale.logger.HytaleLogger;
+import com.hypixel.hytale.server.core.universe.PlayerRef;
+import net.kewwbec.networkcore.api.MessageBus;
+import net.kewwbec.networkcore.api.VanishService;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.BiConsumer;
+import java.util.logging.Level;
+
+/**
+ * In-memory vanish cache, kept consistent across the network via the
+ * {@link MessageBus}. State is not persisted — vanish ends at server restart
+ * (intentional: staff explicitly turn it on each session).
+ */
+public final class VanishServiceImpl implements VanishService {
+
+ private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
+
+ private final MessageBus bus;
+ private final String selfServerId;
+ private final Set vanished = ConcurrentHashMap.newKeySet();
+ /** Listeners notified on every change, local or remote-via-bus. */
+ private final List> listeners = new CopyOnWriteArrayList<>();
+ @Nullable private MessageBus.Subscription sub;
+
+ public VanishServiceImpl(@Nonnull MessageBus bus, @Nonnull String selfServerId) {
+ this.bus = bus;
+ this.selfServerId = selfServerId;
+ }
+
+ public void start() {
+ sub = bus.subscribe(VanishChangePayload.CHANNEL, VanishChangePayload.class, this::onRemote);
+ }
+
+ public void stop() {
+ if (sub != null) {
+ try { sub.close(); } catch (RuntimeException ignored) {}
+ sub = null;
+ }
+ vanished.clear();
+ }
+
+ @Override
+ public void addListener(@Nonnull BiConsumer listener) {
+ listeners.add(listener);
+ }
+
+ @Override
+ public boolean isVanished(@Nonnull UUID playerUuid) {
+ return vanished.contains(playerUuid);
+ }
+
+ @Override
+ public void setVanished(@Nonnull UUID playerUuid, boolean vanishedNow) {
+ boolean changed = vanishedNow ? vanished.add(playerUuid) : vanished.remove(playerUuid);
+ if (!changed) return;
+ fireLocal(playerUuid, vanishedNow);
+ publish(playerUuid, vanishedNow);
+ }
+
+ @Override
+ @Nonnull
+ public Set vanishedPlayers() {
+ return Collections.unmodifiableSet(vanished);
+ }
+
+ @Override
+ public boolean isHiddenFrom(@Nonnull UUID target, @Nullable PlayerRef viewer) {
+ if (!vanished.contains(target)) return false;
+ if (viewer == null) return true;
+ if (viewer.getUuid().equals(target)) return false;
+ return !viewer.hasPermission(VanishEnforcer.PERM_SEE);
+ }
+
+ private void onRemote(@Nonnull VanishChangePayload p) {
+ if (p.uuid == null || p.serverId == null) return;
+ if (selfServerId.equals(p.serverId)) return;
+ UUID uuid;
+ try { uuid = UUID.fromString(p.uuid); } catch (IllegalArgumentException e) { return; }
+ boolean changed = p.vanished ? vanished.add(uuid) : vanished.remove(uuid);
+ if (changed) fireLocal(uuid, p.vanished);
+ }
+
+ private void publish(@Nonnull UUID uuid, boolean vanishedNow) {
+ try {
+ VanishChangePayload p = new VanishChangePayload();
+ p.uuid = uuid.toString();
+ p.vanished = vanishedNow;
+ p.serverId = selfServerId;
+ p.ts = System.currentTimeMillis();
+ bus.publish(VanishChangePayload.CHANNEL, p);
+ } catch (RuntimeException e) {
+ LOGGER.at(Level.WARNING).log("Vanish publish failed: %s", e.getMessage());
+ }
+ }
+
+ private void fireLocal(@Nonnull UUID uuid, boolean vanishedNow) {
+ for (BiConsumer listener : listeners) {
+ try {
+ listener.accept(uuid, vanishedNow);
+ } catch (RuntimeException e) {
+ LOGGER.at(Level.WARNING).log("Vanish listener threw: %s", e.getMessage());
+ }
+ }
+ }
+}