fix(visibility): enforce the role visibility matrix between all three roles - #199
Open
TheMeinerLP wants to merge 1 commit into
Open
fix(visibility): enforce the role visibility matrix between all three roles#199TheMeinerLP wants to merge 1 commit into
TheMeinerLP wants to merge 1 commit into
Conversation
… roles
Visibility was steered by two competing mechanisms at once: Minestom's
viewable rule and manual viewer packets (updateNewViewer/updateOldViewer).
The latter are purely packet based and never touch EntityView's viewer bit
set, so both drifted apart -- GameStartListener hid the slender without
deregistering anyone, which made the next rule evaluation skip the spawn
packet, while the manual toggle produced duplicated spawn packets.
Everything now goes through per-viewer predicates only, collected in the new
VisibilityRules class. Verified against Minestom 2026.07.22-26.2: the
predicate receives the viewer and is re-evaluated whenever somebody enters
range, so the matrix is expressible natively. addViewer/removeViewer stay
unused on purpose -- they record players in EntityView#manualViewers, which
would disable the rule for them permanently.
Resulting matrix:
Slender -> sees survivors, never spectators
Survivor -> sees the slender only while revealed, never spectators
Spectator -> sees survivors and other spectators, and shares the survivor
view of the slender
Fixed along the way:
- Spectators were invisible to each other (`_ -> false`).
- A revived slender got neither rule nor hidden tag and stayed visible for
the rest of the round.
- The slender was visible between team allocation and game start, because
the rule was installed before Tags.HIDDEN existed.
- Spectator chat leaked to everyone whenever no phase was active (the
`null instanceof GamePhase` fallback was fail-open) and throughout the
restart phase. The filter no longer depends on the phase at all.
- Spectators took slender damage through setHealth, which bypasses the
damage event chain. A dying spectator then triggered a second death
message and a second SpectatorAddEvent.
- Spectators heard the teleport sound at the exact moment the slender turned
invisible.
- Slender and spectators received jumpscares, including DARKNESS for 40
ticks on the slender.
ViewRuleUpdater is removed: isViewAble was dead code with an inverted name,
isHidden and updateViewer moved into VisibilityRules. SlenderBarTrigger lost
its updateRuneFunction parameter and PlayerChatListener its phaseSupplier,
since both only fed the paths that are gone now.
Known leaks outside this change are documented in
docs/plans/role-visibility-matrix.md, most notably that updateTabList gives
the slender a red display name that outs them in the tab list and under every
chat message.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQCb6kSTw5NnRF1Yc4hYXg
TheMeinerLP
force-pushed
the
fix/role-visibility-matrix
branch
from
August 22, 2026 15:14
9f6c7ec to
9321c37
Compare
Contributor
Test results249 files 249 suites 1m 50s ⏱️ Results for commit 9321c37. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Target matrix
Who (row) may see whom (column) as an entity:
Chat: survivor and slender messages reach everyone, spectator chat stays among spectators — in every phase.
Root cause
Visibility was driven by two competing mechanisms at the same time: Minestom's viewable rule and manual viewer packets (
updateNewViewer/updateOldViewer). The latter are purely packet based and never touch the viewerbitSetof theEntityView, so the two layers drifted apart:GameStartListenerhid the slender viaupdateOldViewerwithout deregistering anyone. The next rule evaluation sawisRegistered == trueand skipped the spawn packet.updateNewViewerfirst and re-evaluated the rule afterwards → duplicated spawn packets.This change unifies everything onto one layer: per-viewer predicates exclusively, collected in the new
VisibilityRulesclass.Verified against Minestom
2026.07.22-26.2(EntityView.java): the predicate receives the viewer and is evaluated whenever somebody enters range, so the matrix is natively expressible.addViewer/removeViewerstay unused on purpose, since they record players inmanualViewersand would disable the rule for them permanently.Violations fixed
updateViewableRule(_ -> false)Tags.HIDDENTags.HIDDENexistednull instanceof GamePhase == false, plus a gap during the restart phasesetHealthwithout a team/game mode filter — a dying spectator then triggered a second death message and a secondSpectatorAddEventDARKNESSfor 40 ticks on the slenderCleanup
ViewRuleUpdaterremoved:isViewAblewas dead code with an inverted name;isHiddenandupdateViewerwere absorbed intoVisibilityRules.SlenderBarTriggerlosesupdateRuneFunction,PlayerChatListenerlosesphaseSupplier— both only fed the paths that are gone now.switchEntityTyperespawns on its own.Tests
./gradlew buildis green across all modules. One test per matrix cell plus regression tests:VisibilityRulesTest— all six cellsSpectatorServiceTest—testJoinMakesPlayerInvisibleToOtherscemented the old bug and was replacedSlenderReviveIntegrationTest.testRevivedSlenderIsHiddenFromEveryoneElsePlayerChatListenerTest— game phase, restart phase and without an active phaseSlenderBarHelperTest,CygnusPlayerTickListenerTestSlenderBarIntegrationTest.testAutoDepletionPlaysTeleportSoundhad to be adjusted: its own comment says "plays to nearby survivors", but it never tagged the player as a survivor and only passed because there was no role filter at all.Deliberately out of scope
Fully documented in
docs/plans/role-visibility-matrix.md. The largest remaining leak:TeamHelper.updateTabListgives the slender the display name⛧ namein red. This is broadcast to everyone viaUPDATE_DISPLAY_NAME— every survivor and spectator immediately sees who the slender is in the tab list, and the same name sits under every chat message they send. Possibly intentional, so it is left untouched here.Also open: page discoveries and death messages are broadcast unfiltered (real-time information for the slender); the corpse mannequin's particles bypass every viewable rule through
sendGroupedPacket;finishGameresets no rules (harmless today, since one process serves exactly one round, but relevant as soon as a round reset is introduced).Follow-up from the implementation:
stamina→teamis now a package cycle, becauseTeamHelperreads theHIDDENconstant fromSlenderBarHelper. MovingVISIBLE/HIDDENinto a neutral holder would resolve it.🤖 Generated with Claude Code
https://claude.ai/code/session_01QQCb6kSTw5NnRF1Yc4hYXg