3 Commits
Author SHA1 Message Date
dmg 902ffc9888 docs(leaf): point to renamed canonical wiki paths
CI / build (push) Successful in 1m23s
Release / release (push) Successful in 4m46s
2026-09-10 07:59:59 -04:00
dmg 54e7f306ed chore(knowledge): move canonical docs to shared SoMC wiki
CI / build (push) Successful in 3m9s
Release / release (push) Successful in 2m12s
2026-09-09 23:18:05 -04:00
dmg 320c8ce193 fix(protection): restore cleared Leaf effects
Release / release (push) Successful in 2m7s
CI / build (push) Successful in 1m1s
2026-08-10 23:09:54 -04:00
17 changed files with 108 additions and 337 deletions
+9
View File
@@ -0,0 +1,9 @@
# spigot-leaf agent entrypoint
The canonical stories, engineering guidance, and **all process documents** are in the private [SoMC OKF wiki](https://git.garvis.dev/dmg/somc-okf/src/branch/main/index.md).
Before work, read the sibling `../somc-okf/index.md`, `../somc-okf/processes/index.md`, `../somc-okf/projects/spigot-leaf/index.md`, `engineering.md` in that project section, and relevant `../somc-okf/user-stories/spigot-leaf/` stories. Also follow the parent workspace `AGENTS.md` when present.
For standalone checkouts, start at the [project page](https://git.garvis.dev/dmg/somc-okf/src/branch/main/projects/spigot-leaf/index.md) and [shared process](https://git.garvis.dev/dmg/somc-okf/src/branch/main/processes/development.md). Obtain wiki access before feature work; do not recreate a local knowledge bundle. Source builds do not require private wiki access.
Development follows [Development cycle](https://git.garvis.dev/dmg/somc-okf/src/branch/main/runbooks/development-cycle.md): approved stories, failing tests, passing implementation, verification, then source/wiki commit and push. GitOps updates are committed locally **without pushing**; only [Do release](https://git.garvis.dev/dmg/somc-okf/src/branch/main/runbooks/do-release.md) authorizes a reviewed GitOps push.
+3 -1
View File
@@ -2,7 +2,7 @@
Leaf is a Spigot 26.2 plugin that lets non-aggressive players opt into visible Resistance and Strength protection.
The approved behavior is specified in the [OKF design bundle](design/index.md).
The approved behavior is specified in the [SoMC OKF wiki](https://git.garvis.dev/dmg/somc-okf/src/branch/main/projects/spigot-leaf/index.md).
## Requirements
@@ -39,6 +39,8 @@ Leaf prefixes the standard Spigot display name (used by standard chat), player-l
Spigot identifies potion effects by type but does not expose their owning plugin. Leaf tracks the exact infinite, quiet Resistance and Strength effects it successfully installed and removes them only while each visible effect still matches. A distinct 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.
Leaf reconciles opted-in online players approximately once per second. Effects cleared by death, milk, commands, or another plugin are restored automatically. A temporary external Resistance or Strength effect is left untouched, the leaf prefix remains visible, and Leaf restores its configured effect after the external effect ends.
## 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.
-12
View File
@@ -1,12 +0,0 @@
---
okf_version: "0.1"
---
# Leaf Design
This bundle documents Leaf, a Spigot plugin that lets players opt into visible Resistance and Strength boosts while requiring them to relinquish that protection when they attack another player.
## Explore
- [User stories](user-stories/index.md)
- [Design log](log.md)
-97
View File
@@ -1,97 +0,0 @@
# Leaf Design Log
## 2026-08-10
### Initial protection design
- Leaf is available to any player who opts in; the seven-day period applies only to onboarding reminders.
- Protection grants Resistance I by default and remains active until the player opts out, attacks another player, an administrator disables it for the player, or Leaf is globally disabled.
- Receiving an attack does not remove protection, but retaliating does, including attributable Thorns damage.
- PvP attribution covers melee, projectiles, potions, owned animals, Thorns, and indirect damage when Spigot exposes a reliable responsible player.
- Opted-in players receive a leaf prefix in tab, overhead names, and chat where supported.
- New players receive an onboarding reminder on every login for seven calendar days after first joining.
- Global disablement suppresses effects, prefixes, and reminders without erasing player choices.
- Administration uses the permission-aware `/leaf` command tree with autocomplete.
- Builds and releases follow the neighboring Spigot Base Gradle and Gitea pipeline.
- The design bundle is committed together; later implementation work is committed separately for each user story.
### Implementation started
- Established the Java 17 Gradle project, strict compilation, JUnit test lifecycle, Leaf plugin metadata, Gradle wrapper, and Gitea CI and semantic-release workflows.
- Verified the initial pipeline with `./gradlew clean check jar` and produced `build/libs/leaf-0.1.0-SNAPSHOT.jar`.
- US-007 remains in progress until the story-by-story implementation commit requirement has been verified.
### Configuration and persistence checkpoint
- Added validated settings with Resistance I and seven-day onboarding defaults.
- Added defensive UUID-keyed YAML player state with RFC 3339 timestamps, forward-field preservation, and atomic replacement where supported.
- Added safe plugin initialization and periodic dirty-state persistence.
- US-006 remains in progress pending live runtime setting commands and Leaf effect ownership behavior.
### US-001 player protection completed
- Added permission-aware player commands with idempotent choice changes and detailed status reporting.
- Added immediate durable opt-in persistence, join-time restoration, and quiet infinite Resistance reconciliation.
- Leaf tracks its live Resistance fingerprint and conservatively preserves a visibly distinct Resistance effect.
- Verified player choice, lock, persistence, command, autocomplete, and build behavior with `./gradlew clean check jar`.
### US-003 identity presentation checkpoint
- Added configurable, color-compatible prefixes to standard chat display names and player-list names.
- Added conservative main-scoreboard team prefixes for overhead names without displacing unrelated teams.
- Name cleanup restores only values Leaf installed; third-party changes are preserved.
- Documented scoreboard, custom chat, tab-list, and per-viewer scoreboard limitations.
- US-003 remains in progress until combat and global administration paths are verified.
### US-002 combat opt-out completed
- Added modern Spigot damage-source attribution for direct attacks, projectiles, thrown potions, area clouds, tamed animals, TNT, Thorns, and other reliably attributed sources.
- Ambiguous, cancelled, zero-damage, environmental, and self damage do not opt out a player.
- Combat opt-out bypasses administrative locks, immediately reconciles effect and prefix state, persists once, and sends one explanatory message.
- Verified attribution, victim safety, lock bypass, duplicate suppression, and the full build with `./gradlew clean check jar`.
### US-004 onboarding completed
- Join observation now sends the configurable welcome message on each login before the original first-join instant plus the configured calendar-day window.
- The exact expiration boundary and globally disabled behavior suppress reminders without resetting first-join state.
- The default message names all player commands and explains automatic combat opt-out.
- Verified repeated joins, expiration, global suppression, first-join retention, and the full build with `./gradlew clean check jar`.
### US-005 administration and US-003 identity completed
- Added the complete `leaf.admin` command tree for global state, live strength, player status, saved choice, and locks.
- Runtime global and strength changes persist before activation and immediately reconcile all online players.
- 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`.
### US-007 build and release completed
- Verified dedicated conventional implementation commits for protection, identity, combat, onboarding, administration, configuration verification, and logout cleanup in addition to the existing build foundation.
- Kept reserved OKF index and log documents in their specification-defined structures.
- Verified all user stories are done, all acceptance criteria are checked, the repository has no remote, and no push was performed.
- Final verification passed with `./gradlew clean check jar` and `okflint` validation.
### US-003 sticky-prefix regression fixed
- Reproduced the cleanup failure that occurred when another plugin changed a Leaf-decorated display name.
- Leaf now removes exact managed-prefix text without discarding surrounding third-party formatting, clears stale prefixes after reload, and normalizes repeated prefixes before applying one leaf.
- Added regression coverage for wrapped names, duplicate stale prefixes, untracked stale prefixes, unrelated formatting, tab names, and scoreboard cleanup.
- Verified the fix with `./gradlew clean check jar`.
### Strength protection added
- Opted-in players now receive quiet, infinite Strength I alongside Resistance I by default; both effects must be active for Leaf status to report active protection.
- Added independent `resistance-level` and `strength-level` settings with live `/leaf effect <resistance|strength> <1-5>` administration and positional autocomplete.
- Retained `/leaf strength <1-5>` as a deprecated compatibility alias for changing Resistance.
- Opt-out, combat, administrative disablement, and global disablement remove both Leaf-managed effects while conservatively preserving distinct external effects.
- Minecraft calculates the initiating PvP hit before Leaf can process the damage event, so that first hit can include the configured Strength bonus before automatic opt-out.
- Verified settings, runtime reconciliation, status, combat cleanup, persistence, command compatibility, autocomplete, and the complete build with `./gradlew clean check jar`.
-9
View File
@@ -1,9 +0,0 @@
# Leaf User Stories
1. [US-001: Opt into Leaf protection](us-001-opt-into-leaf-protection.md)
2. [US-002: Relinquish protection when attacking](us-002-relinquish-protection-when-attacking.md)
3. [US-003: Identify protected players](us-003-identify-protected-players.md)
4. [US-004: Introduce new players to Leaf](us-004-introduce-new-players-to-leaf.md)
5. [US-005: Administer Leaf](us-005-administer-leaf.md)
6. [US-006: Configure and persist Leaf](us-006-configure-and-persist-leaf.md)
7. [US-007: Build and release Leaf](us-007-build-and-release-leaf.md)
@@ -1,30 +0,0 @@
---
type: User Story
title: "US-001: Opt into Leaf protection"
description: Let players voluntarily receive and relinquish a persistent Resistance boost.
status: done
---
# US-001: Opt into Leaf protection
As a **player**, I want to opt into Leaf protection so that I can receive a modest defensive boost while playing on a hard server.
## Acceptance criteria
- [x] Players with the `leaf.use` permission, granted by default, can use `/leaf on`, `/leaf off`, and `/leaf status`.
- [x] `/leaf on` records the player's opt-in choice and grants Resistance I while Leaf is globally enabled.
- [x] Resistance remains continuously effective without distracting expiry or renewal messages or particles.
- [x] `/leaf off` records the player's opt-out choice and immediately removes only the Resistance effect managed by Leaf.
- [x] `/leaf status` clearly distinguishes the player's saved choice, active protection, administrative lock, and global Leaf state.
- [x] Repeating an already-satisfied `on` or `off` command is safe and explains that no change was needed.
- [x] Opt-in choices are keyed by UUID and survive logout and server restart.
- [x] A player who joins while opted in regains protection when Leaf is enabled.
- [x] Player-command autocomplete suggests only valid next arguments available to the sender.
- [x] Opted-in players receive quiet Strength I alongside Resistance I by default.
- [x] Player status reports active protection only when both configured Leaf effects are effective.
## Related
- [US-002: Relinquish protection when attacking](us-002-relinquish-protection-when-attacking.md)
- [US-003: Identify protected players](us-003-identify-protected-players.md)
- [US-005: Administer Leaf](us-005-administer-leaf.md)
@@ -1,31 +0,0 @@
---
type: User Story
title: "US-002: Relinquish protection when attacking"
description: Remove Leaf protection when an opted-in player attacks another player.
status: done
---
# US-002: Relinquish protection when attacking
As a **player facing PvP**, I want Leaf protection to belong only to non-aggressors so that protected players cannot keep its advantage after fighting back.
## Acceptance criteria
- [x] When an opted-in player damages another player, Leaf automatically changes the attacker's saved choice to opted out.
- [x] Automatic opt-out immediately removes Leaf-managed Resistance and the attacker's leaf prefix.
- [x] Automatic opt-out also immediately removes Leaf-managed Strength.
- [x] The attacker receives a clear chat message explaining that attacking another player disabled Leaf and that they may opt in again when permitted.
- [x] Receiving player-caused damage without retaliating does not change the protected player's choice or Resistance.
- [x] Retaliatory damage counts as attacking, including Thorns damage attributable to the protected player.
- [x] Direct melee attacks and player-fired projectiles are attributed to the attacking player.
- [x] Harmful splash or lingering potion damage is attributed to the player who threw the potion.
- [x] Damage caused by a tamed animal is attributed to its player owner when Spigot exposes that ownership.
- [x] TNT, fire, and other indirect damage trigger opt-out only when Spigot exposes a reliable responsible player; ambiguous environmental damage does not opt out an innocent player.
- [x] PvP-triggered opt-out applies even when an administrator has locked the player's preference.
- [x] A single attack produces no duplicate state changes or duplicate notifications.
## Related
- [US-001: Opt into Leaf protection](us-001-opt-into-leaf-protection.md)
- [US-003: Identify protected players](us-003-identify-protected-players.md)
- [US-005: Administer Leaf](us-005-administer-leaf.md)
@@ -1,30 +0,0 @@
---
type: User Story
title: "US-003: Identify protected players"
description: Mark actively protected players with a visible leaf prefix wherever Spigot supports it.
status: done
---
# US-003: Identify protected players
As a **server participant**, I want protected players to be visibly identified so that their Leaf status is clear before I interact with them.
## Acceptance criteria
- [x] An actively protected player has a configurable leaf prefix before their name in the player list.
- [x] An actively protected player has the same prefix before their overhead name where Spigot supports it.
- [x] An actively protected player has the same prefix in standard Spigot chat where the active chat format supports it.
- [x] The default prefix uses a leaf symbol with readable spacing and Minecraft-compatible formatting.
- [x] The prefix is removed immediately when the player opts out or is opted out by PvP or an administrator.
- [x] Prefixes are suppressed while Leaf is globally disabled and restored for online opted-in players when it is re-enabled.
- [x] Prefix updates do not overwrite unrelated display-name text where the Spigot API allows the values to coexist.
- [x] Scoreboard-team and third-party chat-plugin compatibility limitations are documented, and unsupported integrations fail without affecting protection state.
- [x] Repeated opt-in or reconciliation never produces multiple Leaf prefixes.
- [x] Opt-out removes every managed Leaf prefix while preserving unrelated name formatting.
- [x] A stale Leaf prefix left by an earlier plugin instance is normalized instead of duplicated.
## Related
- [US-001: Opt into Leaf protection](us-001-opt-into-leaf-protection.md)
- [US-002: Relinquish protection when attacking](us-002-relinquish-protection-when-attacking.md)
- [US-006: Configure and persist Leaf](us-006-configure-and-persist-leaf.md)
@@ -1,27 +0,0 @@
---
type: User Story
title: "US-004: Introduce new players to Leaf"
description: Remind players how to control Leaf during their first seven calendar days on the server.
status: done
---
# US-004: Introduce new players to Leaf
As a **new player**, I want a brief explanation of Leaf when I join so that I know protection is available and understand how to control it.
## Acceptance criteria
- [x] Leaf records the first observed join time for each player by UUID as an RFC 3339 UTC timestamp.
- [x] On every login before the first-join timestamp plus seven calendar days, the player receives a concise welcome message explaining Leaf protection.
- [x] The welcome message tells the player to use `/leaf on`, `/leaf off`, and `/leaf status`.
- [x] The message makes clear that attacking another player automatically opts the attacker out.
- [x] Players no longer receive the welcome message once their seven-day onboarding period expires.
- [x] Existing first-join timestamps are not reset by logout, restart, opt-in changes, or global disablement.
- [x] No onboarding reminders are shown while Leaf is globally disabled.
- [x] Re-enabling Leaf allows reminders to resume on subsequent logins only for players whose original seven-day period has not expired.
## Related
- [US-001: Opt into Leaf protection](us-001-opt-into-leaf-protection.md)
- [US-005: Administer Leaf](us-005-administer-leaf.md)
- [US-006: Configure and persist Leaf](us-006-configure-and-persist-leaf.md)
@@ -1,35 +0,0 @@
---
type: User Story
title: "US-005: Administer Leaf"
description: Give administrators command-based control over global and per-player Leaf behavior.
status: done
---
# US-005: Administer Leaf
As a **server administrator**, I want to control Leaf globally and per player so that I can support players, prevent misuse, and suspend protection for server events.
## Acceptance criteria
- [x] Administrative commands require `leaf.admin`, which server operators receive by default.
- [x] `/leaf enabled <on|off>` globally enables or disables Leaf.
- [x] Global disablement immediately suppresses Leaf-managed Resistance, prefixes, and onboarding reminders without erasing player opt-in choices.
- [x] Global enablement immediately restores protection and prefixes for eligible online opted-in players.
- [x] `/leaf player <name|uuid> status` reports the player's identity, saved opt-in choice, active-protection state, lock state, and first-join time.
- [x] `/leaf player <name|uuid> enabled <on|off>` changes the player's saved opt-in choice and immediately reconciles their online effect and prefix.
- [x] `/leaf player <name|uuid> locked <on|off>` controls whether that player can change their own saved choice.
- [x] A locked player receives a clear explanation when `/leaf on` or `/leaf off` is denied.
- [x] PvP-triggered opt-out remains authoritative even for a locked player.
- [x] Player targets resolve online names, previously known names, and UUIDs without confusing two players who used the same name.
- [x] `/leaf strength <1-5>` remains a deprecated compatibility alias that changes the live Resistance level.
- [x] `/leaf effect resistance <1-5>` changes the live Resistance level and immediately updates protected online players.
- [x] `/leaf effect strength <1-5>` changes the live Strength level and immediately updates protected online players.
- [x] Effect-command autocomplete suggests effect names and valid levels for the current argument position.
- [x] Administrative autocomplete is permission-aware and suggests valid subcommands, known player targets, properties, levels, and `on` or `off` values for the current argument position.
- [x] Every successful mutation reports exactly what changed, and invalid requests make no partial changes.
## Related
- [US-001: Opt into Leaf protection](us-001-opt-into-leaf-protection.md)
- [US-002: Relinquish protection when attacking](us-002-relinquish-protection-when-attacking.md)
- [US-006: Configure and persist Leaf](us-006-configure-and-persist-leaf.md)
@@ -1,31 +0,0 @@
---
type: User Story
title: "US-006: Configure and persist Leaf"
description: Provide validated configuration and durable, defensive storage for Leaf behavior.
status: done
---
# US-006: Configure and persist Leaf
As a **server operator**, I want Leaf settings and player state to be validated and durable so that protection remains predictable across restarts.
## Acceptance criteria
- [x] Configuration supports the global enabled state, Resistance and Strength levels, leaf prefix, onboarding duration, and player-facing messages.
- [x] Resistance strength defaults to level I and accepts only Minecraft Resistance levels I through V.
- [x] Strength defaults to level I and accepts only Minecraft Strength levels I through V.
- [x] Runtime changes made through `/leaf enabled` and `/leaf effect` 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.
- [x] Removing Leaf-managed Resistance or Strength does not remove a distinct corresponding 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
- [US-001: Opt into Leaf protection](us-001-opt-into-leaf-protection.md)
- [US-004: Introduce new players to Leaf](us-004-introduce-new-players-to-leaf.md)
- [US-005: Administer Leaf](us-005-administer-leaf.md)
- [US-007: Build and release Leaf](us-007-build-and-release-leaf.md)
@@ -1,30 +0,0 @@
---
type: User Story
title: "US-007: Build and release Leaf"
description: Give maintainers repeatable Spigot builds, automated verification, and versioned Gitea releases.
status: done
---
# US-007: Build and release Leaf
As a **plugin maintainer**, I want automated builds and releases modeled on Spigot Base so that tested, correctly versioned Leaf artifacts can be distributed consistently.
## Acceptance criteria
- [x] The Gradle project compiles against Spigot API `26.2-R0.1-SNAPSHOT` using a Java 17 toolchain.
- [x] Compiler lint warnings fail the build.
- [x] Automated JUnit 5 tests run as part of the Gradle check lifecycle.
- [x] Pushes and pull requests build and test Leaf in Gitea Actions.
- [x] Pull requests validate conventional commit messages.
- [x] CI stores a development Leaf JAR as a workflow artifact.
- [x] Main-branch conventional commits drive semantic versioning.
- [x] A successful release builds a versioned Leaf JAR and attaches it to the corresponding Gitea release.
- [x] Build files, Gradle wrapper, workflows, and release behavior follow `../spigot-base/` where applicable while using Leaf names and identifiers.
- [x] The initial approved user-story bundle is kept in one documentation commit.
- [x] Subsequent implementation work is organized into a separate conventional commit for each implemented user story.
- [x] No remote push is performed until the maintainer confirms that the Gitea repository and release token secret are ready.
## Related
- [US-006: Configure and persist Leaf](us-006-configure-and-persist-leaf.md)
- [User-story catalog](index.md)
@@ -37,6 +37,12 @@ public final class LeafPlugin extends JavaPlugin {
return;
}
getServer().getScheduler().runTaskTimer(
this,
new LeafRecoveryTask(runtime),
LeafRecoveryTask.INTERVAL_TICKS,
LeafRecoveryTask.INTERVAL_TICKS
);
getServer().getScheduler().runTaskTimer(this, this::saveState, 600L, 600L);
getLogger().info("Leaf enabled.");
}
@@ -88,23 +88,38 @@ public final class LeafProtection {
ignored -> new HashMap<>()
);
PotionEffect previous = owned.get(desired.getType());
if (previous != null && !previous.equals(desired)) {
PotionEffect active = player.getPotionEffect(desired.getType());
if (previous != null && previous.equals(active)) {
if (previous.equals(desired)) {
return true;
}
removeMatching(player, previous);
owned.remove(desired.getType());
active = player.getPotionEffect(desired.getType());
} else if (previous != null) {
owned.remove(desired.getType());
}
PotionEffect active = player.getPotionEffect(desired.getType());
if (desired.equals(active) && desired.equals(owned.get(desired.getType()))) {
if (active != null) {
removeEmptyOwnership(playerId, owned);
return true;
}
boolean applied = player.addPotionEffect(desired);
if (applied) {
owned.put(desired.getType(), desired);
}
removeEmptyOwnership(playerId, owned);
return applied;
}
private void removeEmptyOwnership(
UUID playerId,
Map<PotionEffectType, PotionEffect> owned
) {
if (owned.isEmpty()) {
appliedEffects.remove(playerId);
}
return applied || desired.equals(active);
}
private static void removeMatching(Player player, PotionEffect expected) {
@@ -0,0 +1,19 @@
package games.dmg.leaf;
import java.util.Objects;
/** Periodically restores eligible protection after effects are cleared or expire. */
public final class LeafRecoveryTask implements Runnable {
static final long INTERVAL_TICKS = 20L;
private final LeafRuntime runtime;
public LeafRecoveryTask(LeafRuntime runtime) {
this.runtime = Objects.requireNonNull(runtime, "runtime");
}
@Override
public void run() {
runtime.reconcileAllOnline();
}
}
@@ -0,0 +1,20 @@
package games.dmg.leaf;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import org.junit.jupiter.api.Test;
final class LeafRecoveryTaskTest {
@Test
void reconcilesAllOnlinePlayersEverySecond() {
LeafRuntime runtime = mock(LeafRuntime.class);
LeafRecoveryTask recovery = new LeafRecoveryTask(runtime);
recovery.run();
verify(runtime).reconcileAllOnline();
assertEquals(20L, LeafRecoveryTask.INTERVAL_TICKS);
}
}
@@ -68,6 +68,38 @@ final class LeafRuntimeTest {
verify(protection).isEffective(player, 1, 1);
}
@Test
void recoveryReconcilesEligiblePlayersAndLeavesOptedOutPlayersUnprotected()
throws Exception {
UUID protectedId = UUID.randomUUID();
UUID optedOutId = UUID.randomUUID();
Player protectedPlayer = player(protectedId, "Alex");
Player optedOutPlayer = player(optedOutId, "Steve");
Server server = mock(Server.class);
when(server.getPlayer(protectedId)).thenReturn(protectedPlayer);
when(server.getPlayer(optedOutId)).thenReturn(optedOutPlayer);
org.mockito.Mockito.doReturn(List.of(protectedPlayer, optedOutPlayer))
.when(server).getOnlinePlayers();
LeafProtection protection = mock(LeafProtection.class);
when(protection.apply(protectedPlayer, 1, 1)).thenReturn(true);
LeafRuntime runtime = runtime(
server,
protection,
temporaryDirectory.resolve("recovery.yml")
);
Instant observedAt = Instant.parse("2026-08-10T00:00:00Z");
runtime.observe(protectedPlayer, observedAt);
runtime.observe(optedOutPlayer, observedAt);
runtime.setOwnChoice(protectedPlayer, true);
org.mockito.Mockito.clearInvocations(protection);
runtime.reconcileAllOnline();
verify(protection).apply(protectedPlayer, 1, 1);
verify(protection, never()).apply(optedOutPlayer, 1, 1);
verify(protection).remove(optedOutPlayer);
}
@Test
void welcomesPlayersOnEachJoinOnlyDuringTheConfiguredWindow() throws Exception {
UUID playerId = UUID.randomUUID();