diff --git a/README.md b/README.md index aa56a26..d7ca891 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,8 @@ Administrative commands require `leaf.admin`, granted to server operators by def Leaf prefixes the standard Spigot display name (used by standard chat), player-list name, and a dedicated `leaf_protected` main-scoreboard team for overhead names. Legacy `&` color codes are supported. Leaf does not move players out of an existing scoreboard team, and per-viewer/custom scoreboards may not show its overhead prefix. Chat or tab-management plugins may replace Leaf's decorated names. Leaf restores a name only when it still matches the value Leaf installed, so unsupported integrations do not affect protection state or get overwritten during cleanup. +Spigot identifies potion effects by type but does not expose their owning plugin. Leaf tracks the exact infinite, quiet Resistance effect it successfully installed and removes it only while the visible effect still matches. A distinct Resistance level, duration, or presentation is preserved. Spigot cannot distinguish an externally supplied effect with an identical fingerprint; Leaf therefore does not claim or later remove an identical effect that was already active when reconciliation ran. + ## Releases Gitea Actions checks pushes and pull requests and stores a development JAR. Pull requests validate conventional commits. Main-branch conventional commits drive semantic releases after the repository defines a `RELEASE_TOKEN` with contents-write permission. diff --git a/design/log.md b/design/log.md index c88c017..89d79bd 100644 --- a/design/log.md +++ b/design/log.md @@ -64,3 +64,11 @@ - Target resolution prefers exact online/current names, retains prior names, accepts UUIDs, and rejects ambiguous reused names. - Added position-specific administrative autocomplete and mutation/no-change reporting with validation before mutation. - Verified global effect and prefix suppression/restoration, live strength, targets, locks, command permissions, completion, durable aliases, and the full build with `./gradlew clean check jar`. + +### US-006 configuration and persistence completed + +- Runtime global and Resistance-level changes are written through Bukkit configuration before becoming active and survive restart. +- Failed runtime setting persistence leaves the active settings unchanged. +- Leaf records only effects it successfully installs, removes them only while the visible fingerprint matches, and preserves distinct Resistance effects; Spigot's identical-effect ownership limitation is documented. +- Durable player state now retains prior names in addition to the required UUID, latest name, choice, lock, and RFC 3339 first-join timestamp. +- Verified settings failure safety, durable aliases, existing defensive YAML behavior, strict compilation, and the full build with `./gradlew clean check jar`. diff --git a/design/user-stories/us-006-configure-and-persist-leaf.md b/design/user-stories/us-006-configure-and-persist-leaf.md index f265a82..8323616 100644 --- a/design/user-stories/us-006-configure-and-persist-leaf.md +++ b/design/user-stories/us-006-configure-and-persist-leaf.md @@ -2,7 +2,7 @@ type: User Story title: "US-006: Configure and persist Leaf" description: Provide validated configuration and durable, defensive storage for Leaf behavior. -status: in-progress +status: done --- # US-006: Configure and persist Leaf @@ -13,13 +13,13 @@ As a **server operator**, I want Leaf settings and player state to be validated - [x] Configuration supports the global enabled state, Resistance strength, leaf prefix, onboarding duration, and player-facing messages. - [x] Resistance strength defaults to level I and accepts only Minecraft Resistance levels I through V. -- [ ] Runtime changes made through `/leaf enabled` and `/leaf strength` are persisted for subsequent restarts. +- [x] Runtime changes made through `/leaf enabled` and `/leaf strength` are persisted for subsequent restarts. - [x] UUID-keyed player state persists the latest known name, saved opt-in choice, administrative lock, and first-join timestamp. - [x] Date-times use RFC 3339 UTC notation with a `Z` suffix. - [x] State is saved safely so that a failed write does not replace valid persisted state with a partial document. - [x] Invalid required configuration prevents partial plugin initialization and produces a clear server log message. - [x] Corrupt or invalid player records are handled defensively and cannot silently grant protection or privileges. -- [ ] Removing Leaf-managed Resistance does not remove a distinct Resistance effect that Leaf does not own when the API provides enough information to distinguish it. +- [x] Removing Leaf-managed Resistance does not remove a distinct Resistance effect that Leaf does not own when the API provides enough information to distinguish it. - [x] Unknown forward-compatible configuration and player-state fields are preserved where practical. ## Related diff --git a/src/test/java/games/dmg/leaf/LeafRuntimeTest.java b/src/test/java/games/dmg/leaf/LeafRuntimeTest.java index 80e09d0..24bd0ec 100644 --- a/src/test/java/games/dmg/leaf/LeafRuntimeTest.java +++ b/src/test/java/games/dmg/leaf/LeafRuntimeTest.java @@ -128,6 +128,29 @@ final class LeafRuntimeTest { verify(identity, org.mockito.Mockito.atLeastOnce()).remove(player); } + @Test + void doesNotActivateASettingWhenItsPersistenceFails() throws Exception { + Server server = mock(Server.class); + LeafSettingsProvider settings = new LeafSettingsProvider(LeafSettings.from(Map.of())); + LeafRuntime runtime = new LeafRuntime( + server, + settings, + new LeafStateManager(new YamlLeafStateRepository( + temporaryDirectory.resolve("failed-settings.yml") + )), + mock(LeafProtection.class), + mock(LeafIdentity.class), + replacement -> { + throw new java.io.IOException("disk unavailable"); + } + ); + + assertThrows(java.io.IOException.class, () -> runtime.setGlobalEnabled(false)); + + assertTrue(settings.current().enabled()); + verify(server, never()).getOnlinePlayers(); + } + @Test void resolvesLatestPreviousNamesAndUuidsWithoutGuessingAmbiguities() throws Exception { Server server = mock(Server.class); diff --git a/src/test/java/games/dmg/leaf/LeafStateManagerTest.java b/src/test/java/games/dmg/leaf/LeafStateManagerTest.java index 2dfeb39..4629fad 100644 --- a/src/test/java/games/dmg/leaf/LeafStateManagerTest.java +++ b/src/test/java/games/dmg/leaf/LeafStateManagerTest.java @@ -27,5 +27,6 @@ final class LeafStateManagerTest { PlayerLeafState saved = repository.load().players().get(playerId); assertEquals(first, saved.firstJoin()); assertEquals("AlexNew", saved.latestName()); + assertEquals(java.util.Set.of("Alex", "AlexNew"), saved.knownNames()); } }