Skip to content

refactor(service): extract pure logic into a Bukkit-free service layer wired via Guice - #17

Merged
nbdSteve merged 10 commits into
masterfrom
fm/armorplus-4e-registries-r9
Jul 24, 2026
Merged

refactor(service): extract pure logic into a Bukkit-free service layer wired via Guice#17
nbdSteve merged 10 commits into
masterfrom
fm/armorplus-4e-registries-r9

Conversation

@nbdSteve

@nbdSteve nbdSteve commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Intent

PR #17 continues a behavior-preserving refactor of ArmorPlus (a Spigot/Paper plugin) toward a layered/hexagonal architecture, with a strict rule that the model layer is pure data and no behavior lives in it. This branch's earlier commits introduced pure registries (ArmorSetRegistry, PlayerArmorWearerRegistry), a pure damage calculator (BasicDamageCalculator, ArmorHandItem), Guice DI wiring (composition root in ArmorPlus.onEnable, shaded/relocated under gg.steve.mc.ap.lib.*), typed-string IDs, and a purged terse-comment convention (comments document only the non-obvious WHY).

The newest commit (5dd4ae2) fixes a layering defect the reviewer flagged: behavior-bearing classes were wrongly nested under model/. It MOVES those pure-logic classes out of model into a new concept-grouped gg.steve.mc.ap.service layer - service.set.ArmorSetRegistry, service.player.PlayerArmorWearerRegistry, service.combat.BasicDamageCalculator, service.combat.ArmorHandItem - leaving model as data-only (value objects, typed IDs, enums, port interfaces). All callers, the Guice bindings, and the JaCoCo core-gate class paths are updated to the new packages. A new ArchUnit test (architecture.LayeringRulesTest, via the archunit-junit5 test dependency) mechanically enforces that neither model.. nor service.. depends on org.bukkit.., net.minecraft.., or de.tr7zw.. - Bukkit stays only at the adapter edges (listeners, commands, GUI, papi, bootstrap).

This is deliberately behavior-preserving: the characterization test suite is byte-identical except for package/import lines (the moved classes and their tests changed package only). No product behavior changes. The full flow/command/converter/Request-Response use-case rearchitecture is intentionally OUT OF SCOPE for this PR and deferred to a later task - do not flag its absence. Constraints honored: JDK 25 build emitting Java 8 bytecode, core coverage gate still 100% line+branch, no Manager-suffixed names, fully-specific class names.

What Changed

  • Moved wearer/set tracking and damage-calculation logic out of Bukkit-facing managers into pure, injectable classes under a new service layer (service.set.ArmorSetRegistry, service.player.PlayerArmorWearerRegistry, service.combat.BasicDamageCalculator, service.combat.ArmorHandItem); renamed the former SetManager/SetPlayerManager singletons to ArmorSetCatalog/PlayerArmorSetService and dropped their static state so the adapters are thin delegators over the registries.
  • Introduced Guice DI: ArmorPlusModule binds the registries, catalog, and player service as singletons with the composition root in ArmorPlus.onEnable, threading collaborators into listeners/commands/GUI/PAPI expansion; Guice, guava, and javax.inject are shaded and relocated under gg.steve.mc.ap.lib.*.
  • Added an ArchUnit LayeringRulesTest that fails the build if any model.. or service.. class imports org.bukkit/net.minecraft/de.tr7zw, relocated the characterization and unit tests to match the new packages, and repointed the JaCoCo core-coverage gate at the new class paths (still 100% line+branch).

Risk Assessment

✅ Low: A well-bounded, fully-tested, behavior-preserving package move with all call sites, DI bindings, coverage gate, and an ArchUnit enforcement rule consistently updated; no material new bugs found.

Testing

Ran the full JDK-25 mvn clean verify (all 237 tests green, JaCoCo check-core gate met), then produced targeted evidence for the intent: rename-aware diffs proving the moved tests are byte-identical apart from package/import lines, a negative test that injected a real Bukkit dependency into a service class and confirmed the new ArchUnit LayeringRulesTest catches it (then reverted), and per-class JaCoCo numbers showing 100% line+branch on the four relocated classes. No UI/visual artifact applies - this is an internal package/layering refactor with no end-user-visible surface, so evidence is the architecture-enforcement proof and coverage data rather than screenshots. Working tree cleaned (build output removed) and verified clean.

Evidence: ArchUnit negative test: guard rejects an injected Bukkit dep in the service layer

LayeringRulesTest.service_stays_platform_free FAILED after injecting org.bukkit.Material into service.set.ArmorSetRegistry: Architecture Violation [Priority: MEDIUM] - Rule 'no classes that reside in a package '..service..' should depend on classes that reside in any package ['org.bukkit..', 'net.minecraft..', 'de.tr7zw..']' was violated (2 times): Method <...ArmorSetRegistry.leakPlatformType()> gets field <org.bukkit.Material.AIR> Method <...ArmorSetRegistry.leakPlatformType()> has return type <org.bukkit.Material> Tests run: 2, Failures: 1 -> BUILD FAILURE. (Injection reverted; working tree clean.)

NEGATIVE TEST: injected 'import org.bukkit.Material' + a Material-returning method into
service.set.ArmorSetRegistry, then ran LayeringRulesTest. Expected: the guard rejects it.

[INFO] Running gg.steve.mc.ap.architecture.LayeringRulesTest
[ERROR] Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 1.106 s <<< FAILURE! -- in gg.steve.mc.ap.architecture.LayeringRulesTest
[ERROR] gg.steve.mc.ap.architecture.LayeringRulesTest.service_stays_platform_free -- Time elapsed: 0.004 s <<< FAILURE!
Architecture Violation [Priority: MEDIUM] - Rule 'no classes that reside in a package '..service..' should depend on classes that reside in any package ['org.bukkit..', 'net.minecraft..', 'de.tr7zw..']' was violated (2 times):
Method <gg.steve.mc.ap.service.set.ArmorSetRegistry.leakPlatformType()> gets field <org.bukkit.Material.AIR> in (ArmorSetRegistry.java:37)
Method <gg.steve.mc.ap.service.set.ArmorSetRegistry.leakPlatformType()> has return type <org.bukkit.Material> in (ArmorSetRegistry.java:0)
[ERROR]   LayeringRulesTest.service_stays_platform_free Architecture Violation [Priority: MEDIUM] - Rule 'no classes that reside in a package '..service..' should depend on classes that reside in any package ['org.bukkit..', 'net.minecraft..', 'de.tr7zw..']' was violated (2 times):
Method <gg.steve.mc.ap.service.set.ArmorSetRegistry.leakPlatformType()> gets field <org.bukkit.Material.AIR> in (ArmorSetRegistry.java:37)
Method <gg.steve.mc.ap.service.set.ArmorSetRegistry.leakPlatformType()> has return type <org.bukkit.Material> in (ArmorSetRegistry.java:0)
[ERROR] Tests run: 2, Failures: 1, Errors: 0, Skipped: 0
[INFO] BUILD FAILURE
Evidence: JaCoCo core-gate: 100% line+branch on relocated service classes

PACKAGE,CLASS,INSTR_MISSED,INSTR_COVERED,BRANCH_MISSED,BRANCH_COVERED,LINE_MISSED,LINE_COVERED service.combat,BasicDamageCalculator,0,26,0,4,0,5 service.combat,ArmorHandItem,0,20,0,2,0,4 service.player,PlayerArmorWearerRegistry,0,50,0,0,0,13 service.set,ArmorSetRegistry,0,33,0,0,0,9 (all zero missed -> check-core: 'All coverage checks have been met')

JaCoCo core-gate: relocated service classes (from target/site/jacoco/jacoco.csv)
columns: PACKAGE,CLASS, missed/covered for INSTRUCTION, BRANCH, LINE ...
GROUP,PACKAGE,CLASS,INSTRUCTION_MISSED,INSTRUCTION_COVERED,BRANCH_MISSED,BRANCH_COVERED,LINE_MISSED,LINE_COVERED,COMPLEXITY_MISSED,COMPLEXITY_COVERED,METHOD_MISSED,METHOD_COVERED
ArmorPlus,gg.steve.mc.ap.service.combat,BasicDamageCalculator,0,26,0,4,0,5,0,4,0,2
ArmorPlus,gg.steve.mc.ap.service.combat,ArmorHandItem,0,20,0,2,0,4,0,2,0,1
ArmorPlus,gg.steve.mc.ap.service.player,PlayerArmorWearerRegistry,0,50,0,0,0,13,0,8,0,8
ArmorPlus,gg.steve.mc.ap.service.set,ArmorSetRegistry,0,33,0,0,0,9,0,6,0,6

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ℹ️ src/main/java/gg/steve/mc/ap/player/PlayerArmorSetService.java:32 - PlayerArmorSetService.init() returns after applying a set to the first matching online player, so on plugin enable/reload only one player's armor-set effects are re-applied even if many are wearing sets. This is a verbatim copy of the pre-existing SetPlayerManager.init() bug - the refactor is intentionally behavior-preserving, and AGENTS.md documents that surprising pinned behavior should be deferred to a separate bug-fix PR. Flagging only so a reviewer scanning the new file doesn't mistake the nested-loop return for a fresh defect.
✅ **Test** - passed

✅ No issues found.

  • JAVA_HOME=&lt;corretto-25&gt; mvn -B clean verify — full build + all 237 tests pass, including relocated characterization suites at service.combat/service.set/service.player
  • mvn -B test -Dtest=LayeringRulesTest after injecting import org.bukkit.Material + a Material-returning method into service.set.ArmorSetRegistry — confirmed the guard FAILS (service_stays_platform_free architecture violation), then reverted the injection (working tree clean)
  • git diff -M (rename-aware) on the four moved test files — verified they differ only by package line + one required model.player.ArmorSetWearer import (97-98% similarity)
  • Parsed target/site/jacoco/jacoco.csv — confirmed 0 missed instructions/branches/lines for service.combat.ArmorHandItem, service.combat.BasicDamageCalculator, service.player.PlayerArmorWearerRegistry, service.set.ArmorSetRegistry (JaCoCo check-core: 'All coverage checks have been met')
✅ **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.
@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
…ayer

model/ should hold data only (value objects, typed IDs, enums, port
interfaces). The registries and damage calculators were behavior-bearing,
so relocate them into a new concept-grouped gg.steve.mc.ap.service layer:

- service.set.ArmorSetRegistry            (from model.set)
- service.player.PlayerArmorWearerRegistry (from model.player)
- service.combat.BasicDamageCalculator    (from model.ability)
- service.combat.ArmorHandItem            (from model.ability)

Update all callers, the Guice bindings, and the JaCoCo core-gate paths.
Add architecture.LayeringRulesTest (ArchUnit) enforcing that neither
model.. nor service.. depends on org.bukkit.., net.minecraft.., or
de.tr7zw.., so the layering can't silently regress. Bukkit stays at the
adapter edges only.

Characterization tests are unchanged apart from package/import lines.
@sonarqubecloud

Copy link
Copy Markdown

@nbdSteve nbdSteve changed the title refactor: replace static manager singletons with Guice-injected services refactor(service): extract pure logic into a Bukkit-free service layer wired via Guice Jul 24, 2026
@nbdSteve
nbdSteve merged commit e1f6d8c into master Jul 24, 2026
2 checks passed
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