Rearchitect use cases into a Bukkit-free flow layer with request/response types - #18
Merged
Conversation
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.
… eager converter work
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.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
Moves every use case in the plugin into a
flowlayer, so the decisions live in plain Java and Bukkit only appears at the edges.Before this, the equip/unequip listeners, the
/apcommand 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:
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.set.ArmorPieceEquipFlowflow.set.ArmorPieceUnequipFlowflow.set.ArmorSetCommandFlow/<set-name> ...routingflow.player.PlayerSetSessionFlowflow.combat.ArmorCombatRoutingFlowflow.command.ArmorPlusCommandFlow/apsubcommand resolutionflow.command.ArmorCommandAccessFlowflow.command.ArmorSetGiveFlowflow.purchase.ArmorPiecePurchaseFlowAdapters.
listener/holds the Bukkit event adapters and the newcommand/package holds the command adapters (ArmorPlusCommandas the/apTabExecutor, plusArmorGuiCommand,ArmorHelpCommand,ArmorReloadCommand,ArmorSetGiveCommand, andArmorCommandAccessGateas the shared permission adapter). This replaces the oldcmd/+cmd/sub/packages.converter/translates between Bukkit objects and the flow request/response types.ArmorSwitchListeneris deliberately left alone: it only republishesArmorEquipEvents for the equip/unequip listeners, so it holds no use case of its own.Damage and tracking logic moved into
service.set.ArmorPieceSlotResolverand the existing pure registries, so the adapters no longer own it.Enforcement
LayeringRulesTestnow rejects any dependency fromflow..onorg.bukkit..,net.minecraft..orde.tr7zw.., alongside the existingmodel..andservice..rules. The layering is mechanically checked, not just documented.check-corelist at 100% line and branch coverage. The build fails if that drops.mvn -B clean verifyis 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:
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 ownArmorSetCompletionRequest/Response) does the scan and the single registry write.ArmorEquipOrderingTestpins the dispatch order with MockitoInOrderso it cannot silently regress again.ArmorCommandAccessGatedefaulted to allowing an unrecognized sender or permission. It now denies.head-itemsconfig list and runningSet.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'sIGNOREDguard 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
ArmorPlusModulethrough@Provides @Singletonfactory methods rather than@Injecton the classes, specifically so theserviceandflowlayers import zero DI types.model.portinterfaces remain the declared boundary for a later change.