Skip to content

Rearchitect use cases into a Bukkit-free flow layer with request/response types - #18

Merged
nbdSteve merged 7 commits into
masterfrom
fm/armorplus-flows-w7
Jul 28, 2026
Merged

Rearchitect use cases into a Bukkit-free flow layer with request/response types#18
nbdSteve merged 7 commits into
masterfrom
fm/armorplus-flows-w7

Conversation

@nbdSteve

Copy link
Copy Markdown
Owner

Summary

Moves every use case in the plugin into a flow layer, so the decisions live in plain Java and Bukkit only appears at the edges.

Before this, the equip/unequip listeners, the /ap command tree and the GUI purchase action each mixed three concerns: reading state out of Bukkit and NBT, deciding what should happen, and applying the result back to the server. That made the decisions untestable without a running server and left the same rules duplicated across handlers.

Control flow is now strictly one direction:

Bukkit event or command -> adapter (listener/, command/)
  -> converter builds the flow's Request (reads Bukkit, NBT, config)
    -> flow decides, returns a Response of plain data
      -> adapter applies the Response back to Bukkit

Adapters carry no decisions and flows touch no platform.

What changed

Nine use-case flows, grouped by concept, each with its own request/response pair:

Flow Use case
flow.set.ArmorPieceEquipFlow equipping a piece: which slot's apply commands run, where a replaced head item goes
flow.set.ArmorPieceUnequipFlow unequipping a piece
flow.set.ArmorSetCommandFlow /<set-name> ... routing
flow.player.PlayerSetSessionFlow join/quit set tracking
flow.combat.ArmorCombatRoutingFlow worn set vs held item vs neither
flow.command.ArmorPlusCommandFlow /ap subcommand resolution
flow.command.ArmorCommandAccessFlow sender kind, then permission
flow.command.ArmorSetGiveFlow give validation
flow.purchase.ArmorPiecePurchaseFlow GUI purchase affordability

Adapters. listener/ holds the Bukkit event adapters and the new command/ package holds the command adapters (ArmorPlusCommand as the /ap TabExecutor, plus ArmorGuiCommand, ArmorHelpCommand, ArmorReloadCommand, ArmorSetGiveCommand, and ArmorCommandAccessGate as the shared permission adapter). This replaces the old cmd/ + cmd/sub/ packages. converter/ translates between Bukkit objects and the flow request/response types.

ArmorSwitchListener is deliberately left alone: it only republishes ArmorEquipEvents for the equip/unequip listeners, so it holds no use case of its own.

Damage and tracking logic moved into service.set.ArmorPieceSlotResolver and the existing pure registries, so the adapters no longer own it.

Enforcement

  • ArchUnit LayeringRulesTest now rejects any dependency from flow.. on org.bukkit.., net.minecraft.. or de.tr7zw.., alongside the existing model.. and service.. rules. The layering is mechanically checked, not just documented.
  • All nine flows and the two pure converters are on the JaCoCo check-core list at 100% line and branch coverage. The build fails if that drops.
  • 380 tests pass; mvn -B clean verify is green on JDK 25.

Behavior preservation

This is a refactor, not a behavior change. The characterization suite under src/test/java/gg/steve/mc/ap/{data,player,armor,message} pins the existing behavior and is unchanged apart from one constructor-arity line - no assertion was modified.

Two rounds of review on this branch caught real regressions in the first draft, each fixed here:

  • Command ordering. An intermediate version computed set-completeness before dispatching the per-slot commands.apply/commands.remove. That inverted what an ArmorPlus PAPI placeholder inside those commands observed, and stopped an apply command that changes worn armor from influencing the completeness verdict. The original ordering is restored: slot commands dispatch first, then a second flow call (completeSet, with its own ArmorSetCompletionRequest/Response) does the scan and the single registry write. ArmorEquipOrderingTest pins the dispatch order with Mockito InOrder so it cannot silently regress again.
  • Fail-open permission gate. ArmorCommandAccessGate defaulted to allowing an unrecognized sender or permission. It now denies.
  • Eager converter work. Converters were reading the head-items config list and running Set.isWearingSet's four-slot NBT scan before the flow's cheap guards could discard the request. They now short-circuit on the same facts the flow's IGNORED guard reads. Same fix applied to the purchase converter's unconditional permission read, the give converter resolving target and permission before the arity check, and the attacker converter resolving a damager on already-cancelled events.

Notes for the reviewer

  • Flows are bound in ArmorPlusModule through @Provides @Singleton factory methods rather than @Inject on the classes, specifically so the service and flow layers import zero DI types.
  • No outbound-port adapter is built yet; flows still hand platform work back to their calling adapter. The model.port interfaces remain the declared boundary for a later change.
  • Emitted bytecode stays Java 8 as before; only the test sources target 17.

Stephen Goodhill added 7 commits July 27, 2026 15:20
Each flow is one use case: equip and unequip a set piece, player session
join/quit, combat routing, give, purchase, and command dispatch. Flows
compose the pure service registries and return a response the adapters
apply, so no Bukkit type crosses into them.
Bukkit event and command adapters now build a request with a converter,
call the matching flow, and apply the response. The equip/unequip,
session, combat-routing, set-command, give, access-gate and purchase use
cases all run through their flows; the adapters keep only Bukkit calls.

Replace cmd/ApCmd and its static sub-commands with injected adapters in
command/, move the gui purchase path off the static GuiItemUtil.purchase
into an injected ArmorPiecePurchaseAction threaded through Set/SetGui,
and resolve every adapter from the injector in SetupManager. Flows are
@Provides-bound so the flow layer imports no DI types.
Unit-test every flow decision (equip head-item restore and cancel rules,
unequip, session tracking, combat routing, set-command resolution, root
command dispatch, access gate, give validation ordering, purchase) plus
the two pure converters, and add all eleven classes to the jacoco
check-core gate at 100% line and branch.

Extend the ArchUnit layering rules so flow.. may not depend on Bukkit,
NMS or the vendored NBT-API, matching model.. and service...

Drop the root-command-name check from ArmorPlusCommandFlow: only `ap`
(with its aliases) is registered, and the request now carries just the
arguments the platform already strips.
A reload rebuilds the set catalog but leaves the wearer registry populated,
so a combat event routed to WORN_SET can name a set the catalog no longer
holds. ArmorBuffListener dereferenced that lookup directly on four paths.
The head-items config read had the same shape: FileManager.get returns null
when the file is not loaded.

Also gives ItemStackUtil a private constructor.
@sonarqubecloud

Copy link
Copy Markdown

@nbdSteve
nbdSteve merged commit 05e13fc into master Jul 28, 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