Skip to content

refactor: replace static manager singletons with Guice-injected services#17

Open
nbdSteve wants to merge 9 commits into
masterfrom
fm/armorplus-4e-registries-r9
Open

refactor: replace static manager singletons with Guice-injected services#17
nbdSteve wants to merge 9 commits into
masterfrom
fm/armorplus-4e-registries-r9

Conversation

@nbdSteve

@nbdSteve nbdSteve commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Intent

Behavior-preserving dependency-injection refactor of the ArmorPlus Spigot plugin, plus a follow-up comment-style cleanup.

Core change: replace the two static god-object singletons (SetManager, SetPlayerManager) with Guice-injected instances. ArmorSetCatalog (adapter over the pure model.set.ArmorSetRegistry) replaces SetManager; PlayerArmorSetService (adapter over the pure model.player.PlayerArmorWearerRegistry, resolving set names through the injected catalog) replaces SetPlayerManager. A new ArmorPlusModule Guice module is the composition root, built once in ArmorPlus.onEnable and used to resolve the shared singletons that are then threaded to listeners, commands, and the PAPI expansion as constructor-injected instances instead of reached-for statics. Google Guice 5.1.0 (JSR-330) was chosen over Dagger to avoid a second annotation processor alongside Lombok; it is shaded/relocated under gg.steve.mc.ap.lib.*. The pure model registries are deliberately bound by type in the module (not annotated) so the domain layer stays DI-framework-free. ArmorSetCatalog.getSet keeps its original null/empty guard so callers never see an exception for a blank/unknown set name (ArmorSetId.of rejects null/empty). Two pre-existing latent NPEs on ConfigManager.CONFIG.get() (in ArmorPlus.getApGui and ArmorSetCatalog.loadSets) were hardened with Objects.requireNonNull during earlier CI fix rounds; happy-path behavior unchanged.

Tests: the static-manager tests were converted to construct the new injected instances directly (SetManagerTest->ArmorSetCatalogTest, SetPlayerManagerTest->PlayerArmorSetServiceTest), keeping all behavioral assertions byte-identical; only the static->instance plumbing changed. A new ArmorPlusModuleTest drives the real Guice injector end-to-end to prove the shared-singleton wiring. The sacrosanct characterization suite (damage calc, warp, XP, potion, set-detection, messaging) is untouched and byte-identical.

This latest commit is a standing-convention comment cleanup requested by the user: comments must be extremely terse, documenting only non-obvious WHY. It purges the bloated javadoc and restating comments added during the DI work (one-line method narration, class summaries derivable from the name), keeping only a few genuine WHY notes. It is comment-only: zero non-comment code lines changed, all 235 tests still pass, and the 100% core-coverage gate still holds. Do NOT change package structure - a separate decision on that is pending.

What Changed

  • Replaced the SetManager and SetPlayerManager static singletons with constructor-injected ArmorSetCatalog and PlayerArmorSetService, both adapters over new Bukkit-free domain registries (model.set.ArmorSetRegistry, model.player.PlayerArmorWearerRegistry); PlayerArmorSetService.getSetPlayer now reconstructs the SetPlayer on demand from the wearer record, and set iteration follows deterministic loaded-config order.
  • Added an ArmorPlusModule Guice composition root, built once in ArmorPlus.onEnable, to resolve the shared singletons and thread them through listeners, commands, the PAPI expansion, and the GUI in place of static .get() access; pulled in Guice 5.1.0 (JSR-330) in pom.xml, shaded and relocated under gg.steve.mc.ap.lib.* with annotation-only transitives excluded.
  • Migrated the former manager tests to construct the injected instances directly (ArmorSetCatalogTest, PlayerArmorSetServiceTest) with assertions byte-identical, added ArmorPlusModuleTest driving the real injector end-to-end plus registry and PlayerCommandListenerTest coverage, and trimmed the DI-era comments to terse WHY-only.

Risk Assessment

✅ Low: Behavior-preserving DI refactor with logic-identical registries, complete Guice shade relocation, comprehensive new/converted tests, and only benign, mostly-intentional semantic side-effects - no reachable correctness regressions found.

Testing

Ran the full JDK 25 Maven build (mvn -B clean verify): all 235 tests pass and the 100% core-coverage gate holds. Beyond green tests, I confirmed the intent at the product level — inspected the shaded JAR to prove Guice, javax.inject, guava, and aopalliance are relocated under gg/steve/mc/ap/lib/* (with annotation-only transitives excluded), which is the real classpath-collision guarantee for a shared Spigot server; captured two end-to-end evidence transcripts showing the actual ArmorPlusModule injector wiring shared singletons that round-trip wearer state and routing player set-commands to their GUIs; verified the head commit changes zero non-comment code lines; and confirmed the sacrosanct characterization suite is byte-identical to base. No product UI surface exists (headless plugin JAR), so CLI/transcript and jar-structure artifacts are the appropriate reviewer-visible evidence. Cleaned the build output afterward, leaving a clean worktree.

Evidence: Guice composition-root wiring transcript (live injector, shared singletons round-trip state)

=== Guice composition root wires shared singletons over the pure registries === (built from the real ArmorPlusModule via a live Injector - the same wiring ArmorPlus.onEnable uses) injector.getInstance(ArmorSetCatalog) -> singleton true injector.getInstance(PlayerArmorSetService) -> singleton true catalog.getRegistry() == injected ArmorSetRegistry singleton -> true catalog.register(dragon); service.addSetPlayer(player, "dragon") service.isWearingSet(player) -> true service.getSetPlayer(player).getSet() -> same instance registered on catalog: true

=== Guice composition root wires shared singletons over the pure registries ===
(built from the real ArmorPlusModule via a live Injector - the same wiring ArmorPlus.onEnable uses)
injector.getInstance(ArmorSetCatalog)        -> singleton true
injector.getInstance(PlayerArmorSetService)  -> singleton true
catalog.getRegistry() == injected ArmorSetRegistry singleton -> true
catalog.register(dragon); service.addSetPlayer(player, "dragon")
service.isWearingSet(player)                 -> true
service.getSetPlayer(player).getSet()        -> same instance registered on catalog: true
Evidence: Player set-command routing transcript (injected catalog)

=== Each set command routes to its own set; unknown command passes through === Registry keys (ArmorSetId): [dragon, knight] player types "/dragon" -> intercepted=true player types "/knight" -> intercepted=true player types "/spawn" -> intercepted=false

=== Each set command routes to its own set; unknown command passes through ===
Registry keys (ArmorSetId): [dragon, knight]
player types "/dragon"              -> intercepted=true
player types "/knight"              -> intercepted=true
player types "/spawn"               -> intercepted=false
Evidence: Shaded-jar DI library relocation verification
Relocated lib packages in target/ArmorPlus-v2.3.6.jar:
gg/steve/mc/ap/lib/aopalliance/
gg/steve/mc/ap/lib/commons/
gg/steve/mc/ap/lib/guava/
gg/steve/mc/ap/lib/inject/ (guice)
gg/steve/mc/ap/lib/javax/ (javax.inject)
Unrelocated com/google/inject at root: 0
Unrelocated root javax/inject: 0
Excluded transitives (checkerframework/errorprone/j2objc/findbugs): 0 each

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 2 infos
  • ℹ️ src/main/java/gg/steve/mc/ap/model/set/ArmorSetRegistry.java:11 - ArmorSetRegistry backs its map with a LinkedHashMap and getAll() returns an unmodifiable view, whereas the old SetManager.getSets() returned a live, hash-ordered HashMap. Consumers that pick the 'first' set a player is wearing (PlayerEquipListener.join, PlayerUnequipListener.quit, PlayerArmorSetService.init) now iterate in deterministic config (loaded-sets) order instead of arbitrary hash order. This only changes the outcome in the rare case where a player's armor simultaneously matches two sets; the new deterministic behavior is an improvement, and all call sites are read-only so the unmodifiable view breaks nothing. Noted so the semantic change is on the record.
  • ℹ️ src/main/java/gg/steve/mc/ap/player/PlayerArmorSetService.java:49 - PlayerArmorSetService.getSetPlayer now reconstructs a fresh SetPlayer (allocating PlayerId + ArmorSetId and doing two map lookups plus a catalog re-resolve) on every call, where the old SetPlayerManager returned a single cached SetPlayer instance stored at add time. In ArmorPlusExpansion this runs on the PAPI placeholder path, which can be invoked frequently (e.g. per-tick scoreboards) and calls getSetPlayer multiple times per request. The extra allocation is negligible for modern JVM GC and the on-demand reconstruction is explicitly documented as intended design in AGENTS.md, so no action needed - flagged only as an efficiency-vs-old-behavior note.
✅ **Test** - passed

✅ No issues found.

  • JAVA_HOME=<corretto-25> mvn -B clean verify — BUILD SUCCESS, 235 tests pass, JaCoCo check-core gate met
  • Shaded-jar relocation check: unzip -l target/ArmorPlus-v2.3.6.jar confirms guice/javax.inject/guava/aopalliance relocated under gg/steve/mc/ap/lib/*, zero unrelocated com/google/inject or root javax/inject, checker-qual/error_prone/j2objc/jsr305 excluded
  • ArmorPlusModuleTest.injectorWiresSharedSingletonsAndTheyRoundTripState — real Guice injector wires shared singletons that round-trip register→isWearingSet→getSetPlayer state (evidence transcript)
  • PlayerCommandListenerTest — injected catalog routes /dragon and /knight to their GUIs, /spawn passes through (evidence transcript)
  • Head commit 91f3a7d comment-only check: comment/blank-stripped before/after of all 12 files is byte-identical
  • Characterization suite (git diff base HEAD over data/message/warp/potion tests) — byte-identical to base
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Stephen Goodhill added 2 commits July 22, 2026 17:37
Introduce PlayerArmorWearerRegistry (model.player) holding the
PlayerId-to-ArmorSetWearer state and ArmorSetRegistry (model.set)
holding the ArmorSetId-to-set mapping. Both are Bukkit-free,
self-contained, and unit-tested to 100% line and branch.

SetPlayerManager and SetManager keep their existing static public API
but delegate to a held registry instance; getSetPlayer reconstructs the
Bukkit SetPlayer on demand from the wearer record, and getSet guards
null/empty names before ArmorSetId rejects them. The characterization
tests are unchanged, proving observable behavior is preserved.
public class SetManager {
private static Map<String, YamlFileUtil> setConfigs;
private static Map<String, Set> sets;
private static final ArmorSetRegistry<Set> registry = new ArmorSetRegistry<>();

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than having everything static can be pass around instances? We shouldnt need to rely on static maps, pass around the registry class instance - might need to use DI

public static Map<String, Set> getSets() {
Map<String, Set> sets = new HashMap<>();
registry.getAll().forEach((id, set) -> sets.put(id.toString(), set));
return sets;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should just return the registry object

Stephen Goodhill added 2 commits July 23, 2026 10:34
SetManager now keeps its ArmorSetRegistry on a shared instance rather
than in a static field, and getSets() returns the registry's own view
instead of copying it into a fresh HashMap. The static entry points
delegate to the shared instance so every caller and the characterization
tests are unchanged; getSets() exposes ArmorSetId keys, so the one
keySet() caller iterates those ids.
@nbdSteve nbdSteve changed the title refactor(model): move player-wearer and armor-set tracking into pure domain registries refactor(model): move wearer and set tracking into pure domain registries Jul 23, 2026
Stephen Goodhill added 3 commits July 23, 2026 12:06
Introduce Google Guice as the plugin's dependency-injection framework and
use it to replace the static SetManager/SetPlayerManager singletons with
ordinary injected instances.

- Add Guice 5.1.0 (JSR-330 javax.inject). Chosen over Dagger to avoid a
  second annotation processor alongside Lombok and for lower runtime
  ceremony on Spigot. Shade + relocate guice/guava/javax.inject/aopalliance
  under gg.steve.mc.ap.lib.* (mirrors the commons-lang3 relocation) and
  exclude annotation-only transitives from the shaded jar.
- Stand up a composition root in ArmorPlus.onEnable: build one Injector
  from ArmorPlusModule and resolve the shared collaborators once.
- Replace SetManager with ArmorSetCatalog and SetPlayerManager with
  PlayerArmorSetService - constructor-injected singletons holding the pure
  ArmorSetRegistry / PlayerArmorWearerRegistry. Drop the static maps and
  get() entry points; thread the instances to listeners, commands, the
  GUI, and the placeholder expansion.
- SetPlayer takes a resolved Set directly instead of resolving a name
  through the old static manager.

Behavior is preserved: the characterization suite is unchanged except for
the manager-coupled tests, which now construct the injected instances
instead of mocking static state (no behavioral assertions changed).

mvn clean verify green on JDK 25 (Java 8 bytecode); check-core coverage
gate still 100%.
@nbdSteve nbdSteve changed the title refactor(model): move wearer and set tracking into pure domain registries refactor: replace static manager singletons with Guice-injected registries Jul 23, 2026
Stephen Goodhill added 2 commits July 23, 2026 21:17
Purge bloated javadoc and comments that merely restate the code (one-line
method narration, class summaries derivable from the name). Keep only the few
genuinely non-obvious WHYs: why the model registries are bound rather than
annotated, why ArmorSetRegistry is generic, and the DI-wiring invariant the
Guice composition-root test turns on. No behavior change; characterization
suite untouched.
@sonarqubecloud

Copy link
Copy Markdown

@nbdSteve nbdSteve changed the title refactor: replace static manager singletons with Guice-injected registries refactor: replace static manager singletons with Guice-injected services Jul 23, 2026
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.

1 participant