vanish impl

This commit is contained in:
2026-06-04 16:48:04 -04:00
parent 52945bdc98
commit 4b5dbd7793
7 changed files with 411 additions and 2 deletions
@@ -13,6 +13,7 @@ import net.kewwbec.networkcore.api.MetricsService;
import net.kewwbec.networkcore.api.PlayerPresence; import net.kewwbec.networkcore.api.PlayerPresence;
import net.kewwbec.networkcore.api.RpcClient; import net.kewwbec.networkcore.api.RpcClient;
import net.kewwbec.networkcore.api.ServerRegistry; import net.kewwbec.networkcore.api.ServerRegistry;
import net.kewwbec.networkcore.api.VanishService;
import net.kewwbec.networkcore.api.events.ServerLeftNetworkEvent; import net.kewwbec.networkcore.api.events.ServerLeftNetworkEvent;
import net.kewwbec.networkcore.bus.RedisMessageBus; import net.kewwbec.networkcore.bus.RedisMessageBus;
import net.kewwbec.networkcore.command.NetworkCoreCommand; 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.presence.PlayerPresenceService;
import net.kewwbec.networkcore.registry.RedisServerRegistry; import net.kewwbec.networkcore.registry.RedisServerRegistry;
import net.kewwbec.networkcore.rpc.RpcClientImpl; 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.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@@ -75,6 +81,7 @@ public final class NetworkCore extends JavaPlugin {
@Nullable private RefRequestService refRequestService; @Nullable private RefRequestService refRequestService;
@Nullable private LockService lockService; @Nullable private LockService lockService;
@Nullable private ShutdownHandoff shutdownHandoff; @Nullable private ShutdownHandoff shutdownHandoff;
@Nullable private VanishServiceImpl vanishService;
public NetworkCore(@Nonnull JavaPluginInit init) { public NetworkCore(@Nonnull JavaPluginInit init) {
super(init); super(init);
@@ -164,6 +171,13 @@ public final class NetworkCore extends JavaPlugin {
this.alertService = new AlertService(messageBus, serverId); this.alertService = new AlertService(messageBus, serverId);
alertService.start(); 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); this.refRequestService = new RefRequestService(messageBus, serverId);
refRequestService.start(); refRequestService.start();
@@ -185,7 +199,7 @@ public final class NetworkCore extends JavaPlugin {
}); });
getCommandRegistry().registerCommand(new AlertCommand(alertService)); 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 HubCommand(lobbyPicker, refRequestService, serverId));
getCommandRegistry().registerCommand(new SendCommand(serverRegistry, presenceService, refRequestService)); getCommandRegistry().registerCommand(new SendCommand(serverRegistry, presenceService, refRequestService));
getCommandRegistry().registerCommand(new SendAllCommand(serverRegistry, presenceService, refRequestService, serverId)); getCommandRegistry().registerCommand(new SendAllCommand(serverRegistry, presenceService, refRequestService, serverId));
@@ -227,6 +241,10 @@ public final class NetworkCore extends JavaPlugin {
try { alertService.stop(); } catch (RuntimeException ignored) {} try { alertService.stop(); } catch (RuntimeException ignored) {}
alertService = null; alertService = null;
} }
if (vanishService != null) {
try { vanishService.stop(); } catch (RuntimeException ignored) {}
vanishService = null;
}
if (presenceService != null) { if (presenceService != null) {
try { presenceService.stop(); } catch (RuntimeException ignored) {} try { presenceService.stop(); } catch (RuntimeException ignored) {}
presenceService = null; presenceService = null;
@@ -288,6 +306,8 @@ public final class NetworkCore extends JavaPlugin {
@Nullable @Nullable
public NetworkXp getNetworkXp() { return services.get(NetworkXp.class); } public NetworkXp getNetworkXp() { return services.get(NetworkXp.class); }
@Nullable @Nullable
public VanishService getVanishService() { return services.get(VanishService.class); }
@Nullable
public RefRequestService getRefRequestService() { return refRequestService; } public RefRequestService getRefRequestService() { return refRequestService; }
@Nullable @Nullable
@@ -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).
*
* <p>Players with the {@code networkstaff.vanish} permission can see vanished
* players regardless. Everyone else sees them as if they weren't there.</p>
*/
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<UUID> 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<UUID, Boolean> listener);
}
@@ -15,6 +15,8 @@ import net.kewwbec.networkcore.api.PlayerLocation;
import net.kewwbec.networkcore.api.PlayerPresence; import net.kewwbec.networkcore.api.PlayerPresence;
import net.kewwbec.networkcore.api.ServerInfo; import net.kewwbec.networkcore.api.ServerInfo;
import net.kewwbec.networkcore.api.ServerRegistry; import net.kewwbec.networkcore.api.ServerRegistry;
import net.kewwbec.networkcore.api.VanishService;
import javax.annotation.Nullable;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.awt.Color; import java.awt.Color;
@@ -36,12 +38,16 @@ public final class GlistCommand extends AbstractPlayerCommand {
private final ServerRegistry registry; private final ServerRegistry registry;
private final PlayerPresence presence; private final PlayerPresence presence;
@Nullable private final VanishService vanish;
private final OptionalArg<String> filterArg = withOptionalArg("filter", "Server id or role to filter by", ArgTypes.STRING); private final OptionalArg<String> 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"); super("glist", "List online players, optionally filtered by server id or role");
this.registry = registry; this.registry = registry;
this.presence = presence; this.presence = presence;
this.vanish = vanish;
requirePermission(NetworkCorePermissions.GLIST); requirePermission(NetworkCorePermissions.GLIST);
} }
@@ -60,11 +66,13 @@ public final class GlistCommand extends AbstractPlayerCommand {
Set<String> includedIds = new LinkedHashSet<>(); Set<String> includedIds = new LinkedHashSet<>();
for (ServerInfo s : servers) includedIds.add(s.id()); for (ServerInfo s : servers) includedIds.add(s.id());
boolean canSeeVanished = sender.hasPermission(net.kewwbec.networkcore.vanish.VanishEnforcer.PERM_SEE);
Map<String, List<PlayerLocation>> grouped = new LinkedHashMap<>(); Map<String, List<PlayerLocation>> grouped = new LinkedHashMap<>();
for (ServerInfo s : servers) grouped.put(s.id(), new ArrayList<>()); for (ServerInfo s : servers) grouped.put(s.id(), new ArrayList<>());
int filteredOnline = 0; int filteredOnline = 0;
for (PlayerLocation loc : presence.all()) { for (PlayerLocation loc : presence.all()) {
if (!includedIds.contains(loc.serverId())) continue; if (!includedIds.contains(loc.serverId())) continue;
if (!canSeeVanished && vanish != null && vanish.isVanished(loc.uuid())) continue;
grouped.computeIfAbsent(loc.serverId(), k -> new ArrayList<>()).add(loc); grouped.computeIfAbsent(loc.serverId(), k -> new ArrayList<>()).add(loc);
filteredOnline++; filteredOnline++;
} }
@@ -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.
*
* <p>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.</p>
*/
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<ServerPlayerListPlayer> 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<ServerPlayerListUpdate> 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;
}
}
@@ -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;
}
@@ -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<com.hypixel.hytale.server.core.universe.world.storage.EntityStore> ref =
event.getPlayerRef();
if (ref == null || !ref.isValid()) return;
com.hypixel.hytale.component.Store<com.hypixel.hytale.server.core.universe.world.storage.EntityStore> 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());
}
}
}
@@ -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<UUID> vanished = ConcurrentHashMap.newKeySet();
/** Listeners notified on every change, local or remote-via-bus. */
private final List<BiConsumer<UUID, Boolean>> 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<UUID, Boolean> 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<UUID> 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<UUID, Boolean> listener : listeners) {
try {
listener.accept(uuid, vanishedNow);
} catch (RuntimeException e) {
LOGGER.at(Level.WARNING).log("Vanish listener threw: %s", e.getMessage());
}
}
}
}