Skip to content

Stop skills from retaining every event they handle - #152

Merged
dmccoystephenson merged 1 commit into
mainfrom
fix/skill-event-retention
Aug 12, 2026
Merged

Stop skills from retaining every event they handle#152
dmccoystephenson merged 1 commit into
mainfrom
fix/skill-event-retention

Conversation

@dmccoystephenson

Copy link
Copy Markdown
Member

Every skill kept a HashSet of every event it had ever handled, and nothing ever removed an entry, so each handled event — and the block, player and entities it references — stayed reachable for as long as the server ran.

The set existed only to stop a skill acting twice on one event. That duplicate is now prevented where it originated, so the memo is no longer needed and has been removed.

What changed

  • A skill now listens only for the events its trigger methods accept. register() previously registered a listener for all twelve values of Triggers for every skill, even though handle(Event) discards an event no trigger method accepts. Registration is now driven by the trigger methods found on the class, which for the skills in this repository is between one and three listeners each instead of twelve.
  • Each listener forwards only events of the exact class it was registered for. Bukkit keys its handler lists on whichever class declares one, so a registration made for an event class that shares its handler list with a relative also receives that relative's events; a skill declaring triggers for both therefore saw one event twice. Dispatch in handle(Event) was already on the event's exact class, so the filter changes nothing about which trigger method runs.
  • The calledEvents set is gone, along with the retention it caused.
  • A package-private getPluginManager() seam was added, because Bukkit.getPluginManager() reads static server state that a unit test has no server to provide.

The event classes named by the skills' constructors are exactly the twelve values of Triggers, so no skill loses a listener it was relying on.

Test plan

  • mvn test — 36 tests, all passing (33 before, 3 added)
  • Regression evidence: with the previous behaviour reinstated locally, all three added tests fail (Field 'calledEvents' retains the handled event, plus the two registration assertions), and pass once the change is restored
  • Added handle_doesNotRetainTheEventItHandled, which asserts that no field of the skill holds the event after it has been handled
  • Added register_registersOnlyTheEventsTheSkillHasATriggerMethodFor and register_deliversAnEventToItsTriggerMethodOnlyOnce
  • Not covered by any automated test: behaviour on a live server. The retention is observable only as heap growth over a long session, and the .testcontainer Spigot harness is not run in CI and was not run here. A smoke test on a real server is worth doing before this reaches users.

Documentation

No command, permission node or configuration key changes, so HelpCommand.java, COMMANDS.md, USER_GUIDE.md and CONFIG.md are unaffected. A Fixed entry has been added to CHANGELOG.md under [Unreleased].

Issues deferred this cycle

The remaining open issues were left untouched: #87, #109, #97, #127 and #88 are feature or persistence work larger than one polish-sized change; #128, #129, #124, #112, #119 and #140 already have a draft pull request open against them (#131, #130, #132, #118, #120, #141); #116 changes pom.xml, which is on this loop's do-not-auto-merge list and is better handled where a human can approve the dependency bump; #77, #86, #89 and #90 are feature requests.

Closes #149

This PR description was drafted during a Gardener session (https://github.com/Stephenson-Software/gardener).


drafted by Claude on behalf of Daniel Stephenson

Each skill kept a HashSet of every event passed to handle(), so that an
event registered under more than one trigger was only acted on once. The
set was never cleared, so every handled event -- and the block, player and
entities it references -- stayed reachable for the lifetime of the server.

The duplicate delivery is now prevented at its source: a skill registers a
listener only for the event classes its trigger methods accept, and each
listener forwards only events of the exact class it was registered for,
since Bukkit delivers an event to registrations made for any class sharing
its handler list. handle() already dispatches on the event's exact class,
so nothing else changes about which trigger runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dmccoystephenson

Copy link
Copy Markdown
Member Author

Self-review rubric (scored against the diff and command output, not judgement):

  • Scope: PASS — three files, each required for AbstractSkill never clears calledEvents, retaining every handled event for the server's lifetime #149: the field and its two uses in AbstractSkill, the test that guards them, and the CHANGELOG entry. No formatting or comment churn elsewhere. One judgement call is flagged below.
  • Tests-new: PASS — the one method added, getPluginManager(), is exercised by both register_* tests through the CountingSkill override; the two changed methods (handle, register) each gained a test.
  • Tests-fix: PASS (empirical) — with the previous behaviour reinstated locally (the calledEvents memo restored and register() returned to registering a fixed trigger list with an unfiltered executor), all three added tests fail, one of them as Field 'calledEvents' retains the handled event; with the change restored, mvn test reports 36/36 passing. Scored from the run, not from reasoning.
  • Sibling structure: PASS — no new files. CountingSkill follows the conventions of the neighbouring tests: MockitoJUnitRunner.Silent, real Bukkit events constructed rather than mocked (their getters are final), collaborators mocked, and a javadoc stating what the fixture stands in for.
  • Sibling renames: no signal — nothing was renamed.
  • Docs: PASS — no command, permission node or config key changed, so the HelpCommand.java / COMMANDS.md / USER_GUIDE.md / CONFIG.md rows are untouched by design; CHANGELOG.md gained a Fixed entry under [Unreleased].
  • Issue resolution: PASS — the field named by AbstractSkill never clears calledEvents, retaining every handled event for the server's lifetime #149 is deleted, and the de-duplication it provided is preserved by other means rather than dropped.
  • CI: PASSbuild green on the head commit of this branch.
  • Skill/stat parity: no signalStatsCommand, TopCommand and SkillCommand are untouched; no stat read or display path changed.
  • Testcontainer harness: UNVERIFIED — the behaviour that actually matters here is Bukkit's own handler-list resolution on a live server, which mvn test cannot exercise and which the .testcontainer smoke harness (not run in CI, and not run for this change) is the only way to observe. A real-server smoke test is worth doing before this reaches users; the unit tests stand in for that delivery rather than reproducing it.

Judgement call left for a reviewer rather than fixed unilaterally: narrowing register() to the skill's own trigger classes is strictly more than deleting the memo, and could be argued to be scope creep. It is included because it is the mechanism that removes the duplicate delivery the memo existed to absorb — without it, the memo could not be deleted safely. The event classes named by the skills' constructors are exactly the twelve values of Triggers, so no skill loses a listener.

Two observations fall outside the diff and have been filed rather than folded in:

  • src/main/java/dansplugins/simpleskills/enums/Triggers.java — the enum is left with no consumer in src/main once register() stops iterating it. Filed as Triggers enum is no longer referenced by any code #153; deleting a public enum was judged a separate decision from this fix.
  • src/main/java/dansplugins/simpleskills/skill/skills/Hardiness.java:31 — the skill declares only EntityDamageEvent, and dispatch has always been on the event's exact class, so damage from a mob or player (an EntityDamageByEntityEvent) has never reached it. Long-standing and unchanged by this pull request; filed as Hardiness never triggers on damage dealt by an entity #154 because the fix is a gameplay decision.

This review was performed and posted during a Gardener session (https://github.com/Stephenson-Software/gardener).


drafted by Claude on behalf of Daniel Stephenson

@dmccoystephenson
dmccoystephenson merged commit 43deb5a into main Aug 12, 2026
1 check passed
@dmccoystephenson
dmccoystephenson deleted the fix/skill-event-retention branch August 12, 2026 08:08
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.

AbstractSkill never clears calledEvents, retaining every handled event for the server's lifetime

1 participant