fix(stealth): suppress concealed disconnect messages
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
## 2026-09-04
|
## 2026-09-04
|
||||||
|
|
||||||
|
- **Completion**: Extended US-002 so concealed players disconnect without a public quit announcement while ordinary quit messages remain unchanged; verified listener tests, the complete Gradle build, and the OKF bundle.
|
||||||
- **Completion**: Extended US-003 and US-004 with contextual, prefix-filtered command completion that suppresses generic player suggestions, plus an administrative list of all known online and offline unlocked players; verified the complete Gradle build and OKF bundle.
|
- **Completion**: Extended US-003 and US-004 with contextual, prefix-filtered command completion that suppresses generic player suggestions, plus an administrative list of all known online and offline unlocked players; verified the complete Gradle build and OKF bundle.
|
||||||
|
|
||||||
## 2026-08-14
|
## 2026-08-14
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ As an **unlocked player**, I want to disconnect while invisibility from a potion
|
|||||||
- [x] A player who has not unlocked stealth cannot prepare a concealed login.
|
- [x] A player who has not unlocked stealth cannot prepare a concealed login.
|
||||||
- [x] An ordinary disconnect without an active qualifying effect clears any preparation for the next login.
|
- [x] An ordinary disconnect without an active qualifying effect clears any preparation for the next login.
|
||||||
- [x] On a prepared login, no public join announcement is shown.
|
- [x] On a prepared login, no public join announcement is shown.
|
||||||
|
- [x] When a concealed player disconnects, no public quit or disconnect announcement is shown.
|
||||||
|
- [x] Ordinary players' quit messages remain unchanged.
|
||||||
|
- [x] Concealment is checked before disconnect cleanup so announcement suppression is reliable.
|
||||||
- [x] Throughout the concealed session, the player is absent from every other player's tab list, including administrators' tab lists.
|
- [x] Throughout the concealed session, the player is absent from every other player's tab list, including administrators' tab lists.
|
||||||
- [x] Throughout the concealed session, no overhead name tag identifies the player to any other player, including administrators.
|
- [x] Throughout the concealed session, no overhead name tag identifies the player to any other player, including administrators.
|
||||||
- [x] The concealed player's physical character remains visible in the world and retains ordinary movement, interaction, combat, and permission behavior.
|
- [x] The concealed player's physical character remains visible in the world and retains ordinary movement, interaction, combat, and permission behavior.
|
||||||
@@ -26,7 +29,7 @@ As an **unlocked player**, I want to disconnect while invisibility from a potion
|
|||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
|
|
||||||
Automated tests verify unlocked and locked disconnect transitions, ordinary-disconnect clearing, one-login consumption, announcement suppression, private activation messaging, ordinary-login presentation, tab removal for existing and new observers, overhead-name suppression, and the absence of entity-hiding calls. ProtocolLib is declared as a required dependency, prepared state round trips through YAML, and `./gradlew clean check jar` passes.
|
Automated tests verify unlocked and locked disconnect transitions, ordinary-disconnect clearing, one-login consumption, concealed join and quit announcement suppression, preservation of ordinary announcements, private activation messaging, ordinary-login presentation, tab removal for existing and new observers, overhead-name suppression, and the absence of entity-hiding calls. ProtocolLib is declared as a required dependency, prepared state round trips through YAML, and `./gradlew clean check jar` passes.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
|
|||||||
@@ -38,10 +38,14 @@ public final class StealthSessionListener implements Listener {
|
|||||||
presentation.refreshForObserver(player);
|
presentation.refreshForObserver(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.MONITOR)
|
@EventHandler(priority = EventPriority.HIGHEST)
|
||||||
public void onQuit(PlayerQuitEvent event) {
|
public void onQuit(PlayerQuitEvent event) {
|
||||||
presentation.reveal(event.getPlayer());
|
Player player = event.getPlayer();
|
||||||
sessions.disconnect(event.getPlayer().getUniqueId());
|
if (sessions.isConcealed(player.getUniqueId())) {
|
||||||
|
event.setQuitMessage(null);
|
||||||
|
}
|
||||||
|
presentation.reveal(player);
|
||||||
|
sessions.disconnect(player.getUniqueId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.MONITOR)
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
|||||||
@@ -10,9 +10,50 @@ import java.util.Map;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.player.PlayerJoinEvent;
|
import org.bukkit.event.player.PlayerJoinEvent;
|
||||||
|
import org.bukkit.event.player.PlayerQuitEvent;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
class StealthSessionListenerTest {
|
class StealthSessionListenerTest {
|
||||||
|
@Test
|
||||||
|
void concealedDisconnectSuppressesPublicAnnouncementBeforeCleanup() {
|
||||||
|
UUID playerId = UUID.randomUUID();
|
||||||
|
try (StealthStateManager manager = manager()) {
|
||||||
|
manager.update(state -> state.withPlayer(unlocked(playerId).withSession(false, true))).join();
|
||||||
|
QualifyingInvisibilityService progression = new QualifyingInvisibilityService(
|
||||||
|
manager, Duration.ofHours(8), System::nanoTime, ignored -> { });
|
||||||
|
StealthSessionService sessions = new StealthSessionService(manager, progression);
|
||||||
|
StealthSessionListener listener = new StealthSessionListener(
|
||||||
|
sessions, mock(IdentityPresentation.class), "Stealth active");
|
||||||
|
PlayerQuitEvent event = mock(PlayerQuitEvent.class);
|
||||||
|
Player player = player(playerId);
|
||||||
|
when(event.getPlayer()).thenReturn(player);
|
||||||
|
|
||||||
|
listener.onQuit(event);
|
||||||
|
|
||||||
|
verify(event).setQuitMessage(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ordinaryDisconnectRetainsPublicAnnouncement() {
|
||||||
|
UUID playerId = UUID.randomUUID();
|
||||||
|
try (StealthStateManager manager = manager()) {
|
||||||
|
QualifyingInvisibilityService progression = new QualifyingInvisibilityService(
|
||||||
|
manager, Duration.ofHours(8), System::nanoTime, ignored -> { });
|
||||||
|
StealthSessionListener listener = new StealthSessionListener(
|
||||||
|
new StealthSessionService(manager, progression),
|
||||||
|
mock(IdentityPresentation.class),
|
||||||
|
"Stealth active");
|
||||||
|
PlayerQuitEvent event = mock(PlayerQuitEvent.class);
|
||||||
|
Player player = player(playerId);
|
||||||
|
when(event.getPlayer()).thenReturn(player);
|
||||||
|
|
||||||
|
listener.onQuit(event);
|
||||||
|
|
||||||
|
verify(event, never()).setQuitMessage(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void preparedLoginSuppressesAnnouncementAndConcealsIdentityForSession() {
|
void preparedLoginSuppressesAnnouncementAndConcealsIdentityForSession() {
|
||||||
UUID playerId = UUID.randomUUID();
|
UUID playerId = UUID.randomUUID();
|
||||||
|
|||||||
Reference in New Issue
Block a user