Skip to content

Fix: ghost entity in EntityStore after vanished player disconnects#59

Open
Dimotai wants to merge 1 commit intoEliteScouter:mainfrom
Dimotai:fix/vanish-ghost-entity
Open

Fix: ghost entity in EntityStore after vanished player disconnects#59
Dimotai wants to merge 1 commit intoEliteScouter:mainfrom
Dimotai:fix/vanish-ghost-entity

Conversation

@Dimotai
Copy link
Copy Markdown
Contributor

@Dimotai Dimotai commented Apr 4, 2026

Problem 1 — Ghost entity after disconnect
When a player uses /vanish and performs cross-world teleports, disconnecting leaves a ghost entity in EntityStore$UUIDSystem. On reconnect, onEntityAdded detects a duplicate UUID and calls removeEntity(), invalidating the Ref that deferred tasks (e.g. SoulboundContainerGuard.registerPlayer, UsageRulesService.doPlayerJoinSetup) use. The player gets kicked with "Player removed from world" and cannot rejoin without a full server restart. Deleting player data files does not help — the ghost entity lives in the runtime UUID→entity map in RAM.
Root cause: onPlayerLeave() only cleaned up internal tracking (vanishedPlayers set, playerStoreRefs map) but never reversed engine-level vanish effects. Cross-world teleports while vanished leave residual HiddenPlayersManager entries that persist even after un-vanishing, interfering with the engine's entity removal pipeline. The bug occurs even when the player un-vanishes before disconnecting.
Fix: onPlayerLeave() now unconditionally calls updateVisibilityForAll(playerId, false) on every disconnect. showPlayer() is a no-op if the player isn't hidden, so zero cost for normal players. If the player is currently vanished, the Invulnerable component is also removed (respecting GodService state).
Problem 2 — Live entity corruption on rapid vanish toggle
Rapidly toggling /v (e.g. twice within 6 seconds) corrupts the player entity while still connected. The Player component becomes null, breaking all commands, movement, and map tracking. The player sees "Player is not in valid world" and "Cannot invoke Player.getUuid() because player is null".
Root cause: updateMobImmunity() had a "synchronous" code path that called store.putComponent(ref, Invulnerable) directly on the ForkJoinPool thread where commands execute — not the WorldThread. Rapid toggles caused concurrent putComponent/removeComponent calls racing with the WorldThread, corrupting the ECS archetype and nulling the Player component.
Fix (two parts):

updateMobImmunity() now always defers component mutations to the world thread via world.execute(), using the caller-provided ref when available for freshness.
toggleVanish() now has a per-player reentrance guard (toggleInProgress set) that ignores a second toggle if one is already in progress for that UUID.

@Dimotai Dimotai force-pushed the fix/vanish-ghost-entity branch from cb6f866 to 33c235d Compare April 5, 2026 03:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant