diff --git a/docs/BANNERMOD_BACKLOG.json b/docs/BANNERMOD_BACKLOG.json index 1de2c31b..1daea512 100644 --- a/docs/BANNERMOD_BACKLOG.json +++ b/docs/BANNERMOD_BACKLOG.json @@ -8475,8 +8475,8 @@ { "id": "EVENTSPKG-005", "title": "Move remaining non-event services out of events/ and finalize package contract", - "status": "open", - "updated": "2026-05-05", + "status": "in_progress", + "updated": "2026-05-08", "why": "EVENTSPKG-001 final phase: any service still in events/ after the prior moves must be relocated. Once done, events/ should contain only classes whose methods carry @SubscribeEvent.", "scope": [ "Inventory events/ — for each class, decide: (a) it is a @SubscribeEvent host (stay) or (b) it is a service (move to the matching subsystem runtime/ folder).", @@ -8491,11 +8491,17 @@ "tools/backlog validate passes." ], "dependencies": [ - "EVENTSPKG-002", - "EVENTSPKG-003", - "EVENTSPKG-004" + "EVENTSPKG-005A", + "EVENTSPKG-005B", + "EVENTSPKG-005C", + "EVENTSPKG-005D" + ], + "progress": [ + { + "date": "2026-05-08", + "text": "parallel wave produced an 80+ file package-move diff in /home/user/bannermod-task-worktrees/EVENTSPKG-005; not merged because it is too broad for safe review as one task and the required runGameTestServer gate is currently blocked by unrelated baseline failures fiverecruitformationholdsacrossdimensionteleport, starterbootstrapseedsrealworkerassignmentsandwaitingreasons, and friendlyclaimbindingallowsplacementandsettlementoperation; moved remaining scope into area-based child tasks EVENTSPKG-005A/B/C plus final guard/gate EVENTSPKG-005D" + } ], - "progress": [], "verification": [], "evidence": [] }, @@ -8757,8 +8763,8 @@ { "id": "PACKETAUTH-012", "title": "Verify leader ownership in MessagePatrolLeaderSetEnemyAction", - "status": "open", - "updated": "2026-05-06", + "status": "done", + "updated": "2026-05-08", "why": "MessagePatrolLeaderSetEnemyAction resolves an AbstractLeaderEntity by UUID and only verifies a range AABB / distance before calling leader.setEnemyAction(...). No ownership check; any client in range can mutate a foreign player's leader entity (waypoints, cycle, speed, info mode, etc.).", "scope": [ "src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetEnemyAction.java executeServerSide; add canDirectlyControl(sender, leader) (or equivalent owner-equality check via leader.getOwnerUUID()) before the mutation." @@ -8768,14 +8774,20 @@ ], "dependencies": [], "progress": [], - "verification": [], - "evidence": [] + "verification": [ + { + "date": "2026-05-08", + "result": "1) Handler now requires RecruitCommandAuthority.canDirectlyControl(player, leader) before leader.setEnemyAction(this.action); 2) focused PatrolLeaderWaypointAuthorityTest passed and asserts a foreign non-op sender has CommandRole.NONE and the authority gate precedes the only enemy-action mutation; 3) ./gradlew compileJava test --tests PatrolLeaderWaypointAuthorityTest --tests MessagePatrolLeaderSetInfoModeAuthorityTest passed." + } + ], + "evidence": [], + "doneDate": "2026-05-08" }, { "id": "PACKETAUTH-013", "title": "Verify leader ownership in MessagePatrolLeaderSetInfoMode", - "status": "open", - "updated": "2026-05-06", + "status": "done", + "updated": "2026-05-08", "why": "MessagePatrolLeaderSetInfoMode resolves an AbstractLeaderEntity by UUID and only verifies a range AABB / distance before calling leader.setInfoMode(...). No ownership check; any client in range can mutate a foreign player's leader entity (waypoints, cycle, speed, info mode, etc.).", "scope": [ "src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetInfoMode.java executeServerSide; add canDirectlyControl(sender, leader) (or equivalent owner-equality check via leader.getOwnerUUID()) before the mutation." @@ -8785,14 +8797,20 @@ ], "dependencies": [], "progress": [], - "verification": [], - "evidence": [] + "verification": [ + { + "date": "2026-05-08", + "result": "1) Handler now requires RecruitCommandAuthority.canDirectlyControl(player, leader) before leader.setInfoMode(state); 2) focused MessagePatrolLeaderSetInfoModeAuthorityTest passed and asserts a foreign non-op sender has CommandRole.NONE and the authority gate precedes the only info-mode mutation; 3) ./gradlew compileJava test --tests PatrolLeaderWaypointAuthorityTest --tests MessagePatrolLeaderSetInfoModeAuthorityTest passed." + } + ], + "evidence": [], + "doneDate": "2026-05-08" }, { "id": "PACKETAUTH-014", "title": "Verify leader ownership in MessagePatrolLeaderSetPatrollingSpeed", - "status": "open", - "updated": "2026-05-06", + "status": "done", + "updated": "2026-05-08", "why": "MessagePatrolLeaderSetPatrollingSpeed resolves an AbstractLeaderEntity by UUID and only verifies a range AABB / distance before calling leader.setPatrolSpeed(...). No ownership check; any client in range can mutate a foreign player's leader entity (waypoints, cycle, speed, info mode, etc.).", "scope": [ "src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetPatrollingSpeed.java executeServerSide; add canDirectlyControl(sender, leader) (or equivalent owner-equality check via leader.getOwnerUUID()) before the mutation." @@ -8802,14 +8820,20 @@ ], "dependencies": [], "progress": [], - "verification": [], - "evidence": [] + "verification": [ + { + "date": "2026-05-08", + "result": "1) Handler now requires RecruitCommandAuthority.canDirectlyControl(player, leader) before leader.setPatrolSpeed(this.speed); 2) focused PatrolLeaderWaypointAuthorityTest passed and asserts a foreign non-op sender has CommandRole.NONE and the authority gate precedes the only patrol-speed mutation; 3) ./gradlew compileJava test --tests PatrolLeaderWaypointAuthorityTest --tests MessagePatrolLeaderSetInfoModeAuthorityTest passed." + } + ], + "evidence": [], + "doneDate": "2026-05-08" }, { "id": "PACKETAUTH-015", "title": "Verify leader ownership in MessagePatrolLeaderSetWaitTime", - "status": "open", - "updated": "2026-05-06", + "status": "done", + "updated": "2026-05-08", "why": "MessagePatrolLeaderSetWaitTime resolves an AbstractLeaderEntity by UUID and only verifies a range AABB / distance before calling leader.setWaitTimeInMin(...). No ownership check; any client in range can mutate a foreign player's leader entity (waypoints, cycle, speed, info mode, etc.).", "scope": [ "src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetWaitTime.java executeServerSide; add canDirectlyControl(sender, leader) (or equivalent owner-equality check via leader.getOwnerUUID()) before the mutation." @@ -8819,14 +8843,20 @@ ], "dependencies": [], "progress": [], - "verification": [], - "evidence": [] + "verification": [ + { + "date": "2026-05-08", + "result": "1) Handler now requires RecruitCommandAuthority.canDirectlyControl(player, leader) before leader.setWaitTimeInMin(this.time); 2) focused PatrolLeaderWaypointAuthorityTest passed and asserts a foreign non-op sender has CommandRole.NONE and the authority gate precedes the only wait-time mutation; 3) ./gradlew compileJava test with packet-authority focused tests passed." + } + ], + "evidence": [], + "doneDate": "2026-05-08" }, { "id": "PACKETAUTH-016", "title": "Verify leader ownership in MessageRemoveAssignedGroupFromCompanion", - "status": "open", - "updated": "2026-05-06", + "status": "done", + "updated": "2026-05-08", "why": "MessageRemoveAssignedGroupFromCompanion resolves an AbstractLeaderEntity by UUID and only checks a 100-block AABB before nulling its leader binding, resetting its army's listen/follow/hold-pos state, and broadcasting a leader-screen update. There is no sender-equality / canDirectlyControl gate; any client in range can detach a foreign player's companion's group.", "scope": [ "src/main/java/com/talhanation/bannermod/network/messages/military/MessageRemoveAssignedGroupFromCompanion.java executeServerSide; require canDirectlyControl(sender, leader) (or owner-equality via leader.getOwnerUUID()) before mutating leaderUUID, army state, or broadcasting screen updates." @@ -8836,14 +8866,20 @@ ], "dependencies": [], "progress": [], - "verification": [], - "evidence": [] + "verification": [ + { + "date": "2026-05-08", + "result": "1) Handler now requires RecruitCommandAuthority.canDirectlyControl(serverPlayer, companionEntity) before group leader binding, army reset, companion army detach, or screen broadcast mutations; 2) focused MessageRemoveAssignedGroupFromCompanionAuthorityTest passed and asserts a foreign non-op sender has CommandRole.NONE and the authority gate precedes those mutations; 3) ./gradlew compileJava test with packet-authority focused tests passed." + } + ], + "evidence": [], + "doneDate": "2026-05-08" }, { "id": "PACKETAUTH-017", "title": "Verify messenger ownership in MessageSendMessenger", - "status": "open", - "updated": "2026-05-06", + "status": "done", + "updated": "2026-05-08", "why": "MessageSendMessenger resolves a MessengerEntity by UUID with only a 16-block AABB check, then mutates messenger.setMessage / setTargetPlayerInfo and starts the delivery. There is no sender-equality check against messenger.getOwnerUUID(); any client in range can hijack a foreign messenger to deliver an arbitrary message and target.", "scope": [ "src/main/java/com/talhanation/bannermod/network/messages/military/MessageSendMessenger.java executeServerSide; require sender.getUUID().equals(messenger.getOwnerUUID()) (or sender.hasPermissions(2)) before mutating message/target/start." @@ -8853,14 +8889,20 @@ ], "dependencies": [], "progress": [], - "verification": [], - "evidence": [] + "verification": [ + { + "date": "2026-05-08", + "result": "1) Handler now returns unless sender owns the messenger or has op permission before message, target, treaty-state, or start mutations; 2) focused MessageSendMessengerAuthorityTest passed and asserts the owner/op gate precedes all messenger delivery mutations; 3) ./gradlew compileJava test with packet-authority focused tests passed." + } + ], + "evidence": [], + "doneDate": "2026-05-08" }, { "id": "PACKETAUTH-018", "title": "Verify recruit ownership in MessagePromoteRecruit", - "status": "open", - "updated": "2026-05-06", + "status": "done", + "updated": "2026-05-08", "why": "MessagePromoteRecruit resolves any recruit within 16 blocks by UUID and calls RecruitEvents.promoteRecruit, which discards the original entity and spawns a companion with setOwnerName(sender.getName()). There is no ownership / canDirectlyControl gate; a non-owner can effectively steal a foreign recruit by promoting it.", "scope": [ "src/main/java/com/talhanation/bannermod/network/messages/military/MessagePromoteRecruit.java executeServerSide and/or RecruitEvents.promoteRecruit; require canDirectlyControl(sender, recruit) before invoking promoteRecruit." @@ -8870,14 +8912,20 @@ ], "dependencies": [], "progress": [], - "verification": [], - "evidence": [] + "verification": [ + { + "date": "2026-05-08", + "result": "1) Handler now requires RecruitCommandAuthority.canDirectlyControl(sender, recruit) before RecruitEvents.promoteRecruit, so foreign nearby senders bail before promotion/discard/spawn; 2) focused MessagePromoteRecruitAuthorityTest passed and asserts the authority gate precedes the promotion call; 3) ./gradlew compileJava test with packet-authority focused tests passed." + } + ], + "evidence": [], + "doneDate": "2026-05-08" }, { "id": "PACKETAUTH-019", "title": "Reject foreign-group target in MessageHire", - "status": "open", - "updated": "2026-05-06", + "status": "done", + "updated": "2026-05-08", "why": "MessageHire accepts any groupUUID from the wire and feeds it directly into RecruitsGroupsManager.getGroup + RecruitLifecycleService.hire. The hired recruit is added to that group's members list and (via RecruitLifecycleService.updateGroup) is later reassigned to the foreign group's owner. A sender can therefore pay to inject recruits into another player's group and bypass MessageAssignGroupToPlayer's audited transfer path.", "scope": [ "src/main/java/com/talhanation/bannermod/network/messages/military/MessageHire.java executeServerSide; require RecruitCommandAuthority.ownedGroup(sender, groupUUID) (or null when not owned, falling back to no group) before passing the group into handleRecruiting." @@ -8887,14 +8935,20 @@ ], "dependencies": [], "progress": [], - "verification": [], - "evidence": [] + "verification": [ + { + "date": "2026-05-08", + "result": "1) Handler now resolves requested groupUUID through RecruitCommandAuthority.ownedGroup(player, groupUUID), so foreign-group UUIDs become null before hire handling; 2) focused MessageHireAuthorityTest passed and asserts the unsafe direct group lookup is absent and the ownership gate precedes CommandEvents.handleRecruiting; 3) ./gradlew compileJava test with packet-authority focused tests passed." + } + ], + "evidence": [], + "doneDate": "2026-05-08" }, { "id": "WORKERUNBIND-001", "title": "Auto-unbind worker when its work-area entity is removed", - "status": "open", - "updated": "2026-05-06", + "status": "done", + "updated": "2026-05-08", "why": "When a worker's bound AbstractWorkAreaEntity is killed/discarded, the worker keeps boundWorkAreaUUID set to the dead entity's id. AbstractWorkerEntity.getCurrentWorkArea() returns null (filters isRemoved/missing entity), but boundWorkAreaUUID never goes back to null and AI goals (e.g. FarmerWorkGoal SELECT_WORK_AREA) cannot reliably tell the area is gone. The symmetric ClaimRemovalFanout exists for full-claim deletion, but not for a single area entity disappearing while its parent claim survives.", "scope": [ "Add an EntityLeaveLevelEvent (or AbstractWorkAreaEntity.remove(...) override) handler that, when an AbstractWorkAreaEntity leaves the level via discard/kill/death, finds workers whose CitizenCore.boundWorkAreaUUID equals the removed entity's UUID and calls setCurrentWorkArea(null) on them within at most ~20 ticks", @@ -8907,8 +8961,14 @@ ], "dependencies": [], "progress": [], - "verification": [], - "evidence": [] + "verification": [ + { + "date": "2026-05-08", + "result": "1) WorkAreaIndexEvents now handles destructive AbstractWorkAreaEntity leave events and calls setCurrentWorkArea(null) for loaded workers whose boundWorkAreaUUID matches the removed area; 2) BannerModWorkerUnbindOnAreaRemovalGameTests#workerAutoUnbindsWithinTwentyTicksAfterCropAreaRemoval has @GameTest enabled and asserts boundWorkAreaUUID and getCurrentWorkArea() become null after crop-area discard; 3) ./gradlew compileJava passed; ./gradlew runGameTestServer completed 163 tests and the worker-unbind test was not among failures, but the overall task failed on unrelated fiverecruitformationholdsacrossdimensionteleport, starterbootstrapseedsrealworkerassignmentsandwaitingreasons, and friendlyclaimbindingallowsplacementandsettlementoperation." + } + ], + "evidence": [], + "doneDate": "2026-05-08" }, { "id": "FUZZHARDEN-001", @@ -8935,8 +8995,8 @@ { "id": "SETTREFACTOR-003A", "title": "Extract settlement treasury heartbeat derivation service", - "status": "open", - "updated": "2026-05-07", + "status": "done", + "updated": "2026-05-08", "why": "SETTREFACTOR-003 targeted treasury-hook methods in BannerModSettlementService, but current code no longer has treasury references there. The live treasury integration now sits in SettlementHeartbeatService's governor heartbeat path, so the extraction must target the actual runtime seam.", "scope": [ "Create SettlementTreasuryDerivationService under settlement/runtime/ for the live governor-heartbeat treasury integration currently embedded in SettlementHeartbeatService.", @@ -8952,13 +9012,19 @@ "SETTREFACTOR-002" ], "progress": [], - "verification": [], - "evidence": [] + "verification": [ + { + "date": "2026-05-08", + "result": "1) SettlementTreasuryDerivationService exists under settlement/runtime and owns BannerModTreasuryManager.get(level) for governor heartbeat batches plus heartbeat accounting derivation; 2) SettlementHeartbeatService no longer imports or directly calls BannerModTreasuryManager while the heartbeat batch counters/stage path are unchanged; 3) SettlementTreasuryDerivationServiceTest, BannerModGovernorHeartbeatTest, ./gradlew compileJava, and ./gradlew test passed." + } + ], + "evidence": [], + "doneDate": "2026-05-08" }, { "id": "PACKETAUTH-008A", "title": "Define server-authoritative AssassinLeader ownership", - "status": "open", + "status": "done", "updated": "2026-05-08", "why": "PACKETAUTH-008 needs an ownership check, but AssassinLeaderEntity currently has no persisted server-authoritative owner source. Adding a packet-side owner gate without defining how leaders become owned would either block legitimate count edits or let clients claim authority indirectly.", "scope": [ @@ -8973,13 +9039,19 @@ ], "dependencies": [], "progress": [], - "verification": [], - "evidence": [] + "verification": [ + { + "date": "2026-05-08", + "result": "1) AssassinLeaderEntity now has a server-side controlOwnerUUID assigned on the server GUI-open path before the menu can send count updates and persisted through ControlOwner NBT; 2) MessageAssassinCount only mutates count when leader.isControlledBy(player) passes and does not assign control from packet data; 3) AssassinLeaderControlAuthorityTest, ./gradlew compileJava, and ./gradlew test passed." + } + ], + "evidence": [], + "doneDate": "2026-05-08" }, { "id": "PACKETAUTH-008B", "title": "Gate MessageAssassinCount by AssassinLeader authority", - "status": "open", + "status": "done", "updated": "2026-05-08", "why": "After PACKETAUTH-008A defines trustworthy AssassinLeader authority, MessageAssassinCount can safely reject foreign sender mutations without breaking legitimate users.", "scope": [ @@ -8996,6 +9068,118 @@ "PACKETAUTH-008A" ], "progress": [], + "verification": [ + { + "date": "2026-05-08", + "result": "1) MessageAssassinCount now gates count mutation with leader.isControlledBy(player) || player.hasPermissions(2) while preserving the existing 16-block AABB check; 2) AssassinLeaderControlAuthorityTest passed and asserts foreign non-op rejection plus owner/op allow-path source gates before the only setCount mutation; 3) ./gradlew compileJava, compileGameTestJava, and ./gradlew test passed." + } + ], + "evidence": [], + "doneDate": "2026-05-08" + }, + { + "id": "EVENTSPKG-005A", + "title": "Move claim and settlement services out of events package", + "status": "done", + "updated": "2026-05-08", + "why": "EVENTSPKG-005 is too large to review safely as one 80+ file package move; claim and settlement service classes can be moved and verified independently.", + "scope": [ + "Move claim and settlement service classes currently under events/ or events/civilian/ into settlement/runtime/ or the nearest existing settlement runtime package: ClaimAccessQueries, ClaimInteractionTargetResolver, ClaimProtectionEventService, ClaimProtectionFeedback, ClaimProtectionPolicy, ClaimQueueTickService, and SettlementContainerHookPolicy.", + "Update all main, test, and gametest imports for those moved classes without changing runtime behavior.", + "Keep only actual NeoForge event subscriber hosts in the events package for this area." + ], + "acceptance": [ + "The listed claim/settlement service classes no longer live under src/main/java/com/talhanation/bannermod/events/**.", + "All imports are updated and no old package references remain for the moved classes.", + "./gradlew compileJava and focused affected unit tests pass; tools/backlog validate passes." + ], + "dependencies": [], + "progress": [], + "verification": [ + { + "date": "2026-05-08", + "result": "1) ClaimAccessQueries, ClaimInteractionTargetResolver, ClaimProtectionEventService, ClaimProtectionFeedback, ClaimProtectionPolicy, ClaimQueueTickService, and SettlementContainerHookPolicy moved from events/** to settlement/runtime; 2) ctx search for the old claim/settlement event-service FQNs returned 0 matches; 3) ./gradlew compileJava, compileGameTestJava, focused ClaimProtectionFeedbackTest and SettlementContainerHookPolicyTest, full ./gradlew test, and tools/backlog validate passed." + } + ], + "evidence": [], + "doneDate": "2026-05-08" + }, + { + "id": "EVENTSPKG-005B", + "title": "Move recruit combat and military runtime services out of events package", + "status": "done", + "updated": "2026-05-08", + "why": "EVENTSPKG-005 mixes several military and combat service moves; separating them keeps review and verification bounded.", + "scope": [ + "Move recruit, command, governor-workflow, debug, combat-policy, world-lifecycle, target-authority, type-conversion, and siege tuning service classes out of events/ and events/runtime/ into combat/runtime/, entity/military/runtime/, war/runtime/, or the nearest existing subsystem package.", + "Update all main, test, and gametest imports for those moved classes without changing runtime behavior.", + "Remove only empty/dead event stubs in this area when no references remain." + ], + "acceptance": [ + "Military/combat runtime service classes moved by this task no longer live under src/main/java/com/talhanation/bannermod/events/**.", + "Actual @SubscribeEvent host classes remain in events/ and still register normally.", + "./gradlew compileJava and focused affected unit tests pass; tools/backlog validate passes." + ], + "dependencies": [], + "progress": [], + "verification": [ + { + "date": "2026-05-08", + "result": "1) Recruit/combat/military runtime services moved out of events/** into army.command.runtime, combat.runtime, entity.military.runtime, governance.runtime, and war.runtime while event subscriber hosts stayed in events; 2) ctx search for old moved military event-service FQNs returned 0 matches; 3) ./gradlew compileJava, compileGameTestJava, focused RecruitGovernorWorkflowTest/SiegeExplosionTuningTest/RecruitTargetAuthorityTest, full ./gradlew test, and tools/backlog validate passed." + } + ], + "evidence": [], + "doneDate": "2026-05-08" + }, + { + "id": "EVENTSPKG-005C", + "title": "Move citizen worker and villager services out of events package", + "status": "done", + "updated": "2026-05-08", + "why": "Citizen, worker, and villager service moves are a separate reviewable slice of EVENTSPKG-005.", + "scope": [ + "Move citizen lifecycle, villager conversion, guard spawn/factory, worker animal/market/settlement/trade service classes out of events/ and events/runtime/ into citizen/runtime/, settlement/civilian/runtime/, or the nearest existing subsystem package.", + "Update all main, test, and gametest imports for those moved classes without changing runtime behavior.", + "Remove only empty/dead event stubs in this area when no references remain." + ], + "acceptance": [ + "Citizen, worker, and villager runtime service classes moved by this task no longer live under src/main/java/com/talhanation/bannermod/events/**.", + "Actual @SubscribeEvent host classes remain in events/ and still register normally.", + "./gradlew compileJava and focused affected unit tests pass; tools/backlog validate passes." + ], + "dependencies": [], + "progress": [], + "verification": [ + { + "date": "2026-05-08", + "result": "1) WorkerAnimalGoalInjector, WorkerMarketAreaAccess, WorkerSettlementClaimPolicy, WorkerSettlementEventService, WorkerSettlementSpawnRuntime, WorkerTradeBootstrap, CitizenWorldLifecycleService, VillagerConversionService, and VillagerProfessionTradeRegistrationService moved out of events/** into ai.civilian.animals, citizen.runtime, entity.military.runtime, and settlement.civilian.runtime; 2) ctx search for old citizen/worker/villager event-service FQNs returned 0 matches; 3) ./gradlew compileJava compileGameTestJava test and tools/backlog validate passed." + } + ], + "evidence": [], + "doneDate": "2026-05-08" + }, + { + "id": "EVENTSPKG-005D", + "title": "Add final events package contract guard and full gate", + "status": "open", + "updated": "2026-05-08", + "why": "EVENTSPKG-005 needs a final inventory and regression guard after the area moves land, plus the full verification gate required by the parent task.", + "scope": [ + "Inventory src/main/java/com/talhanation/bannermod/events/** after EVENTSPKG-005A/B/C and move any remaining non-event service or payload class that does not satisfy the final package contract.", + "Add a guard test under src/test that fails when a class is added under events/ without @SubscribeEvent event-host behavior, or document the same rule only if a code guard is impossible.", + "Run the full EVENTSPKG-005 verification gates and address any package-move regressions." + ], + "acceptance": [ + "events/ contains only classes satisfying the final event-package contract; no service classes remain there.", + "A guard test or explicit CONTRIBUTING.md contract prevents future non-event services from being added to events/.", + "./gradlew compileJava, ./gradlew test, ./gradlew runGameTestServer, and tools/backlog validate pass before EVENTSPKG-005 is closed." + ], + "dependencies": [ + "EVENTSPKG-005A", + "EVENTSPKG-005B", + "EVENTSPKG-005C" + ], + "progress": [], "verification": [], "evidence": [] } diff --git a/src/gametest/java/com/talhanation/bannermod/BannerModWorkerUnbindOnAreaRemovalGameTests.java b/src/gametest/java/com/talhanation/bannermod/BannerModWorkerUnbindOnAreaRemovalGameTests.java index b22b9479..86ab236f 100644 --- a/src/gametest/java/com/talhanation/bannermod/BannerModWorkerUnbindOnAreaRemovalGameTests.java +++ b/src/gametest/java/com/talhanation/bannermod/BannerModWorkerUnbindOnAreaRemovalGameTests.java @@ -14,21 +14,12 @@ import java.util.UUID; /** - * TESTWORKER-001 acceptance gametest. + * WORKERUNBIND-001 acceptance gametest. * *

Verifies that when a worker's bound work-area entity is removed from the level, * the worker auto-unbinds within ~20 ticks: {@code boundWorkAreaUUID} returns to * {@code null} and {@code getCurrentWorkArea()} also returns {@code null}, so no * AI goal can still observe the dead area through the worker's binding. - * - *

Status: the {@code @GameTest} annotation on - * {@link #workerAutoUnbindsWithinTwentyTicksAfterCropAreaRemoval(GameTestHelper)} is - * intentionally commented out. Production code does NOT currently fan out work-area - * entity removal to bound workers (no symmetric counterpart to - * {@link com.talhanation.bannermod.persistence.military.ClaimRemovalFanout} exists for a - * single work-area entity going away while its parent claim survives). The test below - * is the executable acceptance specification for the production fix tracked by - * {@code WORKERUNBIND-001}. Re-enable the {@code @GameTest} line once that fix lands. */ @GameTestHolder(BannerModMain.MOD_ID) public class BannerModWorkerUnbindOnAreaRemovalGameTests { @@ -36,12 +27,8 @@ public class BannerModWorkerUnbindOnAreaRemovalGameTests { private static final UUID OWNER_UUID = UUID.fromString("00000000-0000-0000-0000-000000005001"); private static final String OWNER_TEAM_ID = "workerunbind_owner"; - /** - * TODO(WORKERUNBIND-001): re-enable {@code @GameTest} once the work-area-removal - * fanout is implemented. The body below is the contract. - */ @PrefixGameTestTemplate(false) - // @GameTest(template = "harness_empty", timeoutTicks = 60) + @GameTest(template = "harness_empty", timeoutTicks = 60) public static void workerAutoUnbindsWithinTwentyTicksAfterCropAreaRemoval(GameTestHelper helper) { ServerLevel level = helper.getLevel(); Player owner = BannerModDedicatedServerGameTestSupport.createFakeServerPlayer(level, OWNER_UUID, "workerunbind-owner"); diff --git a/src/main/java/com/talhanation/bannermod/events/WorkerAnimalGoalInjector.java b/src/main/java/com/talhanation/bannermod/ai/civilian/animals/WorkerAnimalGoalInjector.java similarity index 89% rename from src/main/java/com/talhanation/bannermod/events/WorkerAnimalGoalInjector.java rename to src/main/java/com/talhanation/bannermod/ai/civilian/animals/WorkerAnimalGoalInjector.java index 407c0b86..6daa906c 100644 --- a/src/main/java/com/talhanation/bannermod/events/WorkerAnimalGoalInjector.java +++ b/src/main/java/com/talhanation/bannermod/ai/civilian/animals/WorkerAnimalGoalInjector.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events; +package com.talhanation.bannermod.ai.civilian.animals; import com.talhanation.bannermod.ai.civilian.animals.WorkerTemptGoal; import net.minecraft.world.entity.Entity; @@ -9,12 +9,12 @@ import net.minecraft.world.item.Items; import net.minecraft.world.item.crafting.Ingredient; -final class WorkerAnimalGoalInjector { +public final class WorkerAnimalGoalInjector { private WorkerAnimalGoalInjector() { } - static void injectTemptGoal(Entity entity) { + public static void injectTemptGoal(Entity entity) { if (entity instanceof Chicken chicken) { chicken.goalSelector.addGoal(3, new WorkerTemptGoal(chicken, 1.0, Ingredient.of(Items.WHEAT_SEEDS, Items.BEETROOT_SEEDS, Items.PUMPKIN_SEEDS, diff --git a/src/main/java/com/talhanation/bannermod/ai/military/villager/VillagerBecomeNobleGoal.java b/src/main/java/com/talhanation/bannermod/ai/military/villager/VillagerBecomeNobleGoal.java index 9c95925c..23aaab28 100644 --- a/src/main/java/com/talhanation/bannermod/ai/military/villager/VillagerBecomeNobleGoal.java +++ b/src/main/java/com/talhanation/bannermod/ai/military/villager/VillagerBecomeNobleGoal.java @@ -1,7 +1,7 @@ package com.talhanation.bannermod.ai.military.villager; import com.talhanation.bannermod.entity.military.VillagerNobleEntity; -import com.talhanation.bannermod.events.runtime.VillagerConversionService; +import com.talhanation.bannermod.entity.military.runtime.VillagerConversionService; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.ai.goal.Goal; import net.minecraft.world.entity.npc.Villager; diff --git a/src/main/java/com/talhanation/bannermod/events/RecruitCommandActionService.java b/src/main/java/com/talhanation/bannermod/army/command/runtime/RecruitCommandActionService.java similarity index 77% rename from src/main/java/com/talhanation/bannermod/events/RecruitCommandActionService.java rename to src/main/java/com/talhanation/bannermod/army/command/runtime/RecruitCommandActionService.java index 988e42eb..ec963ca6 100644 --- a/src/main/java/com/talhanation/bannermod/events/RecruitCommandActionService.java +++ b/src/main/java/com/talhanation/bannermod/army/command/runtime/RecruitCommandActionService.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events; +package com.talhanation.bannermod.army.command.runtime; import com.talhanation.bannermod.bootstrap.BannerModMain; import com.talhanation.bannermod.config.RecruitsServerConfig; @@ -26,12 +26,12 @@ import java.util.Optional; import java.util.UUID; -final class RecruitCommandActionService { +public final class RecruitCommandActionService { private RecruitCommandActionService() { } - static boolean handleRecruiting(Player player, RecruitsGroup group, AbstractRecruitEntity recruit, boolean message) { + public static boolean handleRecruiting(Player player, RecruitsGroup group, AbstractRecruitEntity recruit, boolean message) { String name = recruit.getName().getString() + ": "; int sollPrice = recruit.getCost(); boolean playerCanPay = BannerModCurrencyHelper.canAfford(player, sollPrice); @@ -50,7 +50,7 @@ static boolean handleRecruiting(Player player, RecruitsGroup group, AbstractRecr return false; } - static void onMountButton(UUID playerUuid, AbstractRecruitEntity recruit, UUID mountUuid, UUID group) { + public static void onMountButton(UUID playerUuid, AbstractRecruitEntity recruit, UUID mountUuid, UUID group) { if (recruit.isEffectedByCommand(playerUuid, group)) { if (mountUuid != null) { recruit.shouldMount(true, mountUuid); @@ -61,7 +61,7 @@ static void onMountButton(UUID playerUuid, AbstractRecruitEntity recruit, UUID m } } - static void onDismountButton(UUID playerUuid, AbstractRecruitEntity recruit, UUID group) { + public static void onDismountButton(UUID playerUuid, AbstractRecruitEntity recruit, UUID group) { if (recruit.isEffectedByCommand(playerUuid, group)) { recruit.shouldMount(false, null); if (recruit.isPassenger()) { @@ -71,13 +71,13 @@ static void onDismountButton(UUID playerUuid, AbstractRecruitEntity recruit, UUI } } - static void onProtectButton(UUID playerUuid, AbstractRecruitEntity recruit, UUID protectUuid, UUID group) { + public static void onProtectButton(UUID playerUuid, AbstractRecruitEntity recruit, UUID protectUuid, UUID group) { if (recruit.isEffectedByCommand(playerUuid, group)) { recruit.shouldProtect(true, protectUuid); } } - static void onClearTargetButton(UUID playerUuid, AbstractRecruitEntity recruit, UUID group) { + public static void onClearTargetButton(UUID playerUuid, AbstractRecruitEntity recruit, UUID group) { if (recruit.isEffectedByCommand(playerUuid, group)) { recruit.setTarget(null); recruit.setLastHurtByPlayer(null); @@ -86,14 +86,14 @@ static void onClearTargetButton(UUID playerUuid, AbstractRecruitEntity recruit, } } - static void onClearUpkeepButton(UUID playerUuid, AbstractRecruitEntity recruit, UUID group) { + public static void onClearUpkeepButton(UUID playerUuid, AbstractRecruitEntity recruit, UUID group) { if (recruit.isEffectedByCommand(playerUuid, group)) { recruit.clearUpkeepEntity(); recruit.clearUpkeepPos(); } } - static void onUpkeepCommand(UUID playerUuid, AbstractRecruitEntity recruit, UUID group, boolean isEntity, UUID entityUuid, BlockPos blockPos) { + public static void onUpkeepCommand(UUID playerUuid, AbstractRecruitEntity recruit, UUID group, boolean isEntity, UUID entityUuid, BlockPos blockPos) { if (recruit.isEffectedByCommand(playerUuid, group)) { if (isEntity) { recruit.setUpkeepUUID(Optional.of(entityUuid)); @@ -108,13 +108,13 @@ static void onUpkeepCommand(UUID playerUuid, AbstractRecruitEntity recruit, UUID } } - static void onShieldsCommand(Player player, UUID playerUuid, AbstractRecruitEntity recruit, UUID group, boolean shields) { + public static void onShieldsCommand(Player player, UUID playerUuid, AbstractRecruitEntity recruit, UUID group, boolean shields) { if (recruit.isEffectedByCommand(playerUuid, group)) { recruit.setShouldBlock(shields); } } - static void onRangedFireCommand(ServerPlayer serverPlayer, UUID playerUuid, AbstractRecruitEntity recruit, UUID group, boolean should) { + public static void onRangedFireCommand(ServerPlayer serverPlayer, UUID playerUuid, AbstractRecruitEntity recruit, UUID group, boolean should) { if (recruit.isEffectedByCommand(playerUuid, group)) { recruit.setShouldRanged(should); @@ -131,7 +131,7 @@ static void onRangedFireCommand(ServerPlayer serverPlayer, UUID playerUuid, Abst } } - static void onRestCommand(ServerPlayer serverPlayer, UUID playerUuid, AbstractRecruitEntity recruit, UUID group, boolean should) { + public static void onRestCommand(ServerPlayer serverPlayer, UUID playerUuid, AbstractRecruitEntity recruit, UUID group, boolean should) { if (recruit.isEffectedByCommand(playerUuid, group)) { onClearTargetButton(playerUuid, recruit, group); recruit.setShouldRest(should); diff --git a/src/main/java/com/talhanation/bannermod/citizen/runtime/CitizenBirthService.java b/src/main/java/com/talhanation/bannermod/citizen/runtime/CitizenBirthService.java index 9f49dca2..c8729b18 100644 --- a/src/main/java/com/talhanation/bannermod/citizen/runtime/CitizenBirthService.java +++ b/src/main/java/com/talhanation/bannermod/citizen/runtime/CitizenBirthService.java @@ -4,7 +4,7 @@ import com.talhanation.bannermod.entity.citizen.CitizenEntity; import com.talhanation.bannermod.entity.citizen.CitizenIndex; import com.talhanation.bannermod.events.ClaimEvents; -import com.talhanation.bannermod.events.WorkerSettlementClaimPolicy; +import com.talhanation.bannermod.settlement.civilian.runtime.WorkerSettlementClaimPolicy; import com.talhanation.bannermod.persistence.military.RecruitsClaim; import com.talhanation.bannermod.registry.citizen.ModCitizenEntityTypes; import com.talhanation.bannermod.settlement.civilian.CitizenBirthRules; diff --git a/src/main/java/com/talhanation/bannermod/events/runtime/CitizenWorldLifecycleService.java b/src/main/java/com/talhanation/bannermod/citizen/runtime/CitizenWorldLifecycleService.java similarity index 92% rename from src/main/java/com/talhanation/bannermod/events/runtime/CitizenWorldLifecycleService.java rename to src/main/java/com/talhanation/bannermod/citizen/runtime/CitizenWorldLifecycleService.java index 76a79cef..859d8e28 100644 --- a/src/main/java/com/talhanation/bannermod/events/runtime/CitizenWorldLifecycleService.java +++ b/src/main/java/com/talhanation/bannermod/citizen/runtime/CitizenWorldLifecycleService.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events.runtime; +package com.talhanation.bannermod.citizen.runtime; import com.talhanation.bannermod.ai.pathfinding.async.TrueAsyncPathfindingRuntime; import com.talhanation.bannermod.config.RecruitsServerConfig; diff --git a/src/main/java/com/talhanation/bannermod/client/military/gui/RecruitInventoryScreen.java b/src/main/java/com/talhanation/bannermod/client/military/gui/RecruitInventoryScreen.java index e589ca31..6d5aa0ee 100644 --- a/src/main/java/com/talhanation/bannermod/client/military/gui/RecruitInventoryScreen.java +++ b/src/main/java/com/talhanation/bannermod/client/military/gui/RecruitInventoryScreen.java @@ -355,23 +355,23 @@ else if(recruit instanceof IVillagerWorker worker && worker.hasOnlyScreen()){ // Convert-Type menu — only for plain base recruits (not companions, not noble villagers, // not villager-worker compat). Sends MessageConvertRecruitType; server discards old // entity and spawns the chosen type carrying owner/group/xp/level/inventory across. - if (com.talhanation.bannermod.events.runtime.RecruitTypeConverter.isConvertibleBaseType(this.recruit)) { + if (com.talhanation.bannermod.entity.military.runtime.RecruitTypeConverter.isConvertibleBaseType(this.recruit)) { ActionMenuButton convertMenu = new ActionMenuButton(zeroLeftPos, zeroTopPos + (20 + topPosGab) * 7, 80, 20, TEXT_MENU_CONVERT, java.util.List.of( new ContextMenuEntry(TEXT_CONVERT_SWORDSMAN.getString(), - () -> sendConvertRecruit(com.talhanation.bannermod.events.runtime.RecruitTypeConverter.Kind.SWORDSMAN), + () -> sendConvertRecruit(com.talhanation.bannermod.entity.military.runtime.RecruitTypeConverter.Kind.SWORDSMAN), !(recruit.getClass() == com.talhanation.bannermod.entity.military.RecruitEntity.class)), new ContextMenuEntry(TEXT_CONVERT_BOWMAN.getString(), - () -> sendConvertRecruit(com.talhanation.bannermod.events.runtime.RecruitTypeConverter.Kind.BOWMAN), + () -> sendConvertRecruit(com.talhanation.bannermod.entity.military.runtime.RecruitTypeConverter.Kind.BOWMAN), !(recruit.getClass() == com.talhanation.bannermod.entity.military.BowmanEntity.class)), new ContextMenuEntry(TEXT_CONVERT_PIKEMAN.getString(), - () -> sendConvertRecruit(com.talhanation.bannermod.events.runtime.RecruitTypeConverter.Kind.PIKEMAN), + () -> sendConvertRecruit(com.talhanation.bannermod.entity.military.runtime.RecruitTypeConverter.Kind.PIKEMAN), !(recruit.getClass() == com.talhanation.bannermod.entity.military.RecruitShieldmanEntity.class)), new ContextMenuEntry(TEXT_CONVERT_CROSSBOWMAN.getString(), - () -> sendConvertRecruit(com.talhanation.bannermod.events.runtime.RecruitTypeConverter.Kind.CROSSBOWMAN), + () -> sendConvertRecruit(com.talhanation.bannermod.entity.military.runtime.RecruitTypeConverter.Kind.CROSSBOWMAN), !(recruit.getClass() == com.talhanation.bannermod.entity.military.CrossBowmanEntity.class)), new ContextMenuEntry(TEXT_CONVERT_CAVALRY.getString(), - () -> sendConvertRecruit(com.talhanation.bannermod.events.runtime.RecruitTypeConverter.Kind.CAVALRY), + () -> sendConvertRecruit(com.talhanation.bannermod.entity.military.runtime.RecruitTypeConverter.Kind.CAVALRY), !(recruit.getClass() == com.talhanation.bannermod.entity.military.HorsemanEntity.class)) )); convertMenu.setTooltip(Tooltip.create(TOOLTIP_CONVERT)); @@ -380,7 +380,7 @@ else if(recruit instanceof IVillagerWorker worker && worker.hasOnlyScreen()){ } } - private void sendConvertRecruit(com.talhanation.bannermod.events.runtime.RecruitTypeConverter.Kind kind) { + private void sendConvertRecruit(com.talhanation.bannermod.entity.military.runtime.RecruitTypeConverter.Kind kind) { BannerModMain.SIMPLE_CHANNEL.sendToServer(new MessageConvertRecruitType(this.recruit.getUUID(), kind)); this.onClose(); } diff --git a/src/main/java/com/talhanation/bannermod/combat/RecruitMoraleService.java b/src/main/java/com/talhanation/bannermod/combat/RecruitMoraleService.java index 354d722e..8693e231 100644 --- a/src/main/java/com/talhanation/bannermod/combat/RecruitMoraleService.java +++ b/src/main/java/com/talhanation/bannermod/combat/RecruitMoraleService.java @@ -16,7 +16,7 @@ * a {@code routEndTick} (the game-tick at which the rout window closes). An entry is created * lazily on the first evaluation and left in place — recruits removed from the world have * their entries reaped by {@link #invalidate(UUID)}, called from the existing entity-leave - * hook in {@link com.talhanation.bannermod.events.runtime.RecruitCombatRuntime}.

+ * hook in {@link com.talhanation.bannermod.combat.runtime.RecruitCombatRuntime}.

* *

Transitions into {@link MoraleState#ROUTED} fire two side effects: *

    diff --git a/src/main/java/com/talhanation/bannermod/events/runtime/RecruitAttackPolicy.java b/src/main/java/com/talhanation/bannermod/combat/runtime/RecruitAttackPolicy.java similarity index 96% rename from src/main/java/com/talhanation/bannermod/events/runtime/RecruitAttackPolicy.java rename to src/main/java/com/talhanation/bannermod/combat/runtime/RecruitAttackPolicy.java index 8a713b21..0deb93da 100644 --- a/src/main/java/com/talhanation/bannermod/events/runtime/RecruitAttackPolicy.java +++ b/src/main/java/com/talhanation/bannermod/combat/runtime/RecruitAttackPolicy.java @@ -1,7 +1,8 @@ -package com.talhanation.bannermod.events.runtime; +package com.talhanation.bannermod.combat.runtime; import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; import com.talhanation.bannermod.entity.military.MessengerEntity; +import com.talhanation.bannermod.entity.military.runtime.RecruitEntityAccess; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.animal.Animal; import net.minecraft.world.entity.player.Player; diff --git a/src/main/java/com/talhanation/bannermod/events/runtime/RecruitCombatRuntime.java b/src/main/java/com/talhanation/bannermod/combat/runtime/RecruitCombatRuntime.java similarity index 98% rename from src/main/java/com/talhanation/bannermod/events/runtime/RecruitCombatRuntime.java rename to src/main/java/com/talhanation/bannermod/combat/runtime/RecruitCombatRuntime.java index 6b69efab..a8f647f5 100644 --- a/src/main/java/com/talhanation/bannermod/events/runtime/RecruitCombatRuntime.java +++ b/src/main/java/com/talhanation/bannermod/combat/runtime/RecruitCombatRuntime.java @@ -1,10 +1,11 @@ -package com.talhanation.bannermod.events.runtime; +package com.talhanation.bannermod.combat.runtime; import com.talhanation.bannermod.bootstrap.BannerModMain; import com.talhanation.bannermod.compat.IWeapon; import com.talhanation.bannermod.config.RecruitsServerConfig; import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; import com.talhanation.bannermod.entity.military.MessengerEntity; +import com.talhanation.bannermod.entity.military.runtime.RecruitEntityAccess; import net.minecraft.nbt.CompoundTag; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.entity.Entity; diff --git a/src/main/java/com/talhanation/bannermod/events/runtime/RecruitDiplomacyPolicy.java b/src/main/java/com/talhanation/bannermod/combat/runtime/RecruitDiplomacyPolicy.java similarity index 98% rename from src/main/java/com/talhanation/bannermod/events/runtime/RecruitDiplomacyPolicy.java rename to src/main/java/com/talhanation/bannermod/combat/runtime/RecruitDiplomacyPolicy.java index 3b31fac8..6b86ee54 100644 --- a/src/main/java/com/talhanation/bannermod/events/runtime/RecruitDiplomacyPolicy.java +++ b/src/main/java/com/talhanation/bannermod/combat/runtime/RecruitDiplomacyPolicy.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events.runtime; +package com.talhanation.bannermod.combat.runtime; import com.talhanation.bannermod.entity.military.RecruitPoliticalContext; import com.talhanation.bannermod.war.WarRuntimeContext; diff --git a/src/main/java/com/talhanation/bannermod/events/runtime/RecruitTargetAuthority.java b/src/main/java/com/talhanation/bannermod/combat/runtime/RecruitTargetAuthority.java similarity index 88% rename from src/main/java/com/talhanation/bannermod/events/runtime/RecruitTargetAuthority.java rename to src/main/java/com/talhanation/bannermod/combat/runtime/RecruitTargetAuthority.java index 78df710c..f45084b8 100644 --- a/src/main/java/com/talhanation/bannermod/events/runtime/RecruitTargetAuthority.java +++ b/src/main/java/com/talhanation/bannermod/combat/runtime/RecruitTargetAuthority.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events.runtime; +package com.talhanation.bannermod.combat.runtime; import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; import com.talhanation.bannermod.entity.military.RecruitPoliticalContext; @@ -18,7 +18,7 @@ import java.util.Collection; import java.util.UUID; -final class RecruitTargetAuthority { +public final class RecruitTargetAuthority { private RecruitTargetAuthority() { } @@ -41,10 +41,10 @@ static boolean canTargetUnderClaimAuthority(LivingEntity attacker, LivingEntity return claimAuthorityAllowsTarget(attackerPoliticalEntityId, claimOwnerPoliticalEntityId, occupiesClaim, atWarWithClaimOwner); } - static boolean claimAuthorityAllowsTarget(@Nullable UUID attackerPoliticalEntityId, - @Nullable UUID claimOwnerPoliticalEntityId, - boolean occupiesClaim, - boolean atWarWithClaimOwner) { + public static boolean claimAuthorityAllowsTarget(@Nullable UUID attackerPoliticalEntityId, + @Nullable UUID claimOwnerPoliticalEntityId, + boolean occupiesClaim, + boolean atWarWithClaimOwner) { if (claimOwnerPoliticalEntityId == null) { return true; } diff --git a/src/main/java/com/talhanation/bannermod/entity/civilian/workarea/WorkAreaIndexEvents.java b/src/main/java/com/talhanation/bannermod/entity/civilian/workarea/WorkAreaIndexEvents.java index 735c14dc..76843713 100644 --- a/src/main/java/com/talhanation/bannermod/entity/civilian/workarea/WorkAreaIndexEvents.java +++ b/src/main/java/com/talhanation/bannermod/entity/civilian/workarea/WorkAreaIndexEvents.java @@ -1,6 +1,9 @@ package com.talhanation.bannermod.entity.civilian.workarea; import com.talhanation.bannermod.bootstrap.BannerModMain; +import com.talhanation.bannermod.entity.civilian.AbstractWorkerEntity; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; import net.neoforged.neoforge.event.entity.EntityJoinLevelEvent; import net.neoforged.neoforge.event.entity.EntityLeaveLevelEvent; import net.neoforged.neoforge.event.level.LevelEvent; @@ -24,7 +27,18 @@ public static void onJoin(EntityJoinLevelEvent event) { @SubscribeEvent public static void onLeave(EntityLeaveLevelEvent event) { - WorkAreaIndex.instance().onEntityLeave(event.getEntity()); + Entity entity = event.getEntity(); + WorkAreaIndex.instance().onEntityLeave(entity); + if (!(entity instanceof AbstractWorkAreaEntity area)) return; + if (!(event.getLevel() instanceof ServerLevel serverLevel)) return; + Entity.RemovalReason reason = area.getRemovalReason(); + if (reason == null || !reason.shouldDestroy()) return; + for (Entity loadedEntity : serverLevel.getAllEntities()) { + if (loadedEntity instanceof AbstractWorkerEntity worker + && area.getUUID().equals(worker.getBoundWorkAreaUUID())) { + worker.setCurrentWorkArea(null); + } + } } @SubscribeEvent diff --git a/src/main/java/com/talhanation/bannermod/entity/military/AssassinLeaderEntity.java b/src/main/java/com/talhanation/bannermod/entity/military/AssassinLeaderEntity.java index 32771863..bf14e3a2 100644 --- a/src/main/java/com/talhanation/bannermod/entity/military/AssassinLeaderEntity.java +++ b/src/main/java/com/talhanation/bannermod/entity/military/AssassinLeaderEntity.java @@ -29,10 +29,15 @@ import com.talhanation.bannermod.network.compat.BannerModNetworkHooks; import javax.annotation.Nullable; +import java.util.UUID; import java.util.function.Predicate; public class AssassinLeaderEntity extends AbstractOrderAbleEntity { private static final EntityDataAccessor COUNT = SynchedEntityData.defineId(AssassinLeaderEntity.class, EntityDataSerializers.INT); + private static final String CONTROL_OWNER_TAG = "ControlOwner"; + + @Nullable + private UUID controlOwnerUUID; private final Predicate ALLOWED_ITEMS = (item) -> (!item.hasPickUpDelay() && item.isAlive() && getInventory().canAddItem(item.getItem()) && this.wantsToPickUp(item.getItem())); @@ -111,6 +116,7 @@ public void openGUI(Player player) { this.navigation.stop(); if (player instanceof ServerPlayer) { + assignControlOwnerIfAbsent(player); BannerModNetworkHooks.openScreen((ServerPlayer) player, new MenuProvider() { @Override public Component getDisplayName() { @@ -153,12 +159,16 @@ protected void defineSynchedData(net.minecraft.network.syncher.SynchedEntityData public void addAdditionalSaveData(CompoundTag nbt) { super.addAdditionalSaveData(nbt); nbt.putInt("Count", this.getCount()); + if (this.controlOwnerUUID != null) { + nbt.putUUID(CONTROL_OWNER_TAG, this.controlOwnerUUID); + } } @Override public void readAdditionalSaveData(CompoundTag nbt) { super.readAdditionalSaveData(nbt); this.setCount(nbt.getInt("Count")); + this.controlOwnerUUID = nbt.hasUUID(CONTROL_OWNER_TAG) ? nbt.getUUID(CONTROL_OWNER_TAG) : null; } public void setCount(int x){ @@ -169,6 +179,25 @@ public int getCount(){ return entityData.get(COUNT); } + /** + * Server-authoritative controller for count changes. The first server-side GUI + * open claims an unclaimed leader; client count packets may only check it. + */ + public void assignControlOwnerIfAbsent(Player player) { + if (!this.level().isClientSide && this.controlOwnerUUID == null) { + this.controlOwnerUUID = player.getUUID(); + } + } + + public boolean isControlledBy(Player player) { + return this.controlOwnerUUID != null && this.controlOwnerUUID.equals(player.getUUID()); + } + + @Nullable + public UUID getControlOwnerUUID() { + return controlOwnerUUID; + } + public int getAssassinCosts(){ return 12; } @@ -189,4 +218,3 @@ public int getMaxAssassinCount(){ - diff --git a/src/main/java/com/talhanation/bannermod/events/DebugEvents.java b/src/main/java/com/talhanation/bannermod/entity/military/runtime/DebugEvents.java similarity index 96% rename from src/main/java/com/talhanation/bannermod/events/DebugEvents.java rename to src/main/java/com/talhanation/bannermod/entity/military/runtime/DebugEvents.java index 68a87484..27442a65 100644 --- a/src/main/java/com/talhanation/bannermod/events/DebugEvents.java +++ b/src/main/java/com/talhanation/bannermod/entity/military/runtime/DebugEvents.java @@ -1,8 +1,9 @@ -package com.talhanation.bannermod.events; +package com.talhanation.bannermod.entity.military.runtime; import com.talhanation.bannermod.config.RecruitsServerConfig; import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; import com.talhanation.bannermod.entity.military.ICompanion; +import com.talhanation.bannermod.events.RecruitEvents; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; diff --git a/src/main/java/com/talhanation/bannermod/events/runtime/IronGolemRecruitReplacementFactory.java b/src/main/java/com/talhanation/bannermod/entity/military/runtime/IronGolemRecruitReplacementFactory.java similarity index 96% rename from src/main/java/com/talhanation/bannermod/events/runtime/IronGolemRecruitReplacementFactory.java rename to src/main/java/com/talhanation/bannermod/entity/military/runtime/IronGolemRecruitReplacementFactory.java index e64196ab..521a26bc 100644 --- a/src/main/java/com/talhanation/bannermod/events/runtime/IronGolemRecruitReplacementFactory.java +++ b/src/main/java/com/talhanation/bannermod/entity/military/runtime/IronGolemRecruitReplacementFactory.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events.runtime; +package com.talhanation.bannermod.entity.military.runtime; import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; import com.talhanation.bannermod.registry.military.ModEntityTypes; diff --git a/src/main/java/com/talhanation/bannermod/events/runtime/RecruitEntityAccess.java b/src/main/java/com/talhanation/bannermod/entity/military/runtime/RecruitEntityAccess.java similarity index 65% rename from src/main/java/com/talhanation/bannermod/events/runtime/RecruitEntityAccess.java rename to src/main/java/com/talhanation/bannermod/entity/military/runtime/RecruitEntityAccess.java index d7757a98..eb586125 100644 --- a/src/main/java/com/talhanation/bannermod/events/runtime/RecruitEntityAccess.java +++ b/src/main/java/com/talhanation/bannermod/entity/military/runtime/RecruitEntityAccess.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events.runtime; +package com.talhanation.bannermod.entity.military.runtime; import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; import net.minecraft.world.entity.Entity; @@ -6,12 +6,12 @@ import javax.annotation.Nullable; -final class RecruitEntityAccess { +public final class RecruitEntityAccess { private RecruitEntityAccess() { } @Nullable - static AbstractRecruitEntity asRecruit(@Nullable Entity entity) { + public static AbstractRecruitEntity asRecruit(@Nullable Entity entity) { if (!AbstractRecruitEntity.class.isInstance(entity)) { return null; } @@ -19,7 +19,7 @@ static AbstractRecruitEntity asRecruit(@Nullable Entity entity) { } @Nullable - static AbstractRecruitEntity asRecruit(@Nullable LivingEntity entity) { + public static AbstractRecruitEntity asRecruit(@Nullable LivingEntity entity) { return asRecruit((Entity) entity); } } diff --git a/src/main/java/com/talhanation/bannermod/events/runtime/RecruitTypeConverter.java b/src/main/java/com/talhanation/bannermod/entity/military/runtime/RecruitTypeConverter.java similarity index 99% rename from src/main/java/com/talhanation/bannermod/events/runtime/RecruitTypeConverter.java rename to src/main/java/com/talhanation/bannermod/entity/military/runtime/RecruitTypeConverter.java index 46d0346b..9d5a97be 100644 --- a/src/main/java/com/talhanation/bannermod/events/runtime/RecruitTypeConverter.java +++ b/src/main/java/com/talhanation/bannermod/entity/military/runtime/RecruitTypeConverter.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events.runtime; +package com.talhanation.bannermod.entity.military.runtime; import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; import com.talhanation.bannermod.entity.military.BowmanEntity; diff --git a/src/main/java/com/talhanation/bannermod/events/runtime/RecruitWorldLifecycleService.java b/src/main/java/com/talhanation/bannermod/entity/military/runtime/RecruitWorldLifecycleService.java similarity index 97% rename from src/main/java/com/talhanation/bannermod/events/runtime/RecruitWorldLifecycleService.java rename to src/main/java/com/talhanation/bannermod/entity/military/runtime/RecruitWorldLifecycleService.java index d1c3678e..cf67222a 100644 --- a/src/main/java/com/talhanation/bannermod/events/runtime/RecruitWorldLifecycleService.java +++ b/src/main/java/com/talhanation/bannermod/entity/military/runtime/RecruitWorldLifecycleService.java @@ -1,9 +1,10 @@ -package com.talhanation.bannermod.events.runtime; +package com.talhanation.bannermod.entity.military.runtime; import com.talhanation.bannermod.ai.military.horse.HorseRiddenByRecruitGoal; import com.talhanation.bannermod.config.RecruitsServerConfig; import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; import com.talhanation.bannermod.entity.military.RecruitIndex; +import com.talhanation.bannermod.citizen.runtime.CitizenWorldLifecycleService; import com.talhanation.bannermod.persistence.military.PillagerPatrolSpawn; import com.talhanation.bannermod.persistence.military.RecruitsGroup; import com.talhanation.bannermod.persistence.military.RecruitsGroupsManager; diff --git a/src/main/java/com/talhanation/bannermod/events/runtime/VillageGuardRecruitFactory.java b/src/main/java/com/talhanation/bannermod/entity/military/runtime/VillageGuardRecruitFactory.java similarity index 99% rename from src/main/java/com/talhanation/bannermod/events/runtime/VillageGuardRecruitFactory.java rename to src/main/java/com/talhanation/bannermod/entity/military/runtime/VillageGuardRecruitFactory.java index ffd6c21c..92f10565 100644 --- a/src/main/java/com/talhanation/bannermod/events/runtime/VillageGuardRecruitFactory.java +++ b/src/main/java/com/talhanation/bannermod/entity/military/runtime/VillageGuardRecruitFactory.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events.runtime; +package com.talhanation.bannermod.entity.military.runtime; import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; import com.talhanation.bannermod.entity.military.BowmanEntity; diff --git a/src/main/java/com/talhanation/bannermod/events/runtime/VillageGuardSpawnService.java b/src/main/java/com/talhanation/bannermod/entity/military/runtime/VillageGuardSpawnService.java similarity index 98% rename from src/main/java/com/talhanation/bannermod/events/runtime/VillageGuardSpawnService.java rename to src/main/java/com/talhanation/bannermod/entity/military/runtime/VillageGuardSpawnService.java index 18825b2c..6b6c2b6c 100644 --- a/src/main/java/com/talhanation/bannermod/events/runtime/VillageGuardSpawnService.java +++ b/src/main/java/com/talhanation/bannermod/entity/military/runtime/VillageGuardSpawnService.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events.runtime; +package com.talhanation.bannermod.entity.military.runtime; import com.talhanation.bannermod.config.RecruitsServerConfig; import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; diff --git a/src/main/java/com/talhanation/bannermod/events/runtime/VillageGuardSquadFactory.java b/src/main/java/com/talhanation/bannermod/entity/military/runtime/VillageGuardSquadFactory.java similarity index 98% rename from src/main/java/com/talhanation/bannermod/events/runtime/VillageGuardSquadFactory.java rename to src/main/java/com/talhanation/bannermod/entity/military/runtime/VillageGuardSquadFactory.java index b8e1264b..4cdd64b2 100644 --- a/src/main/java/com/talhanation/bannermod/events/runtime/VillageGuardSquadFactory.java +++ b/src/main/java/com/talhanation/bannermod/entity/military/runtime/VillageGuardSquadFactory.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events.runtime; +package com.talhanation.bannermod.entity.military.runtime; import com.talhanation.bannermod.entity.military.RecruitEntity; import net.minecraft.core.BlockPos; diff --git a/src/main/java/com/talhanation/bannermod/events/runtime/VillagerConversionService.java b/src/main/java/com/talhanation/bannermod/entity/military/runtime/VillagerConversionService.java similarity index 98% rename from src/main/java/com/talhanation/bannermod/events/runtime/VillagerConversionService.java rename to src/main/java/com/talhanation/bannermod/entity/military/runtime/VillagerConversionService.java index 2a752c10..ce61efcd 100644 --- a/src/main/java/com/talhanation/bannermod/events/runtime/VillagerConversionService.java +++ b/src/main/java/com/talhanation/bannermod/entity/military/runtime/VillagerConversionService.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events.runtime; +package com.talhanation.bannermod.entity.military.runtime; import com.talhanation.bannermod.config.RecruitsServerConfig; import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; diff --git a/src/main/java/com/talhanation/bannermod/events/runtime/VillagerProfessionTradeRegistrationService.java b/src/main/java/com/talhanation/bannermod/entity/military/runtime/VillagerProfessionTradeRegistrationService.java similarity index 98% rename from src/main/java/com/talhanation/bannermod/events/runtime/VillagerProfessionTradeRegistrationService.java rename to src/main/java/com/talhanation/bannermod/entity/military/runtime/VillagerProfessionTradeRegistrationService.java index 1c5a1c53..26534b3c 100644 --- a/src/main/java/com/talhanation/bannermod/events/runtime/VillagerProfessionTradeRegistrationService.java +++ b/src/main/java/com/talhanation/bannermod/entity/military/runtime/VillagerProfessionTradeRegistrationService.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events.runtime; +package com.talhanation.bannermod.entity.military.runtime; import com.talhanation.bannermod.registry.military.ModBlocks; import net.minecraft.util.RandomSource; diff --git a/src/main/java/com/talhanation/bannermod/events/ClaimEvents.java b/src/main/java/com/talhanation/bannermod/events/ClaimEvents.java index a7ee1908..c75a4e35 100644 --- a/src/main/java/com/talhanation/bannermod/events/ClaimEvents.java +++ b/src/main/java/com/talhanation/bannermod/events/ClaimEvents.java @@ -2,6 +2,8 @@ import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; import com.talhanation.bannermod.entity.military.RecruitIndex; +import com.talhanation.bannermod.settlement.runtime.ClaimProtectionEventService; +import com.talhanation.bannermod.settlement.runtime.ClaimQueueTickService; import com.talhanation.bannermod.settlement.runtime.ClaimRuntimeService; import com.talhanation.bannermod.settlement.runtime.SettlementHeartbeatService; import com.talhanation.bannermod.util.RuntimeProfilingCounters; diff --git a/src/main/java/com/talhanation/bannermod/events/CommandEvents.java b/src/main/java/com/talhanation/bannermod/events/CommandEvents.java index a87e056d..86fffb53 100644 --- a/src/main/java/com/talhanation/bannermod/events/CommandEvents.java +++ b/src/main/java/com/talhanation/bannermod/events/CommandEvents.java @@ -2,6 +2,7 @@ import com.talhanation.bannermod.bootstrap.BannerModMain; import com.talhanation.bannermod.ai.military.CombatStance; +import com.talhanation.bannermod.army.command.runtime.RecruitCommandActionService; import com.talhanation.bannermod.army.command.runtime.MovementFormationCommandService; import com.talhanation.bannermod.army.command.RecruitSelectionRegistry; import com.talhanation.bannermod.client.military.ClientManager; diff --git a/src/main/java/com/talhanation/bannermod/events/RecruitCombatEvents.java b/src/main/java/com/talhanation/bannermod/events/RecruitCombatEvents.java index 541c1ca4..6cdf7752 100644 --- a/src/main/java/com/talhanation/bannermod/events/RecruitCombatEvents.java +++ b/src/main/java/com/talhanation/bannermod/events/RecruitCombatEvents.java @@ -1,6 +1,6 @@ package com.talhanation.bannermod.events; -import com.talhanation.bannermod.events.runtime.RecruitCombatRuntime; +import com.talhanation.bannermod.combat.runtime.RecruitCombatRuntime; import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.neoforge.event.entity.EntityLeaveLevelEvent; import net.neoforged.neoforge.event.entity.ProjectileImpactEvent; diff --git a/src/main/java/com/talhanation/bannermod/events/RecruitEvents.java b/src/main/java/com/talhanation/bannermod/events/RecruitEvents.java index 0e471d5a..5315794b 100644 --- a/src/main/java/com/talhanation/bannermod/events/RecruitEvents.java +++ b/src/main/java/com/talhanation/bannermod/events/RecruitEvents.java @@ -2,6 +2,7 @@ import com.talhanation.bannermod.bootstrap.BannerModMain; import com.talhanation.bannermod.governance.BannerModGovernorPolicy; +import com.talhanation.bannermod.governance.runtime.RecruitGovernorWorkflow; import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; import com.talhanation.bannermod.entity.military.ICompanion; import com.talhanation.bannermod.registry.military.ModEntityTypes; @@ -9,8 +10,8 @@ import com.talhanation.bannermod.network.messages.military.MessageOpenPromoteScreen; import com.talhanation.bannermod.persistence.military.*; import com.talhanation.bannermod.events.RecruitEvent; -import com.talhanation.bannermod.events.runtime.RecruitCombatRuntime; -import com.talhanation.bannermod.events.runtime.RecruitWorldLifecycleService; +import com.talhanation.bannermod.combat.runtime.RecruitCombatRuntime; +import com.talhanation.bannermod.entity.military.runtime.RecruitWorldLifecycleService; import net.neoforged.neoforge.common.NeoForge; import net.minecraft.network.chat.Component; import net.minecraft.server.MinecraftServer; diff --git a/src/main/java/com/talhanation/bannermod/events/RecruitLifecycleEvents.java b/src/main/java/com/talhanation/bannermod/events/RecruitLifecycleEvents.java index f14f253c..54eecb7c 100644 --- a/src/main/java/com/talhanation/bannermod/events/RecruitLifecycleEvents.java +++ b/src/main/java/com/talhanation/bannermod/events/RecruitLifecycleEvents.java @@ -3,7 +3,8 @@ import com.talhanation.bannermod.ai.pathfinding.AsyncPathProcessor; import com.talhanation.bannermod.ai.pathfinding.async.TrueAsyncPathfindingRuntime; import com.talhanation.bannermod.entity.military.RecruitIndex; -import com.talhanation.bannermod.events.runtime.RecruitWorldLifecycleService; +import com.talhanation.bannermod.entity.military.runtime.RecruitWorldLifecycleService; +import com.talhanation.bannermod.governance.runtime.RecruitGovernorWorkflow; import com.talhanation.bannermod.util.FormationDimensionGuard; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; diff --git a/src/main/java/com/talhanation/bannermod/events/VillagerEvents.java b/src/main/java/com/talhanation/bannermod/events/VillagerEvents.java index 1ee367c9..957fb195 100644 --- a/src/main/java/com/talhanation/bannermod/events/VillagerEvents.java +++ b/src/main/java/com/talhanation/bannermod/events/VillagerEvents.java @@ -3,9 +3,9 @@ import com.talhanation.bannermod.config.RecruitsServerConfig; import com.talhanation.bannermod.entity.military.*; import com.talhanation.bannermod.ai.military.villager.VillagerBecomeNobleGoal; -import com.talhanation.bannermod.events.runtime.VillageGuardSpawnService; -import com.talhanation.bannermod.events.runtime.VillagerConversionService; -import com.talhanation.bannermod.events.runtime.VillagerProfessionTradeRegistrationService; +import com.talhanation.bannermod.entity.military.runtime.VillageGuardSpawnService; +import com.talhanation.bannermod.entity.military.runtime.VillagerConversionService; +import com.talhanation.bannermod.entity.military.runtime.VillagerProfessionTradeRegistrationService; import com.talhanation.bannermod.registry.military.ModEntityTypes; import com.talhanation.bannermod.registry.military.ModProfessions; import com.talhanation.bannermod.persistence.military.RecruitsHireTradesRegistry; diff --git a/src/main/java/com/talhanation/bannermod/events/WorkersVillagerEvents.java b/src/main/java/com/talhanation/bannermod/events/WorkersVillagerEvents.java index e423e709..f92aef3c 100644 --- a/src/main/java/com/talhanation/bannermod/events/WorkersVillagerEvents.java +++ b/src/main/java/com/talhanation/bannermod/events/WorkersVillagerEvents.java @@ -1,8 +1,12 @@ package com.talhanation.bannermod.events; import com.talhanation.bannermod.persistence.military.RecruitsClaim; +import com.talhanation.bannermod.ai.civilian.animals.WorkerAnimalGoalInjector; import com.talhanation.bannermod.entity.civilian.AbstractWorkerEntity; import com.talhanation.bannermod.settlement.civilian.WorkerSettlementSpawnRules; +import com.talhanation.bannermod.settlement.civilian.runtime.WorkerMarketAreaAccess; +import com.talhanation.bannermod.settlement.civilian.runtime.WorkerSettlementEventService; +import com.talhanation.bannermod.settlement.civilian.runtime.WorkerTradeBootstrap; import com.talhanation.bannermod.network.messages.civilian.MessageToClientUpdateConfig; import com.talhanation.bannermod.config.WorkersServerConfig; import com.talhanation.bannermod.bootstrap.WorkersRuntime; diff --git a/src/main/java/com/talhanation/bannermod/events/civilian/SettlementMutationRefreshEvents.java b/src/main/java/com/talhanation/bannermod/events/civilian/SettlementMutationRefreshEvents.java index 2c00cabe..dcf8b955 100644 --- a/src/main/java/com/talhanation/bannermod/events/civilian/SettlementMutationRefreshEvents.java +++ b/src/main/java/com/talhanation/bannermod/events/civilian/SettlementMutationRefreshEvents.java @@ -3,6 +3,7 @@ import com.talhanation.bannermod.entity.civilian.AbstractWorkerEntity; import com.talhanation.bannermod.entity.civilian.workarea.StorageArea; import com.talhanation.bannermod.entity.civilian.workarea.WorkAreaIndex; +import com.talhanation.bannermod.settlement.runtime.SettlementContainerHookPolicy; import com.talhanation.bannermod.shared.settlement.BannerModSettlementRefreshSupport; import net.minecraft.core.BlockPos; import net.minecraft.server.level.ServerLevel; diff --git a/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorHeartbeat.java b/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorHeartbeat.java index 430498be..b91b170f 100644 --- a/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorHeartbeat.java +++ b/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorHeartbeat.java @@ -8,6 +8,7 @@ import com.talhanation.bannermod.persistence.military.RecruitsClaimManager; import com.talhanation.bannermod.entity.civilian.AbstractWorkerEntity; import com.talhanation.bannermod.entity.civilian.WorkerIndex; +import com.talhanation.bannermod.settlement.runtime.SettlementTreasuryDerivationService; import com.talhanation.bannermod.util.RuntimeProfilingCounters; import com.talhanation.bannermod.war.runtime.WarSiegeQueries; import net.minecraft.world.Container; @@ -192,7 +193,7 @@ public static BatchResult runGovernedClaimHeartbeatBatch(ServerLevel level, snapshot )); - BannerModTreasuryLedgerSnapshot.FiscalRollup fiscalRollup = recordHeartbeatAccounting( + BannerModTreasuryLedgerSnapshot.FiscalRollup fiscalRollup = SettlementTreasuryDerivationService.deriveHeartbeatAccounting( treasuryManager, snapshot, binding, @@ -324,71 +325,6 @@ private static List recruitsInClaim(ServerLevel level, Re }); } - static void depositTaxes(@Nullable BannerModTreasuryManager treasuryManager, - BannerModGovernorSnapshot snapshot, - BannerModSettlementBinding.Binding binding, - HeartbeatReport report) { - if (treasuryManager == null || snapshot == null || binding == null || report == null || report.taxesCollected() <= 0) { - return; - } - treasuryManager.depositTaxes( - snapshot.claimUuid(), - snapshot.anchorChunk(), - binding.claimFactionId(), - report.taxesCollected(), - report.collectionTick() - ); - } - - static void recordArmyUpkeepDebit(@Nullable BannerModTreasuryManager treasuryManager, - BannerModGovernorSnapshot snapshot, - BannerModSettlementBinding.Binding binding, - HeartbeatReport report, - @Nullable BannerModSupplyStatus.RecruitSupplyStatus recruitSupplyStatus) { - if (treasuryManager == null || snapshot == null || binding == null || report == null || recruitSupplyStatus == null) { - return; - } - BannerModSupplyStatus.ArmyUpkeepStatus accounting = recruitSupplyStatus.accounting(); - if (accounting == null || !accounting.unpaid()) { - return; - } - treasuryManager.recordArmyUpkeepDebit( - snapshot.claimUuid(), - snapshot.anchorChunk(), - binding.claimFactionId(), - accounting.unpaidLevel(), - report.heartbeatTick() - ); - } - - @Nullable - static BannerModTreasuryLedgerSnapshot.FiscalRollup recordHeartbeatAccounting(@Nullable BannerModTreasuryManager treasuryManager, - BannerModGovernorSnapshot snapshot, - BannerModSettlementBinding.Binding binding, - HeartbeatReport report, - @Nullable BannerModSupplyStatus.RecruitSupplyStatus recruitSupplyStatus) { - if (treasuryManager == null || snapshot == null || binding == null || report == null) { - return null; - } - int requestedArmyUpkeepDebit = resolveRequestedArmyUpkeepDebit(recruitSupplyStatus); - BannerModTreasuryLedgerSnapshot updated = treasuryManager.applyHeartbeatAccounting( - snapshot.claimUuid(), - snapshot.anchorChunk(), - binding.claimFactionId(), - report.taxesCollected(), - requestedArmyUpkeepDebit, - report.heartbeatTick() - ); - return updated.projectFiscalRollup(report.taxesCollected(), requestedArmyUpkeepDebit, report.heartbeatTick()); - } - - private static int resolveRequestedArmyUpkeepDebit(@Nullable BannerModSupplyStatus.RecruitSupplyStatus recruitSupplyStatus) { - if (recruitSupplyStatus == null || recruitSupplyStatus.accounting() == null || !recruitSupplyStatus.accounting().unpaid()) { - return 0; - } - return recruitSupplyStatus.accounting().unpaidLevel(); - } - public record HeartbeatInput(BannerModSettlementBinding.Status settlementStatus, boolean underSiege, int villagerCount, diff --git a/src/main/java/com/talhanation/bannermod/events/RecruitGovernorWorkflow.java b/src/main/java/com/talhanation/bannermod/governance/runtime/RecruitGovernorWorkflow.java similarity index 90% rename from src/main/java/com/talhanation/bannermod/events/RecruitGovernorWorkflow.java rename to src/main/java/com/talhanation/bannermod/governance/runtime/RecruitGovernorWorkflow.java index 3cc2fefe..b4ac3d2c 100644 --- a/src/main/java/com/talhanation/bannermod/events/RecruitGovernorWorkflow.java +++ b/src/main/java/com/talhanation/bannermod/governance/runtime/RecruitGovernorWorkflow.java @@ -1,7 +1,8 @@ -package com.talhanation.bannermod.events; +package com.talhanation.bannermod.governance.runtime; import com.talhanation.bannermod.bootstrap.BannerModMain; import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; +import com.talhanation.bannermod.events.ClaimEvents; import com.talhanation.bannermod.governance.BannerModGovernorAuthority; import com.talhanation.bannermod.governance.BannerModGovernorManager; import com.talhanation.bannermod.governance.BannerModGovernorPolicy; @@ -29,12 +30,12 @@ import com.talhanation.bannermod.network.compat.BannerModPacketDistributor; import org.jetbrains.annotations.NotNull; -final class RecruitGovernorWorkflow { +public final class RecruitGovernorWorkflow { private RecruitGovernorWorkflow() { } - static boolean tryPromoteRecruit(AbstractRecruitEntity recruit, String name, ServerPlayer player) { + public static boolean tryPromoteRecruit(AbstractRecruitEntity recruit, String name, ServerPlayer player) { if (!(recruit.getCommandSenderWorld() instanceof ServerLevel serverLevel)) { return true; } @@ -67,7 +68,7 @@ static boolean tryPromoteRecruit(AbstractRecruitEntity recruit, String name, Ser return true; } - static void openGovernorScreen(Player player, AbstractRecruitEntity recruit) { + public static void openGovernorScreen(Player player, AbstractRecruitEntity recruit) { if (player instanceof ServerPlayer serverPlayer) { BannerModNetworkHooks.openScreen(serverPlayer, new MenuProvider() { @Override @@ -86,7 +87,7 @@ public AbstractContainerMenu createMenu(int i, @NotNull Inventory playerInventor } } - static void syncGovernorScreen(ServerPlayer player, AbstractRecruitEntity recruit) { + public static void syncGovernorScreen(ServerPlayer player, AbstractRecruitEntity recruit) { RecruitsClaim claim = resolveClaim(recruit); long gameTime = recruit.getCommandSenderWorld().getGameTime(); Envelope envelope = Envelope.empty(0L, gameTime, RefreshTrigger.SCREEN_OPEN); @@ -100,7 +101,7 @@ static void syncGovernorScreen(ServerPlayer player, AbstractRecruitEntity recrui new MessageToClientUpdateGovernorScreen(recruit.getUUID(), envelope)); } - static void syncGovernorSnapshotsOnLogin(ServerPlayer player) { + public static void syncGovernorSnapshotsOnLogin(ServerPlayer player) { ServerLevel level = player.serverLevel(); BannerModGovernorManager governorManager = BannerModGovernorManager.get(level); BannerModSettlementManager settlementManager = BannerModSettlementManager.get(level); @@ -116,7 +117,7 @@ static void syncGovernorSnapshotsOnLogin(ServerPlayer player) { } } - static void syncGovernorMutationRefresh(ServerLevel level, RecruitsClaim claim) { + public static void syncGovernorMutationRefresh(ServerLevel level, RecruitsClaim claim) { BannerModGovernorSnapshot governorSnapshot = BannerModGovernorManager.get(level).getSnapshot(claim.getUUID()); if (governorSnapshot == null || governorSnapshot.governorRecruitUuid() == null || governorSnapshot.governorOwnerUuid() == null) { return; @@ -130,7 +131,7 @@ static void syncGovernorMutationRefresh(ServerLevel level, RecruitsClaim claim) sendGovernorUpdate(player, governorSnapshot.governorRecruitUuid(), envelope); } - static Envelope buildEnvelope(RecruitsClaim claim, + public static Envelope buildEnvelope(RecruitsClaim claim, BannerModSettlementSnapshot settlementSnapshot, BannerModGovernorSnapshot governorSnapshot, long gameTime, @@ -138,7 +139,7 @@ static Envelope buildEnvelope(RecruitsClaim claim, return buildEnvelope(claim.getUUID(), settlementSnapshot, governorSnapshot, gameTime, trigger); } - static Envelope buildEnvelope(java.util.UUID claimUuid, + public static Envelope buildEnvelope(java.util.UUID claimUuid, BannerModSettlementSnapshot settlementSnapshot, BannerModGovernorSnapshot governorSnapshot, long gameTime, @@ -152,7 +153,7 @@ private static void sendGovernorUpdate(ServerPlayer player, java.util.UUID recru new MessageToClientUpdateGovernorScreen(recruitUuid, envelope)); } - static void updateGovernorPolicy(ServerPlayer player, AbstractRecruitEntity recruit, BannerModGovernorPolicy policy, int value) { + public static void updateGovernorPolicy(ServerPlayer player, AbstractRecruitEntity recruit, BannerModGovernorPolicy policy, int value) { if (!(recruit.getCommandSenderWorld() instanceof ServerLevel serverLevel)) { return; } diff --git a/src/main/java/com/talhanation/bannermod/network/messages/military/MessageAssassinCount.java b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageAssassinCount.java index 3d9cdfd8..31522d4a 100644 --- a/src/main/java/com/talhanation/bannermod/network/messages/military/MessageAssassinCount.java +++ b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageAssassinCount.java @@ -33,6 +33,7 @@ public void executeServerSide(BannerModNetworkContext context){ ServerPlayer player = Objects.requireNonNull(context.getSender()); Entity entity = player.serverLevel().getEntity(this.uuid); if (entity instanceof AssassinLeaderEntity leader + && (leader.isControlledBy(player) || player.hasPermissions(2)) && player.getBoundingBox().inflate(16.0D).intersects(leader.getBoundingBox())) { leader.setCount(this.count); } diff --git a/src/main/java/com/talhanation/bannermod/network/messages/military/MessageConvertRecruitType.java b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageConvertRecruitType.java index 0dce6bf1..1c0b864a 100644 --- a/src/main/java/com/talhanation/bannermod/network/messages/military/MessageConvertRecruitType.java +++ b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageConvertRecruitType.java @@ -1,7 +1,7 @@ package com.talhanation.bannermod.network.messages.military; import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; -import com.talhanation.bannermod.events.runtime.RecruitTypeConverter; +import com.talhanation.bannermod.entity.military.runtime.RecruitTypeConverter; import com.talhanation.bannermod.network.compat.BannerModNetworkContext; import com.talhanation.bannermod.network.payload.BannerModMessage; import net.minecraft.network.FriendlyByteBuf; diff --git a/src/main/java/com/talhanation/bannermod/network/messages/military/MessageDebugGui.java b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageDebugGui.java index c4f82ffd..9e6647d2 100644 --- a/src/main/java/com/talhanation/bannermod/network/messages/military/MessageDebugGui.java +++ b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageDebugGui.java @@ -3,7 +3,7 @@ import com.talhanation.bannermod.army.command.CommandHierarchy; import com.talhanation.bannermod.army.command.CommandRole; import com.talhanation.bannermod.army.command.RecruitCommandAuthority; -import com.talhanation.bannermod.events.DebugEvents; +import com.talhanation.bannermod.entity.military.runtime.DebugEvents; import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; import com.talhanation.bannermod.network.payload.BannerModMessage; import net.minecraft.network.protocol.PacketFlow; diff --git a/src/main/java/com/talhanation/bannermod/network/messages/military/MessageHire.java b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageHire.java index f742ee45..6e4b9d53 100644 --- a/src/main/java/com/talhanation/bannermod/network/messages/military/MessageHire.java +++ b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageHire.java @@ -1,7 +1,7 @@ package com.talhanation.bannermod.network.messages.military; +import com.talhanation.bannermod.army.command.RecruitCommandAuthority; import com.talhanation.bannermod.events.CommandEvents; -import com.talhanation.bannermod.events.RecruitEvents; import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; import com.talhanation.bannermod.persistence.military.RecruitsGroup; import com.talhanation.bannermod.network.payload.BannerModMessage; @@ -35,7 +35,7 @@ public PacketFlow getExecutingSide() { public void executeServerSide(BannerModNetworkContext context) { context.enqueueWork(() -> { ServerPlayer player = Objects.requireNonNull(context.getSender()); - RecruitsGroup group = RecruitEvents.groupsManager().getGroup(groupUUID); + RecruitsGroup group = RecruitCommandAuthority.ownedGroup(player, groupUUID); AbstractRecruitEntity recruit = RecruitMessageEntityResolver.resolveRecruitWithinDistance(player, this.recruit, 16.0D * 16.0D); if (recruit != null) { CommandEvents.handleRecruiting(player, group, recruit, true); diff --git a/src/main/java/com/talhanation/bannermod/network/messages/military/MessageHireFromNobleVillager.java b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageHireFromNobleVillager.java index c9a84f2b..ae7ccad2 100644 --- a/src/main/java/com/talhanation/bannermod/network/messages/military/MessageHireFromNobleVillager.java +++ b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageHireFromNobleVillager.java @@ -4,7 +4,7 @@ import com.talhanation.bannermod.events.RecruitEvents; import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; import com.talhanation.bannermod.entity.military.VillagerNobleEntity; -import com.talhanation.bannermod.events.runtime.VillagerConversionService; +import com.talhanation.bannermod.entity.military.runtime.VillagerConversionService; import com.talhanation.bannermod.persistence.military.RecruitsGroup; import com.talhanation.bannermod.persistence.military.RecruitsHireTrade; import com.talhanation.bannermod.network.payload.BannerModMessage; diff --git a/src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetEnemyAction.java b/src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetEnemyAction.java index 312c0661..6ffd37d8 100644 --- a/src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetEnemyAction.java +++ b/src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetEnemyAction.java @@ -1,5 +1,6 @@ package com.talhanation.bannermod.network.messages.military; +import com.talhanation.bannermod.army.command.RecruitCommandAuthority; import com.talhanation.bannermod.entity.military.AbstractLeaderEntity; import com.talhanation.bannermod.network.payload.BannerModMessage; import net.minecraft.network.protocol.PacketFlow; @@ -31,7 +32,10 @@ public void executeServerSide(BannerModNetworkContext context) { context.enqueueWork(() -> { ServerPlayer player = Objects.requireNonNull(context.getSender()); Entity entity = player.serverLevel().getEntity(this.recruit); - if (entity instanceof AbstractLeaderEntity leader && leader.isAlive() && leader.distanceToSqr(player) <= 100.0D * 100.0D) { + if (entity instanceof AbstractLeaderEntity leader + && leader.isAlive() + && RecruitCommandAuthority.canDirectlyControl(player, leader) + && leader.distanceToSqr(player) <= 100.0D * 100.0D) { leader.setEnemyAction(this.action); } }); diff --git a/src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetInfoMode.java b/src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetInfoMode.java index 328406ef..045b12e5 100644 --- a/src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetInfoMode.java +++ b/src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetInfoMode.java @@ -1,5 +1,6 @@ package com.talhanation.bannermod.network.messages.military; +import com.talhanation.bannermod.army.command.RecruitCommandAuthority; import com.talhanation.bannermod.entity.military.AbstractLeaderEntity; import com.talhanation.bannermod.network.payload.BannerModMessage; import net.minecraft.network.protocol.PacketFlow; @@ -33,6 +34,7 @@ public void executeServerSide(BannerModNetworkContext context) { Entity entity = player.serverLevel().getEntity(this.recruit); if (entity instanceof AbstractLeaderEntity leader && leader.isAlive() + && RecruitCommandAuthority.canDirectlyControl(player, leader) && player.getBoundingBox().inflate(16.0D).intersects(leader.getBoundingBox())) { leader.setInfoMode(state); } diff --git a/src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetPatrollingSpeed.java b/src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetPatrollingSpeed.java index 666d4581..25b6fb79 100644 --- a/src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetPatrollingSpeed.java +++ b/src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetPatrollingSpeed.java @@ -1,5 +1,6 @@ package com.talhanation.bannermod.network.messages.military; +import com.talhanation.bannermod.army.command.RecruitCommandAuthority; import com.talhanation.bannermod.entity.military.AbstractLeaderEntity; import com.talhanation.bannermod.network.payload.BannerModMessage; import net.minecraft.network.protocol.PacketFlow; @@ -31,7 +32,9 @@ public void executeServerSide(BannerModNetworkContext context) { context.enqueueWork(() -> { ServerPlayer player = Objects.requireNonNull(context.getSender()); Entity entity = player.serverLevel().getEntity(this.recruit); - if (entity instanceof AbstractLeaderEntity leader && leader.distanceToSqr(player) <= 100.0D * 100.0D) { + if (entity instanceof AbstractLeaderEntity leader + && RecruitCommandAuthority.canDirectlyControl(player, leader) + && leader.distanceToSqr(player) <= 100.0D * 100.0D) { leader.setPatrolSpeed(this.speed); } }); diff --git a/src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetWaitTime.java b/src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetWaitTime.java index 80be69ff..9fbdb948 100644 --- a/src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetWaitTime.java +++ b/src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetWaitTime.java @@ -1,5 +1,6 @@ package com.talhanation.bannermod.network.messages.military; +import com.talhanation.bannermod.army.command.RecruitCommandAuthority; import com.talhanation.bannermod.entity.military.AbstractLeaderEntity; import com.talhanation.bannermod.network.payload.BannerModMessage; import net.minecraft.network.protocol.PacketFlow; @@ -33,6 +34,7 @@ public void executeServerSide(BannerModNetworkContext context) { ServerPlayer player = Objects.requireNonNull(context.getSender()); Entity entity = player.serverLevel().getEntity(this.recruit); if (entity instanceof AbstractLeaderEntity leader + && RecruitCommandAuthority.canDirectlyControl(player, leader) && player.getBoundingBox().inflate(100.0D).intersects(leader.getBoundingBox())) { leader.setWaitTimeInMin(this.time); } diff --git a/src/main/java/com/talhanation/bannermod/network/messages/military/MessagePromoteRecruit.java b/src/main/java/com/talhanation/bannermod/network/messages/military/MessagePromoteRecruit.java index d287adb2..62f55f07 100644 --- a/src/main/java/com/talhanation/bannermod/network/messages/military/MessagePromoteRecruit.java +++ b/src/main/java/com/talhanation/bannermod/network/messages/military/MessagePromoteRecruit.java @@ -1,5 +1,6 @@ package com.talhanation.bannermod.network.messages.military; +import com.talhanation.bannermod.army.command.RecruitCommandAuthority; import com.talhanation.bannermod.events.RecruitEvents; import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; import com.talhanation.bannermod.network.payload.BannerModMessage; @@ -35,7 +36,7 @@ public void executeServerSide(BannerModNetworkContext context){ return; } AbstractRecruitEntity recruit = RecruitMessageEntityResolver.resolveRecruitWithinDistance(sender, this.recruit, 16D * 16D); - if (recruit != null) { + if (RecruitCommandAuthority.canDirectlyControl(sender, recruit)) { RecruitEvents.promoteRecruit(recruit, profession, name, sender); } }); diff --git a/src/main/java/com/talhanation/bannermod/network/messages/military/MessageRemoveAssignedGroupFromCompanion.java b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageRemoveAssignedGroupFromCompanion.java index 24074ba3..fee3b848 100644 --- a/src/main/java/com/talhanation/bannermod/network/messages/military/MessageRemoveAssignedGroupFromCompanion.java +++ b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageRemoveAssignedGroupFromCompanion.java @@ -1,6 +1,7 @@ package com.talhanation.bannermod.network.messages.military; import com.talhanation.bannermod.bootstrap.BannerModMain; +import com.talhanation.bannermod.army.command.RecruitCommandAuthority; import com.talhanation.bannermod.events.RecruitEvents; import com.talhanation.bannermod.entity.military.AbstractLeaderEntity; import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; @@ -39,7 +40,8 @@ public void executeServerSide(BannerModNetworkContext context) { ServerPlayer serverPlayer = context.getSender(); Entity entity = serverPlayer.serverLevel().getEntity(this.companion); if (entity instanceof AbstractLeaderEntity companionEntity - && serverPlayer.getBoundingBox().inflate(100D).intersects(companionEntity.getBoundingBox())) { + && serverPlayer.getBoundingBox().inflate(100D).intersects(companionEntity.getBoundingBox()) + && RecruitCommandAuthority.canDirectlyControl(serverPlayer, companionEntity)) { RecruitsGroup group = RecruitEvents.groupsManager().getGroup(companionEntity.getGroup()); if(group == null) return; diff --git a/src/main/java/com/talhanation/bannermod/network/messages/military/MessageSendMessenger.java b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageSendMessenger.java index 5376cae5..793fdadf 100644 --- a/src/main/java/com/talhanation/bannermod/network/messages/military/MessageSendMessenger.java +++ b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageSendMessenger.java @@ -47,6 +47,9 @@ public void executeServerSide(BannerModNetworkContext context) { Entity entity = player.serverLevel().getEntity(this.recruit); if (entity instanceof MessengerEntity messenger && player.getBoundingBox().inflate(16D).intersects(messenger.getBoundingBox())) { + if (!player.getUUID().equals(messenger.getOwnerUUID()) && !player.hasPermissions(2)) { + return; + } messenger.setMessage(this.message); diff --git a/src/main/java/com/talhanation/bannermod/events/WorkerMarketAreaAccess.java b/src/main/java/com/talhanation/bannermod/settlement/civilian/runtime/WorkerMarketAreaAccess.java similarity index 89% rename from src/main/java/com/talhanation/bannermod/events/WorkerMarketAreaAccess.java rename to src/main/java/com/talhanation/bannermod/settlement/civilian/runtime/WorkerMarketAreaAccess.java index 966f9c33..881a581f 100644 --- a/src/main/java/com/talhanation/bannermod/events/WorkerMarketAreaAccess.java +++ b/src/main/java/com/talhanation/bannermod/settlement/civilian/runtime/WorkerMarketAreaAccess.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events; +package com.talhanation.bannermod.settlement.civilian.runtime; import com.talhanation.bannermod.entity.civilian.workarea.MarketArea; import com.talhanation.bannermod.entity.civilian.workarea.WorkAreaIndex; @@ -11,12 +11,12 @@ import java.util.List; import java.util.UUID; -final class WorkerMarketAreaAccess { +public final class WorkerMarketAreaAccess { private WorkerMarketAreaAccess() { } - static boolean shouldBlockInteraction(Player player, Level level, BlockPos pos) { + public static boolean shouldBlockInteraction(Player player, Level level, BlockPos pos) { AABB queryBox = new AABB(pos).inflate(8); double queryRadius = Math.sqrt(queryBox.getXsize() * queryBox.getXsize() + queryBox.getYsize() * queryBox.getYsize() diff --git a/src/main/java/com/talhanation/bannermod/events/WorkerSettlementClaimPolicy.java b/src/main/java/com/talhanation/bannermod/settlement/civilian/runtime/WorkerSettlementClaimPolicy.java similarity index 99% rename from src/main/java/com/talhanation/bannermod/events/WorkerSettlementClaimPolicy.java rename to src/main/java/com/talhanation/bannermod/settlement/civilian/runtime/WorkerSettlementClaimPolicy.java index 3b437934..dc06a388 100644 --- a/src/main/java/com/talhanation/bannermod/events/WorkerSettlementClaimPolicy.java +++ b/src/main/java/com/talhanation/bannermod/settlement/civilian/runtime/WorkerSettlementClaimPolicy.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events; +package com.talhanation.bannermod.settlement.civilian.runtime; import com.talhanation.bannermod.citizen.runtime.CitizenBirthService; import com.talhanation.bannermod.entity.civilian.AbstractWorkerEntity; @@ -13,6 +13,7 @@ import com.talhanation.bannermod.entity.civilian.workarea.StorageArea; import com.talhanation.bannermod.entity.civilian.workarea.WorkAreaIndex; import com.talhanation.bannermod.entity.military.RecruitPoliticalContext; +import com.talhanation.bannermod.events.ClaimEvents; import com.talhanation.bannermod.persistence.military.RecruitsClaim; import com.talhanation.bannermod.settlement.BannerModSettlementBuildingCategory; import com.talhanation.bannermod.settlement.BannerModSettlementBuildingRecord; diff --git a/src/main/java/com/talhanation/bannermod/events/WorkerSettlementEventService.java b/src/main/java/com/talhanation/bannermod/settlement/civilian/runtime/WorkerSettlementEventService.java similarity index 85% rename from src/main/java/com/talhanation/bannermod/events/WorkerSettlementEventService.java rename to src/main/java/com/talhanation/bannermod/settlement/civilian/runtime/WorkerSettlementEventService.java index aca50d97..96a91673 100644 --- a/src/main/java/com/talhanation/bannermod/events/WorkerSettlementEventService.java +++ b/src/main/java/com/talhanation/bannermod/settlement/civilian/runtime/WorkerSettlementEventService.java @@ -1,9 +1,10 @@ -package com.talhanation.bannermod.events; +package com.talhanation.bannermod.settlement.civilian.runtime; import com.talhanation.bannermod.citizen.runtime.CitizenBirthService; import com.talhanation.bannermod.citizen.CitizenProfession; import com.talhanation.bannermod.config.WorkersServerConfig; import com.talhanation.bannermod.entity.civilian.AbstractWorkerEntity; +import com.talhanation.bannermod.events.ClaimEvents; import com.talhanation.bannermod.persistence.military.RecruitsClaim; import com.talhanation.bannermod.settlement.civilian.WorkerSettlementSpawnRules; import com.talhanation.bannermod.war.runtime.WarSiegeQueries; @@ -20,39 +21,39 @@ import java.util.Map; import java.util.UUID; -final class WorkerSettlementEventService { +public final class WorkerSettlementEventService { private static final Map CLAIM_WORKER_GROWTH_SPAWN_TIMES = new HashMap<>(); private WorkerSettlementEventService() { } - static void resetRuntimeState() { + public static void resetRuntimeState() { CLAIM_WORKER_GROWTH_SPAWN_TIMES.clear(); WorkerSettlementSpawnRuntime.reset(); CitizenBirthService.resetRuntimeState(); } - static void runCitizenBirthPass(ServerLevel level) { + public static void runCitizenBirthPass(ServerLevel level) { CitizenBirthService.runCitizenBirthPass(level); } - static void recordVillagerJoin(Villager villager) { + public static void recordVillagerJoin(Villager villager) { WorkerSettlementSpawnRuntime.recordVillagerJoin(villager); } - static void handleVillagerAdultTick(ServerLevel level, Villager villager) { + public static void handleVillagerAdultTick(ServerLevel level, Villager villager) { WorkerSettlementSpawnRuntime.handleVillagerAdultTick(level, villager); } - static AbstractWorkerEntity attemptBirthWorkerSpawn(ServerLevel level, Villager villager) { + public static AbstractWorkerEntity attemptBirthWorkerSpawn(ServerLevel level, Villager villager) { return WorkerSettlementSpawnRuntime.attemptBirthWorkerSpawn(level, villager); } - static AbstractWorkerEntity attemptSettlementWorkerSpawn(ServerLevel level, Villager villager) { + public static AbstractWorkerEntity attemptSettlementWorkerSpawn(ServerLevel level, Villager villager) { return WorkerSettlementSpawnRuntime.attemptSettlementWorkerSpawn(level, villager); } - static void runClaimWorkerGrowthPass(ServerLevel level) { + public static void runClaimWorkerGrowthPass(ServerLevel level) { if (level == null || ClaimEvents.claimManager() == null) { return; } @@ -111,7 +112,7 @@ private static boolean isPoliticalMember(PoliticalEntityRecord entity, UUID play return entity.coLeaderUuids().contains(playerUuid); } - static AbstractWorkerEntity attemptClaimWorkerGrowth(ServerLevel level, + public static AbstractWorkerEntity attemptClaimWorkerGrowth(ServerLevel level, RecruitsClaim claim, BannerModSettlementBinding.Binding binding, long gameTime, @@ -119,14 +120,14 @@ static AbstractWorkerEntity attemptClaimWorkerGrowth(ServerLevel level, return WorkerSettlementClaimPolicy.attemptClaimWorkerGrowth(level, claim, binding, gameTime, config, CLAIM_WORKER_GROWTH_SPAWN_TIMES); } - static AbstractWorkerEntity attemptClaimWorkerGrowth(ServerLevel level, + public static AbstractWorkerEntity attemptClaimWorkerGrowth(ServerLevel level, RecruitsClaim claim, String settlementFactionId, long gameTime) { return attemptClaimWorkerGrowth(level, claim, settlementFactionId, gameTime, WorkersServerConfig.claimWorkerGrowthConfig()); } - static AbstractWorkerEntity attemptClaimWorkerGrowth(ServerLevel level, + public static AbstractWorkerEntity attemptClaimWorkerGrowth(ServerLevel level, RecruitsClaim claim, String settlementFactionId, long gameTime, diff --git a/src/main/java/com/talhanation/bannermod/events/WorkerSettlementSpawnRuntime.java b/src/main/java/com/talhanation/bannermod/settlement/civilian/runtime/WorkerSettlementSpawnRuntime.java similarity index 98% rename from src/main/java/com/talhanation/bannermod/events/WorkerSettlementSpawnRuntime.java rename to src/main/java/com/talhanation/bannermod/settlement/civilian/runtime/WorkerSettlementSpawnRuntime.java index a271371e..5c5636ba 100644 --- a/src/main/java/com/talhanation/bannermod/events/WorkerSettlementSpawnRuntime.java +++ b/src/main/java/com/talhanation/bannermod/settlement/civilian/runtime/WorkerSettlementSpawnRuntime.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events; +package com.talhanation.bannermod.settlement.civilian.runtime; import com.talhanation.bannermod.config.WorkersServerConfig; import com.talhanation.bannermod.entity.civilian.AbstractWorkerEntity; diff --git a/src/main/java/com/talhanation/bannermod/events/WorkerTradeBootstrap.java b/src/main/java/com/talhanation/bannermod/settlement/civilian/runtime/WorkerTradeBootstrap.java similarity index 96% rename from src/main/java/com/talhanation/bannermod/events/WorkerTradeBootstrap.java rename to src/main/java/com/talhanation/bannermod/settlement/civilian/runtime/WorkerTradeBootstrap.java index f2dc552b..a9ea7bb1 100644 --- a/src/main/java/com/talhanation/bannermod/events/WorkerTradeBootstrap.java +++ b/src/main/java/com/talhanation/bannermod/settlement/civilian/runtime/WorkerTradeBootstrap.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events; +package com.talhanation.bannermod.settlement.civilian.runtime; import com.talhanation.bannermod.config.WorkersServerConfig; import com.talhanation.bannermod.persistence.military.RecruitsHireTrade; @@ -6,7 +6,7 @@ import com.talhanation.bannermod.registry.civilian.ModEntityTypes; import net.minecraft.network.chat.Component; -final class WorkerTradeBootstrap { +public final class WorkerTradeBootstrap { private static final Component TITLE_FARMER = Component.translatable("description.bannermod.workers.title.farmer"); private static final Component TITLE_MINER = Component.translatable("description.bannermod.workers.title.miner"); private static final Component TITLE_LUMBERJACK = Component.translatable("description.bannermod.workers.title.lumberjack"); @@ -25,7 +25,7 @@ final class WorkerTradeBootstrap { private WorkerTradeBootstrap() { } - static void registerTrades() { + public static void registerTrades() { RecruitsHireTrade farmer = new RecruitsHireTrade(ModEntityTypes.FARMER.getId(), WorkersServerConfig.FarmerCost.get(), TITLE_FARMER, DESCRIPTION_FARMER); RecruitsHireTrade lumberjack = new RecruitsHireTrade(ModEntityTypes.LUMBERJACK.getId(), WorkersServerConfig.LumberjackCost.get(), TITLE_LUMBERJACK, DESCRIPTION_LUMBERJACK); RecruitsHireTrade miner = new RecruitsHireTrade(ModEntityTypes.MINER.getId(), WorkersServerConfig.MinerCost.get(), TITLE_MINER, DESCRIPTION_MINER); diff --git a/src/main/java/com/talhanation/bannermod/events/ClaimAccessQueries.java b/src/main/java/com/talhanation/bannermod/settlement/runtime/ClaimAccessQueries.java similarity index 98% rename from src/main/java/com/talhanation/bannermod/events/ClaimAccessQueries.java rename to src/main/java/com/talhanation/bannermod/settlement/runtime/ClaimAccessQueries.java index dffc4c6d..b788de08 100644 --- a/src/main/java/com/talhanation/bannermod/events/ClaimAccessQueries.java +++ b/src/main/java/com/talhanation/bannermod/settlement/runtime/ClaimAccessQueries.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events; +package com.talhanation.bannermod.settlement.runtime; import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; import com.talhanation.bannermod.entity.military.RecruitPoliticalContext; diff --git a/src/main/java/com/talhanation/bannermod/events/ClaimInteractionTargetResolver.java b/src/main/java/com/talhanation/bannermod/settlement/runtime/ClaimInteractionTargetResolver.java similarity index 94% rename from src/main/java/com/talhanation/bannermod/events/ClaimInteractionTargetResolver.java rename to src/main/java/com/talhanation/bannermod/settlement/runtime/ClaimInteractionTargetResolver.java index 0d1583af..8c0074fd 100644 --- a/src/main/java/com/talhanation/bannermod/events/ClaimInteractionTargetResolver.java +++ b/src/main/java/com/talhanation/bannermod/settlement/runtime/ClaimInteractionTargetResolver.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events; +package com.talhanation.bannermod.settlement.runtime; import net.minecraft.core.BlockPos; import net.minecraft.world.InteractionHand; diff --git a/src/main/java/com/talhanation/bannermod/events/ClaimProtectionEventService.java b/src/main/java/com/talhanation/bannermod/settlement/runtime/ClaimProtectionEventService.java similarity index 88% rename from src/main/java/com/talhanation/bannermod/events/ClaimProtectionEventService.java rename to src/main/java/com/talhanation/bannermod/settlement/runtime/ClaimProtectionEventService.java index 71abaee4..77c3202c 100644 --- a/src/main/java/com/talhanation/bannermod/events/ClaimProtectionEventService.java +++ b/src/main/java/com/talhanation/bannermod/settlement/runtime/ClaimProtectionEventService.java @@ -1,9 +1,11 @@ -package com.talhanation.bannermod.events; +package com.talhanation.bannermod.settlement.runtime; import com.talhanation.bannermod.config.RecruitsServerConfig; +import com.talhanation.bannermod.events.ClaimEvents; import com.talhanation.bannermod.persistence.military.RecruitsClaim; import com.talhanation.bannermod.settlement.validation.BuildingInvalidationReason; import com.talhanation.bannermod.settlement.validation.BuildingInvalidationRuntime; +import com.talhanation.bannermod.war.runtime.SiegeExplosionTuning; import net.minecraft.core.BlockPos; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.InteractionResult; @@ -17,8 +19,8 @@ import net.neoforged.neoforge.event.level.BlockEvent; import net.neoforged.neoforge.event.level.ExplosionEvent; -final class ClaimProtectionEventService { - void onBlockBreakEvent(BlockEvent.BreakEvent event) { +public final class ClaimProtectionEventService { + public void onBlockBreakEvent(BlockEvent.BreakEvent event) { if(event.getLevel().isClientSide()) return; if(claimProtectionPolicy().shouldDenyBlockBreak(event.getLevel(), event.getPos(), event.getPlayer())) { event.setCanceled(true); @@ -30,7 +32,7 @@ void onBlockBreakEvent(BlockEvent.BreakEvent event) { } } - void onBlockPlaceEvent(BlockEvent.EntityPlaceEvent event) { + public void onBlockPlaceEvent(BlockEvent.EntityPlaceEvent event) { if(event.getLevel().isClientSide()) return; if(claimProtectionPolicy().shouldDenyBlockPlacement(event.getLevel(), event.getPos(), event.getEntity())) { event.setCanceled(true); @@ -44,7 +46,7 @@ void onBlockPlaceEvent(BlockEvent.EntityPlaceEvent event) { } } - void onFluidPlaceBlockEvent(BlockEvent.FluidPlaceBlockEvent event) { + public void onFluidPlaceBlockEvent(BlockEvent.FluidPlaceBlockEvent event) { LevelAccessor level = event.getLevel(); if(level.isClientSide()) return; if(claimProtectionPolicy().shouldDenyFluidPlacement(level, event.getPos(), event.getLiquidPos())) { @@ -56,7 +58,7 @@ void onFluidPlaceBlockEvent(BlockEvent.FluidPlaceBlockEvent event) { } } - void onExplosion(ExplosionEvent.Start event) { + public void onExplosion(ExplosionEvent.Start event) { if(event.getLevel().isClientSide()) return; Vec3 vec = event.getExplosion().center(); BlockPos pos = new BlockPos((int) vec.x, (int) vec.y, (int) vec.z); @@ -88,7 +90,7 @@ void onExplosion(ExplosionEvent.Start event) { } } - void onExplosionDetonate(ExplosionEvent.Detonate event) { + public void onExplosionDetonate(ExplosionEvent.Detonate event) { if (event.getLevel().isClientSide()) return; if (!SiegeExplosionTuning.shouldLimitTerrainDamage(event.getExplosion().getDirectSourceEntity())) { return; @@ -96,7 +98,7 @@ void onExplosionDetonate(ExplosionEvent.Detonate event) { SiegeExplosionTuning.limitAffectedBlocks(event.getExplosion().center(), event.getAffectedBlocks()); } - void onBlockInteract(PlayerInteractEvent.RightClickBlock event) { + public void onBlockInteract(PlayerInteractEvent.RightClickBlock event) { if(event.getLevel().isClientSide()) return; Player player = event.getEntity(); if(claimProtectionPolicy().shouldDenyBlockInteraction(event.getLevel(), event.getPos(), player, event.getHand())){ @@ -106,7 +108,7 @@ void onBlockInteract(PlayerInteractEvent.RightClickBlock event) { } } - void onItemInteract(PlayerInteractEvent.RightClickItem event) { + public void onItemInteract(PlayerInteractEvent.RightClickItem event) { if(event.getLevel().isClientSide()) return; BlockPos targetPos = ClaimInteractionTargetResolver.resolveItemInteractionTarget(event.getEntity(), event.getHand()); if(targetPos == null) return; @@ -117,7 +119,7 @@ void onItemInteract(PlayerInteractEvent.RightClickItem event) { } } - void onEntityInteract(PlayerInteractEvent.EntityInteract event) { + public void onEntityInteract(PlayerInteractEvent.EntityInteract event) { if(event.getLevel().isClientSide()) return; if(claimProtectionPolicy().shouldDenyEntityInteraction(event.getEntity(), event.getTarget())){ event.setCanceled(true); @@ -126,7 +128,7 @@ void onEntityInteract(PlayerInteractEvent.EntityInteract event) { } } - void onEntityInteractSpecific(PlayerInteractEvent.EntityInteractSpecific event) { + public void onEntityInteractSpecific(PlayerInteractEvent.EntityInteractSpecific event) { if(event.getLevel().isClientSide()) return; if(claimProtectionPolicy().shouldDenyEntityInteraction(event.getEntity(), event.getTarget())){ event.setCanceled(true); @@ -135,7 +137,7 @@ void onEntityInteractSpecific(PlayerInteractEvent.EntityInteractSpecific event) } } - void onAttackEntity(AttackEntityEvent event) { + public void onAttackEntity(AttackEntityEvent event) { Player player = event.getEntity(); if(player.level().isClientSide()) return; if(claimProtectionPolicy().shouldDenyEntityAttack(player, event.getTarget())){ diff --git a/src/main/java/com/talhanation/bannermod/events/ClaimProtectionFeedback.java b/src/main/java/com/talhanation/bannermod/settlement/runtime/ClaimProtectionFeedback.java similarity index 97% rename from src/main/java/com/talhanation/bannermod/events/ClaimProtectionFeedback.java rename to src/main/java/com/talhanation/bannermod/settlement/runtime/ClaimProtectionFeedback.java index e1f9c6d2..ea85d666 100644 --- a/src/main/java/com/talhanation/bannermod/events/ClaimProtectionFeedback.java +++ b/src/main/java/com/talhanation/bannermod/settlement/runtime/ClaimProtectionFeedback.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events; +package com.talhanation.bannermod.settlement.runtime; import com.talhanation.bannermod.persistence.military.RecruitsClaim; import com.talhanation.bannermod.persistence.military.RecruitsClaimManager; diff --git a/src/main/java/com/talhanation/bannermod/events/ClaimProtectionPolicy.java b/src/main/java/com/talhanation/bannermod/settlement/runtime/ClaimProtectionPolicy.java similarity index 99% rename from src/main/java/com/talhanation/bannermod/events/ClaimProtectionPolicy.java rename to src/main/java/com/talhanation/bannermod/settlement/runtime/ClaimProtectionPolicy.java index 34efd89f..3609e81f 100644 --- a/src/main/java/com/talhanation/bannermod/events/ClaimProtectionPolicy.java +++ b/src/main/java/com/talhanation/bannermod/settlement/runtime/ClaimProtectionPolicy.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events; +package com.talhanation.bannermod.settlement.runtime; import com.talhanation.bannermod.config.RecruitsServerConfig; import com.talhanation.bannermod.persistence.military.RecruitsClaim; diff --git a/src/main/java/com/talhanation/bannermod/events/ClaimQueueTickService.java b/src/main/java/com/talhanation/bannermod/settlement/runtime/ClaimQueueTickService.java similarity index 77% rename from src/main/java/com/talhanation/bannermod/events/ClaimQueueTickService.java rename to src/main/java/com/talhanation/bannermod/settlement/runtime/ClaimQueueTickService.java index beb2a6e7..b9043ced 100644 --- a/src/main/java/com/talhanation/bannermod/events/ClaimQueueTickService.java +++ b/src/main/java/com/talhanation/bannermod/settlement/runtime/ClaimQueueTickService.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events; +package com.talhanation.bannermod.settlement.runtime; import com.talhanation.bannermod.army.command.CommandIntentQueueRuntime; import com.talhanation.bannermod.config.WorkersServerConfig; @@ -7,25 +7,25 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; -final class ClaimQueueTickService { +public final class ClaimQueueTickService { private long serverTickStartedAtNanos; - void onServerTickStart() { + public void onServerTickStart() { serverTickStartedAtNanos = System.nanoTime(); } - void recordServerTickDuration() { + public void recordServerTickDuration() { if (serverTickStartedAtNanos > 0L) { AdaptiveRuntimeBudgets.recordServerTickNanos(System.nanoTime() - serverTickStartedAtNanos); } } - void tickCommandQueue(MinecraftServer server, ServerLevel level) { + public void tickCommandQueue(MinecraftServer server, ServerLevel level) { // Command-intent queue advancement runs every tick; the runtime no-ops when idle. CommandIntentQueueRuntime.instance().tick(server, level.getGameTime()); } - void tickBuildingInvalidationQueue(ServerLevel level) { + public void tickBuildingInvalidationQueue(ServerLevel level) { int revalidationBudget = AdaptiveRuntimeBudgets.intBudget( "settlement.revalidation.batch", WorkersServerConfig.settlementRevalidationBatchSizePerTick(), diff --git a/src/main/java/com/talhanation/bannermod/events/civilian/SettlementContainerHookPolicy.java b/src/main/java/com/talhanation/bannermod/settlement/runtime/SettlementContainerHookPolicy.java similarity index 93% rename from src/main/java/com/talhanation/bannermod/events/civilian/SettlementContainerHookPolicy.java rename to src/main/java/com/talhanation/bannermod/settlement/runtime/SettlementContainerHookPolicy.java index de16ce47..11c3efc7 100644 --- a/src/main/java/com/talhanation/bannermod/events/civilian/SettlementContainerHookPolicy.java +++ b/src/main/java/com/talhanation/bannermod/settlement/runtime/SettlementContainerHookPolicy.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events.civilian; +package com.talhanation.bannermod.settlement.runtime; /** * Pure decision helper for "should this container place/break trigger a settlement refresh?" diff --git a/src/main/java/com/talhanation/bannermod/settlement/runtime/SettlementHeartbeatService.java b/src/main/java/com/talhanation/bannermod/settlement/runtime/SettlementHeartbeatService.java index 198b3e14..77802b62 100644 --- a/src/main/java/com/talhanation/bannermod/settlement/runtime/SettlementHeartbeatService.java +++ b/src/main/java/com/talhanation/bannermod/settlement/runtime/SettlementHeartbeatService.java @@ -3,7 +3,6 @@ import com.talhanation.bannermod.events.ClaimEvents; import com.talhanation.bannermod.governance.BannerModGovernorHeartbeat; import com.talhanation.bannermod.governance.BannerModGovernorManager; -import com.talhanation.bannermod.governance.BannerModTreasuryManager; import com.talhanation.bannermod.settlement.BannerModSettlementManager; import com.talhanation.bannermod.settlement.BannerModSettlementOrchestrator; import com.talhanation.bannermod.settlement.BannerModSettlementService; @@ -51,11 +50,10 @@ private void tickGovernorMaintenance(ServerLevel level) { if (governorMaintenanceStage == GOVERNOR_STAGE_HEARTBEAT) { long startNanos = System.nanoTime(); - BannerModGovernorHeartbeat.BatchResult result = BannerModGovernorHeartbeat.runGovernedClaimHeartbeatBatch( + BannerModGovernorHeartbeat.BatchResult result = SettlementTreasuryDerivationService.runGovernorHeartbeatBatch( level, ClaimEvents.claimManager(), governorManager, - BannerModTreasuryManager.get(level), governorMaintenanceCursor, GOVERNOR_HEARTBEAT_BATCH_SIZE ); diff --git a/src/main/java/com/talhanation/bannermod/settlement/runtime/SettlementTreasuryDerivationService.java b/src/main/java/com/talhanation/bannermod/settlement/runtime/SettlementTreasuryDerivationService.java new file mode 100644 index 00000000..150b2e80 --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/settlement/runtime/SettlementTreasuryDerivationService.java @@ -0,0 +1,66 @@ +package com.talhanation.bannermod.settlement.runtime; + +import com.talhanation.bannermod.governance.BannerModGovernorHeartbeat; +import com.talhanation.bannermod.governance.BannerModGovernorManager; +import com.talhanation.bannermod.governance.BannerModGovernorSnapshot; +import com.talhanation.bannermod.governance.BannerModTreasuryLedgerSnapshot; +import com.talhanation.bannermod.governance.BannerModTreasuryManager; +import com.talhanation.bannermod.persistence.military.RecruitsClaimManager; +import com.talhanation.bannermod.shared.logistics.BannerModSupplyStatus; +import com.talhanation.bannermod.shared.settlement.BannerModSettlementBinding; +import net.minecraft.server.level.ServerLevel; + +import javax.annotation.Nullable; + +public final class SettlementTreasuryDerivationService { + private SettlementTreasuryDerivationService() { + } + + public static BannerModGovernorHeartbeat.BatchResult runGovernorHeartbeatBatch(ServerLevel level, + RecruitsClaimManager claimManager, + BannerModGovernorManager governorManager, + int startIndex, + int maxSnapshots) { + BannerModTreasuryManager treasuryManager = level == null ? null : BannerModTreasuryManager.get(level); + return BannerModGovernorHeartbeat.runGovernedClaimHeartbeatBatch( + level, + claimManager, + governorManager, + treasuryManager, + startIndex, + maxSnapshots + ); + } + + @Nullable + public static BannerModTreasuryLedgerSnapshot.FiscalRollup deriveHeartbeatAccounting(@Nullable BannerModTreasuryManager treasuryManager, + BannerModGovernorSnapshot snapshot, + BannerModSettlementBinding.Binding binding, + BannerModGovernorHeartbeat.HeartbeatReport report, + @Nullable BannerModSupplyStatus.RecruitSupplyStatus recruitSupplyStatus) { + if (treasuryManager == null || snapshot == null || binding == null || report == null) { + return null; + } + int requestedArmyUpkeepDebit = resolveRequestedArmyUpkeepDebit(recruitSupplyStatus); + BannerModTreasuryLedgerSnapshot updated = treasuryManager.applyHeartbeatAccounting( + snapshot.claimUuid(), + snapshot.anchorChunk(), + binding.claimFactionId(), + report.taxesCollected(), + requestedArmyUpkeepDebit, + report.heartbeatTick() + ); + return updated.projectFiscalRollup(report.taxesCollected(), requestedArmyUpkeepDebit, report.heartbeatTick()); + } + + private static int resolveRequestedArmyUpkeepDebit(@Nullable BannerModSupplyStatus.RecruitSupplyStatus recruitSupplyStatus) { + if (recruitSupplyStatus == null) { + return 0; + } + BannerModSupplyStatus.ArmyUpkeepStatus accounting = recruitSupplyStatus.accounting(); + if (accounting == null || !accounting.unpaid()) { + return 0; + } + return accounting.unpaidLevel(); + } +} diff --git a/src/main/java/com/talhanation/bannermod/events/SiegeExplosionTuning.java b/src/main/java/com/talhanation/bannermod/war/runtime/SiegeExplosionTuning.java similarity index 88% rename from src/main/java/com/talhanation/bannermod/events/SiegeExplosionTuning.java rename to src/main/java/com/talhanation/bannermod/war/runtime/SiegeExplosionTuning.java index 835fd7c2..b86e66f9 100644 --- a/src/main/java/com/talhanation/bannermod/events/SiegeExplosionTuning.java +++ b/src/main/java/com/talhanation/bannermod/war/runtime/SiegeExplosionTuning.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events; +package com.talhanation.bannermod.war.runtime; import com.talhanation.bannermod.compat.MedievalSiegeMachinesCompat; import net.minecraft.core.BlockPos; @@ -13,7 +13,7 @@ import java.util.List; import java.util.Set; -final class SiegeExplosionTuning { +public final class SiegeExplosionTuning { private static final Set FULL_SIEGE_NAMESPACES = Set.of( MedievalSiegeMachinesCompat.MOD_ID, MedievalSiegeMachinesCompat.LEGACY_MOD_ID @@ -26,11 +26,11 @@ final class SiegeExplosionTuning { private SiegeExplosionTuning() { } - static boolean shouldLimitTerrainDamage(@Nullable Entity sourceEntity) { + public static boolean shouldLimitTerrainDamage(@Nullable Entity sourceEntity) { return isSiegeExplosionSource(entityTypeId(sourceEntity)); } - static boolean isSiegeExplosionSource(@Nullable ResourceLocation entityId) { + public static boolean isSiegeExplosionSource(@Nullable ResourceLocation entityId) { if (entityId == null) { return false; } @@ -45,7 +45,7 @@ static boolean isSiegeExplosionSource(@Nullable ResourceLocation entityId) { return path.contains("cannon") || path.contains("bomb") || path.contains("shell"); } - static void limitAffectedBlocks(Vec3 center, List affectedBlocks) { + public static void limitAffectedBlocks(Vec3 center, List affectedBlocks) { if (affectedBlocks == null || affectedBlocks.size() <= MAX_AFFECTED_BLOCKS) { return; } diff --git a/src/test/java/com/talhanation/bannermod/events/RecruitGovernorWorkflowTest.java b/src/test/java/com/talhanation/bannermod/events/RecruitGovernorWorkflowTest.java index 2a8400ca..cce7b7cc 100644 --- a/src/test/java/com/talhanation/bannermod/events/RecruitGovernorWorkflowTest.java +++ b/src/test/java/com/talhanation/bannermod/events/RecruitGovernorWorkflowTest.java @@ -1,5 +1,6 @@ package com.talhanation.bannermod.events; +import com.talhanation.bannermod.governance.runtime.RecruitGovernorWorkflow; import com.talhanation.bannermod.governance.BannerModGovernorSnapshot; import com.talhanation.bannermod.persistence.military.RecruitsClaim; import com.talhanation.bannermod.settlement.BannerModSettlementSnapshot; diff --git a/src/test/java/com/talhanation/bannermod/events/SiegeExplosionTuningTest.java b/src/test/java/com/talhanation/bannermod/events/SiegeExplosionTuningTest.java index 443a10eb..d896f20c 100644 --- a/src/test/java/com/talhanation/bannermod/events/SiegeExplosionTuningTest.java +++ b/src/test/java/com/talhanation/bannermod/events/SiegeExplosionTuningTest.java @@ -1,5 +1,6 @@ package com.talhanation.bannermod.events; +import com.talhanation.bannermod.war.runtime.SiegeExplosionTuning; import net.minecraft.core.BlockPos; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.phys.Vec3; diff --git a/src/test/java/com/talhanation/bannermod/events/runtime/RecruitTargetAuthorityTest.java b/src/test/java/com/talhanation/bannermod/events/runtime/RecruitTargetAuthorityTest.java index 89a46691..2c58dea8 100644 --- a/src/test/java/com/talhanation/bannermod/events/runtime/RecruitTargetAuthorityTest.java +++ b/src/test/java/com/talhanation/bannermod/events/runtime/RecruitTargetAuthorityTest.java @@ -1,5 +1,6 @@ package com.talhanation.bannermod.events.runtime; +import com.talhanation.bannermod.combat.runtime.RecruitTargetAuthority; import org.junit.jupiter.api.Test; import java.util.UUID; diff --git a/src/test/java/com/talhanation/bannermod/governance/BannerModGovernorHeartbeatTest.java b/src/test/java/com/talhanation/bannermod/governance/BannerModGovernorHeartbeatTest.java index da83adaf..833720dd 100644 --- a/src/test/java/com/talhanation/bannermod/governance/BannerModGovernorHeartbeatTest.java +++ b/src/test/java/com/talhanation/bannermod/governance/BannerModGovernorHeartbeatTest.java @@ -9,8 +9,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; class BannerModGovernorHeartbeatTest { @@ -169,148 +167,4 @@ void shortageAndLowDefenseMapToExplicitRecommendationAndIncidentVocabulary() { ); } - @Test - void friendlyHeartbeatDepositsCollectedTaxesIntoClaimLedger() { - UUID claimUuid = UUID.randomUUID(); - BannerModGovernorSnapshot snapshot = BannerModGovernorSnapshot.create(claimUuid, new net.minecraft.world.level.ChunkPos(4, 7), "blueguild"); - BannerModTreasuryManager treasuryManager = new BannerModTreasuryManager(); - - BannerModGovernorHeartbeat.depositTaxes( - treasuryManager, - snapshot, - new BannerModSettlementBinding.Binding(BannerModSettlementBinding.Status.FRIENDLY_CLAIM, "blueguild", "blueguild"), - new BannerModGovernorHeartbeat.HeartbeatReport(6, 12, 12, List.of(), List.of(), 100L, 100L) - ); - - BannerModTreasuryLedgerSnapshot ledger = treasuryManager.getLedger(claimUuid); - assertNotNull(ledger); - assertEquals("blueguild", ledger.settlementFactionId()); - assertEquals(12, ledger.accruedTaxes()); - assertEquals(12, ledger.lastDepositAmount()); - assertEquals(100L, ledger.lastDepositTick()); - } - - @Test - void hostileOrEmptyHeartbeatDoesNotCreateOrIncreaseTreasuryLedger() { - UUID claimUuid = UUID.randomUUID(); - BannerModGovernorSnapshot snapshot = BannerModGovernorSnapshot.create(claimUuid, new net.minecraft.world.level.ChunkPos(2, 9), "blueguild"); - BannerModTreasuryManager treasuryManager = new BannerModTreasuryManager(); - - BannerModGovernorHeartbeat.depositTaxes( - treasuryManager, - snapshot, - new BannerModSettlementBinding.Binding(BannerModSettlementBinding.Status.HOSTILE_CLAIM, "blueguild", "redguild"), - new BannerModGovernorHeartbeat.HeartbeatReport(4, 8, 0, List.of(), List.of(), 120L, 90L) - ); - - assertNull(treasuryManager.getLedger(claimUuid)); - } - - @Test - void unpaidRecruitUpkeepDebitsTreasuryLedgerWithinCollectedBalance() { - UUID claimUuid = UUID.randomUUID(); - BannerModGovernorSnapshot snapshot = BannerModGovernorSnapshot.create(claimUuid, new net.minecraft.world.level.ChunkPos(4, 7), "blueguild"); - BannerModTreasuryManager treasuryManager = new BannerModTreasuryManager(); - BannerModSettlementBinding.Binding binding = new BannerModSettlementBinding.Binding(BannerModSettlementBinding.Status.FRIENDLY_CLAIM, "blueguild", "blueguild"); - BannerModGovernorHeartbeat.HeartbeatReport report = new BannerModGovernorHeartbeat.HeartbeatReport(6, 12, 12, List.of(), List.of(), 100L, 100L); - - BannerModGovernorHeartbeat.depositTaxes(treasuryManager, snapshot, binding, report); - BannerModGovernorHeartbeat.recordArmyUpkeepDebit( - treasuryManager, - snapshot, - binding, - report, - new BannerModSupplyStatus.RecruitSupplyStatus( - BannerModSupplyStatus.RecruitSupplyState.NEEDS_PAYMENT, - true, - false, - true, - "recruit_upkeep_missing_payment", - BannerModSupplyStatus.armyUpkeepStatus(true, false, 100.0F) - ) - ); - - BannerModTreasuryLedgerSnapshot ledger = treasuryManager.getLedger(claimUuid); - assertNotNull(ledger); - assertEquals(12, ledger.accruedTaxes()); - assertEquals(1, ledger.spentArmyUpkeep()); - assertEquals(11, ledger.treasuryBalance()); - assertEquals(1, ledger.lastArmyUpkeepDebitAmount()); - assertEquals(100L, ledger.lastArmyUpkeepDebitTick()); - } - - @Test - void stableRecruitUpkeepDoesNotDebitTreasuryLedger() { - UUID claimUuid = UUID.randomUUID(); - BannerModGovernorSnapshot snapshot = BannerModGovernorSnapshot.create(claimUuid, new net.minecraft.world.level.ChunkPos(4, 7), "blueguild"); - BannerModTreasuryManager treasuryManager = new BannerModTreasuryManager(); - BannerModSettlementBinding.Binding binding = new BannerModSettlementBinding.Binding(BannerModSettlementBinding.Status.FRIENDLY_CLAIM, "blueguild", "blueguild"); - BannerModGovernorHeartbeat.HeartbeatReport report = new BannerModGovernorHeartbeat.HeartbeatReport(6, 12, 12, List.of(), List.of(), 100L, 100L); - - BannerModGovernorHeartbeat.depositTaxes(treasuryManager, snapshot, binding, report); - BannerModGovernorHeartbeat.recordArmyUpkeepDebit( - treasuryManager, - snapshot, - binding, - report, - new BannerModSupplyStatus.RecruitSupplyStatus( - BannerModSupplyStatus.RecruitSupplyState.READY, - false, - false, - false, - null, - BannerModSupplyStatus.armyUpkeepStatus(false, false, 100.0F) - ) - ); - - BannerModTreasuryLedgerSnapshot ledger = treasuryManager.getLedger(claimUuid); - assertNotNull(ledger); - assertEquals(12, ledger.treasuryBalance()); - assertEquals(0, ledger.spentArmyUpkeep()); - assertEquals(0, ledger.lastArmyUpkeepDebitAmount()); - } - - @Test - void heartbeatAccountingReturnsFiscalRollupForGovernorState() { - UUID claimUuid = UUID.randomUUID(); - BannerModGovernorSnapshot snapshot = BannerModGovernorSnapshot.create(claimUuid, new net.minecraft.world.level.ChunkPos(4, 7), "blueguild"); - BannerModTreasuryManager treasuryManager = new BannerModTreasuryManager(); - BannerModSettlementBinding.Binding binding = new BannerModSettlementBinding.Binding(BannerModSettlementBinding.Status.FRIENDLY_CLAIM, "blueguild", "blueguild"); - BannerModGovernorHeartbeat.HeartbeatReport report = new BannerModGovernorHeartbeat.HeartbeatReport(6, 12, 12, List.of(), List.of(), 100L, 100L); - - BannerModTreasuryLedgerSnapshot.FiscalRollup rollup = BannerModGovernorHeartbeat.recordHeartbeatAccounting( - treasuryManager, - snapshot, - binding, - report, - new BannerModSupplyStatus.RecruitSupplyStatus( - BannerModSupplyStatus.RecruitSupplyState.NEEDS_PAYMENT, - true, - false, - true, - "recruit_upkeep_missing_payment", - BannerModSupplyStatus.armyUpkeepStatus(true, false, 100.0F) - ) - ); - - assertNotNull(rollup); - assertEquals(11, rollup.treasuryBalance()); - assertEquals(11, rollup.lastNetChange()); - assertEquals(22, rollup.projectedNextBalance()); - assertEquals(100L, rollup.accountingTick()); - - BannerModGovernorSnapshot updatedSnapshot = snapshot.withHeartbeatReport( - report.heartbeatTick(), - report.collectionTick(), - report.citizenCount(), - report.taxesDue(), - report.taxesCollected(), - List.of(), - List.of() - ).withFiscalRollup(rollup); - - assertEquals(11, updatedSnapshot.treasuryBalance()); - assertEquals(11, updatedSnapshot.lastTreasuryNet()); - assertEquals(22, updatedSnapshot.projectedTreasuryBalance()); - } } diff --git a/src/test/java/com/talhanation/bannermod/network/messages/military/AssassinLeaderControlAuthorityTest.java b/src/test/java/com/talhanation/bannermod/network/messages/military/AssassinLeaderControlAuthorityTest.java new file mode 100644 index 00000000..0e154b96 --- /dev/null +++ b/src/test/java/com/talhanation/bannermod/network/messages/military/AssassinLeaderControlAuthorityTest.java @@ -0,0 +1,68 @@ +package com.talhanation.bannermod.network.messages.military; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AssassinLeaderControlAuthorityTest { + @Test + void serverGuiOpenAssignsAndPersistsControlOwner() throws IOException { + Path entityPath = Paths.get("src/main/java/com/talhanation/bannermod/entity/military/AssassinLeaderEntity.java"); + String source = Files.readString(entityPath); + + int serverGuiBranch = source.indexOf("if (player instanceof ServerPlayer)"); + int assignment = source.indexOf("assignControlOwnerIfAbsent(player)"); + int openScreen = source.indexOf("BannerModNetworkHooks.openScreen"); + int saveOwner = source.indexOf("nbt.putUUID(CONTROL_OWNER_TAG, this.controlOwnerUUID)"); + int loadOwner = source.indexOf("nbt.hasUUID(CONTROL_OWNER_TAG) ? nbt.getUUID(CONTROL_OWNER_TAG) : null"); + + assertTrue(serverGuiBranch >= 0, "assassin leader GUI must still have a server-side open path"); + assertTrue(assignment > serverGuiBranch && assignment < openScreen, + "server-side GUI open must assign the control owner before the menu can send count changes"); + assertTrue(saveOwner >= 0, "control owner must be persisted with the entity"); + assertTrue(loadOwner >= 0, "control owner must be restored when the entity loads"); + } + + @Test + void countPacketChecksControlOwnerWithoutAssigningIt() throws IOException { + Path handlerPath = Paths.get("src/main/java/com/talhanation/bannermod/network/messages/military/MessageAssassinCount.java"); + String source = Files.readString(handlerPath); + + int controlCheck = source.indexOf("leader.isControlledBy(player) || player.hasPermissions(2)"); + int countMutation = source.indexOf("leader.setCount(this.count)"); + int ownerAssignment = source.indexOf("assignControlOwnerIfAbsent"); + + assertTrue(controlCheck >= 0, "count updates must require the server-side control owner or op permission"); + assertTrue(countMutation >= 0, "authorized count updates must still set the leader count"); + assertTrue(controlCheck < countMutation, + "forged count packets must hit the owner-or-op authority check before count mutation"); + assertTrue(ownerAssignment < 0, "count packets must not assign or claim control ownership"); + } + + @Test + void countPacketRejectsForeignNonOpAndKeepsRangeGate() throws IOException { + Path handlerPath = Paths.get("src/main/java/com/talhanation/bannermod/network/messages/military/MessageAssassinCount.java"); + String source = Files.readString(handlerPath); + + int authorityCheck = source.indexOf("leader.isControlledBy(player) || player.hasPermissions(2)"); + int rangeCheck = source.indexOf("player.getBoundingBox().inflate(16.0D).intersects(leader.getBoundingBox())"); + int countMutation = source.indexOf("leader.setCount(this.count)"); + + assertTrue(authorityCheck >= 0, "foreign non-op senders must not satisfy the owner-or-op count gate"); + assertTrue(rangeCheck >= 0, "count updates must preserve the existing nearby-leader range gate"); + assertTrue(countMutation >= 0, "owner or op senders must still be able to update count"); + assertTrue(authorityCheck < countMutation, "foreign non-op senders must be rejected before count mutation"); + assertTrue(rangeCheck < countMutation, "range validation must still run before count mutation"); + assertTrue(source.indexOf("player.hasPermissions(2)") < countMutation, + "op senders must pass the authority gate before count mutation"); + assertTrue(source.indexOf("leader.isControlledBy(player)") < countMutation, + "owner senders must pass the authority gate before count mutation"); + assertTrue(countMutation == source.lastIndexOf("leader.setCount(this.count)"), + "the guarded handler path must be the only count mutation entry point"); + } +} diff --git a/src/test/java/com/talhanation/bannermod/network/messages/military/MessageHireAuthorityTest.java b/src/test/java/com/talhanation/bannermod/network/messages/military/MessageHireAuthorityTest.java new file mode 100644 index 00000000..658b3de3 --- /dev/null +++ b/src/test/java/com/talhanation/bannermod/network/messages/military/MessageHireAuthorityTest.java @@ -0,0 +1,32 @@ +package com.talhanation.bannermod.network.messages.military; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MessageHireAuthorityTest { + private static final Path HANDLER = Path.of( + "src/main/java/com/talhanation/bannermod/network/messages/military/MessageHire.java"); + + @Test + void forgedForeignGroupUuidCannotReachHireHandling() throws IOException { + String src = Files.readString(HANDLER); + String authorityGate = "RecruitCommandAuthority.ownedGroup(player, groupUUID)"; + String unsafeLookup = "RecruitEvents.groupsManager().getGroup(groupUUID)"; + String hireHandling = "CommandEvents.handleRecruiting(player, group, recruit, true)"; + + int gateIndex = src.indexOf(authorityGate); + int hireHandlingIndex = src.indexOf(hireHandling); + + assertTrue(gateIndex >= 0, "hire must resolve the requested group through the canonical ownership gate"); + assertEquals(-1, src.indexOf(unsafeLookup), "hire must not pass arbitrary wire group UUIDs to group lookup"); + assertTrue(hireHandlingIndex >= 0, "authorized hires must still reach recruit hire handling"); + assertTrue(gateIndex < hireHandlingIndex, + "foreign group UUIDs must become null before hire handling can add members or update ownership"); + } +} diff --git a/src/test/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetInfoModeAuthorityTest.java b/src/test/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetInfoModeAuthorityTest.java new file mode 100644 index 00000000..8faee05b --- /dev/null +++ b/src/test/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetInfoModeAuthorityTest.java @@ -0,0 +1,42 @@ +package com.talhanation.bannermod.network.messages.military; + +import com.talhanation.bannermod.army.command.CommandHierarchy; +import com.talhanation.bannermod.army.command.CommandRole; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MessagePatrolLeaderSetInfoModeAuthorityTest { + private static final Path HANDLER = Path.of( + "src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetInfoMode.java"); + + private static final UUID OWNER = UUID.fromString("00000000-0000-0000-0000-000000000913"); + private static final UUID FOREIGN_SENDER = UUID.fromString("00000000-0000-0000-0000-000000000914"); + + @Test + void forgedForeignLeaderInfoModePacketCannotReachMutation() throws IOException { + assertEquals(CommandRole.NONE, + CommandHierarchy.roleFor(FOREIGN_SENDER, null, false, OWNER, null, true), + "Foreign non-op sender must not directly control another player's leader"); + + String src = Files.readString(HANDLER); + String authorityGate = "RecruitCommandAuthority.canDirectlyControl(player, leader)"; + String handlerMutation = "leader.setInfoMode(state)"; + + int gateIndex = src.indexOf(authorityGate); + int mutationIndex = src.indexOf(handlerMutation); + + assertTrue(gateIndex >= 0, "Info-mode handler must use the canonical recruit authority gate"); + assertTrue(mutationIndex >= 0, "Info-mode handler must still mutate info mode for authorized leaders"); + assertTrue(gateIndex < mutationIndex, + "Forged packet for a foreign nearby leader must fail authority before info-mode state can change"); + assertEquals(mutationIndex, src.lastIndexOf(handlerMutation), + "The guarded handler path must be the only info-mode mutation entry point"); + } +} diff --git a/src/test/java/com/talhanation/bannermod/network/messages/military/MessagePromoteRecruitAuthorityTest.java b/src/test/java/com/talhanation/bannermod/network/messages/military/MessagePromoteRecruitAuthorityTest.java new file mode 100644 index 00000000..040d3e4f --- /dev/null +++ b/src/test/java/com/talhanation/bannermod/network/messages/military/MessagePromoteRecruitAuthorityTest.java @@ -0,0 +1,26 @@ +package com.talhanation.bannermod.network.messages.military; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MessagePromoteRecruitAuthorityTest { + @Test + void foreignNearbySenderCannotReachPromotion() throws IOException { + Path handler = Paths.get("src/main/java/com/talhanation/bannermod/network/messages/military/MessagePromoteRecruit.java"); + String source = Files.readString(handler); + + int authorityCheck = source.indexOf("RecruitCommandAuthority.canDirectlyControl(sender, recruit)"); + int promoteRecruit = source.indexOf("RecruitEvents.promoteRecruit(recruit, profession, name, sender)"); + + assertTrue(authorityCheck >= 0, "promote recruit must require direct recruit command authority"); + assertTrue(promoteRecruit >= 0, "promote recruit must still promote authorized recruits"); + assertTrue(authorityCheck < promoteRecruit, + "a forged recruit UUID must hit the authority check before promotion can discard or transfer it"); + } +} diff --git a/src/test/java/com/talhanation/bannermod/network/messages/military/MessageRemoveAssignedGroupFromCompanionAuthorityTest.java b/src/test/java/com/talhanation/bannermod/network/messages/military/MessageRemoveAssignedGroupFromCompanionAuthorityTest.java new file mode 100644 index 00000000..321f0679 --- /dev/null +++ b/src/test/java/com/talhanation/bannermod/network/messages/military/MessageRemoveAssignedGroupFromCompanionAuthorityTest.java @@ -0,0 +1,59 @@ +package com.talhanation.bannermod.network.messages.military; + +import com.talhanation.bannermod.army.command.CommandHierarchy; +import com.talhanation.bannermod.army.command.CommandRole; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MessageRemoveAssignedGroupFromCompanionAuthorityTest { + private static final Path HANDLER = Path.of( + "src/main/java/com/talhanation/bannermod/network/messages/military/MessageRemoveAssignedGroupFromCompanion.java"); + + private static final UUID OWNER = UUID.fromString("00000000-0000-0000-0000-000000000916"); + private static final UUID FOREIGN_SENDER = UUID.fromString("00000000-0000-0000-0000-000000000917"); + + @Test + void forgedForeignCompanionGroupRemovalCannotReachMutations() throws IOException { + assertEquals(CommandRole.NONE, + CommandHierarchy.roleFor(FOREIGN_SENDER, null, false, OWNER, null, true), + "Foreign non-op sender must not directly control another player's companion leader"); + + String src = Files.readString(HANDLER); + String authorityGate = "RecruitCommandAuthority.canDirectlyControl(serverPlayer, companionEntity)"; + String leaderBindingMutation = "group.leaderUUID = null"; + String armyListenMutation = "RecruitCommanderUtil.setRecruitsListen"; + String armyResetMutation = "companionEntity.army = null"; + String screenBroadcast = "new MessageToClientUpdateLeaderScreen"; + + int gateIndex = src.indexOf(authorityGate); + int leaderBindingIndex = src.indexOf(leaderBindingMutation); + int armyListenIndex = src.indexOf(armyListenMutation); + int armyResetIndex = src.indexOf(armyResetMutation); + int screenBroadcastIndex = src.indexOf(screenBroadcast); + + assertTrue(gateIndex >= 0, "Group-removal handler must use the canonical recruit authority gate"); + assertTrue(leaderBindingIndex >= 0, "Authorized removal must still clear the group leader binding"); + assertTrue(armyListenIndex >= 0, "Authorized removal must still reset army listen state"); + assertTrue(armyResetIndex >= 0, "Authorized removal must still detach the companion army"); + assertTrue(screenBroadcastIndex >= 0, "Authorized removal must still broadcast the leader screen update"); + assertTrue(gateIndex < leaderBindingIndex, + "Forged packet for a foreign nearby companion must fail authority before group binding changes"); + assertTrue(gateIndex < armyListenIndex, + "Forged packet for a foreign nearby companion must fail authority before army state changes"); + assertTrue(gateIndex < armyResetIndex, + "Forged packet for a foreign nearby companion must fail authority before army detaches"); + assertTrue(gateIndex < screenBroadcastIndex, + "Forged packet for a foreign nearby companion must fail authority before screen updates broadcast"); + assertEquals(leaderBindingIndex, src.lastIndexOf(leaderBindingMutation), + "The guarded handler path must be the only leader-binding mutation entry point"); + assertEquals(armyResetIndex, src.lastIndexOf(armyResetMutation), + "The guarded handler path must be the only army-detach mutation entry point"); + } +} diff --git a/src/test/java/com/talhanation/bannermod/network/messages/military/MessageSendMessengerAuthorityTest.java b/src/test/java/com/talhanation/bannermod/network/messages/military/MessageSendMessengerAuthorityTest.java new file mode 100644 index 00000000..ceec1b6d --- /dev/null +++ b/src/test/java/com/talhanation/bannermod/network/messages/military/MessageSendMessengerAuthorityTest.java @@ -0,0 +1,34 @@ +package com.talhanation.bannermod.network.messages.military; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MessageSendMessengerAuthorityTest { + @Test + void foreignNearbySenderCannotMutateMessengerDelivery() throws IOException { + Path handler = Paths.get("src/main/java/com/talhanation/bannermod/network/messages/military/MessageSendMessenger.java"); + String source = Files.readString(handler); + + int authorityCheck = source.indexOf("!player.getUUID().equals(messenger.getOwnerUUID()) && !player.hasPermissions(2)"); + int setMessage = source.indexOf("messenger.setMessage(this.message)"); + int setTargetPlayerInfo = source.indexOf("messenger.setTargetPlayerInfo"); + int setTreatyState = source.indexOf("messenger.setIsTreatyMessenger(false)"); + int startDelivery = source.indexOf("messenger.start()"); + + assertTrue(authorityCheck >= 0, "messenger send must require owner or op authority"); + assertTrue(setMessage >= 0, "messenger send must still set message for authorized senders"); + assertTrue(setTargetPlayerInfo >= 0, "messenger send must still set target for authorized senders"); + assertTrue(setTreatyState >= 0, "messenger send must still reset treaty state for authorized senders"); + assertTrue(startDelivery >= 0, "messenger send must still start delivery for authorized senders"); + assertTrue(authorityCheck < setMessage, "foreign senders must be rejected before message mutation"); + assertTrue(authorityCheck < setTargetPlayerInfo, "foreign senders must be rejected before target mutation"); + assertTrue(authorityCheck < setTreatyState, "foreign senders must be rejected before treaty state mutation"); + assertTrue(authorityCheck < startDelivery, "foreign senders must be rejected before delivery start"); + } +} diff --git a/src/test/java/com/talhanation/bannermod/network/messages/military/PatrolLeaderWaypointAuthorityTest.java b/src/test/java/com/talhanation/bannermod/network/messages/military/PatrolLeaderWaypointAuthorityTest.java index ff19830e..1d83a30f 100644 --- a/src/test/java/com/talhanation/bannermod/network/messages/military/PatrolLeaderWaypointAuthorityTest.java +++ b/src/test/java/com/talhanation/bannermod/network/messages/military/PatrolLeaderWaypointAuthorityTest.java @@ -20,6 +20,12 @@ class PatrolLeaderWaypointAuthorityTest { "src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderRemoveWayPoint.java"); private static final Path SET_CYCLE_MESSAGE = ROOT.resolve( "src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetCycle.java"); + private static final Path SET_ENEMY_ACTION_MESSAGE = ROOT.resolve( + "src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetEnemyAction.java"); + private static final Path SET_PATROLLING_SPEED_MESSAGE = ROOT.resolve( + "src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetPatrollingSpeed.java"); + private static final Path SET_WAIT_TIME_MESSAGE = ROOT.resolve( + "src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetWaitTime.java"); private static final UUID OWNER = UUID.fromString("00000000-0000-0000-0000-000000000911"); private static final UUID FOREIGN_SENDER = UUID.fromString("00000000-0000-0000-0000-000000000912"); @@ -86,4 +92,67 @@ void forgedForeignLeaderCyclePacketCannotReachMutation() throws IOException { assertEquals(mutationIndex, src.lastIndexOf(handlerMutation), "The guarded handler path must be the only cycle mutation entry point"); } + + @Test + void forgedForeignLeaderEnemyActionPacketCannotReachMutation() throws IOException { + assertEquals(CommandRole.NONE, + CommandHierarchy.roleFor(FOREIGN_SENDER, null, false, OWNER, null, true), + "Foreign non-op sender must not directly control another player's leader"); + + String src = Files.readString(SET_ENEMY_ACTION_MESSAGE); + String authorityGate = "RecruitCommandAuthority.canDirectlyControl(player, leader)"; + String handlerMutation = "leader.setEnemyAction(this.action)"; + + int gateIndex = src.indexOf(authorityGate); + int mutationIndex = src.indexOf(handlerMutation); + + assertTrue(gateIndex >= 0, "Enemy-action handler must use the canonical recruit authority gate"); + assertTrue(mutationIndex >= 0, "Enemy-action handler must still mutate enemy action for authorized leaders"); + assertTrue(gateIndex < mutationIndex, + "Forged packet for a foreign leader must fail authority before enemy-action state can change"); + assertEquals(mutationIndex, src.lastIndexOf(handlerMutation), + "The guarded handler path must be the only enemy-action mutation entry point"); + } + + @Test + void forgedForeignLeaderPatrollingSpeedPacketCannotReachMutation() throws IOException { + assertEquals(CommandRole.NONE, + CommandHierarchy.roleFor(FOREIGN_SENDER, null, false, OWNER, null, true), + "Foreign non-op sender must not directly control another player's leader"); + + String src = Files.readString(SET_PATROLLING_SPEED_MESSAGE); + String authorityGate = "RecruitCommandAuthority.canDirectlyControl(player, leader)"; + String handlerMutation = "leader.setPatrolSpeed(this.speed)"; + + int gateIndex = src.indexOf(authorityGate); + int mutationIndex = src.indexOf(handlerMutation); + + assertTrue(gateIndex >= 0, "Patrolling speed handler must use the canonical recruit authority gate"); + assertTrue(mutationIndex >= 0, "Patrolling speed handler must still mutate speed for authorized leaders"); + assertTrue(gateIndex < mutationIndex, + "Forged packet for a foreign leader must fail authority before speed state can change"); + assertEquals(mutationIndex, src.lastIndexOf(handlerMutation), + "The guarded handler path must be the only speed mutation entry point"); + } + + @Test + void forgedForeignLeaderWaitTimePacketCannotReachMutation() throws IOException { + assertEquals(CommandRole.NONE, + CommandHierarchy.roleFor(FOREIGN_SENDER, null, false, OWNER, null, true), + "Foreign non-op sender must not directly control another player's leader"); + + String src = Files.readString(SET_WAIT_TIME_MESSAGE); + String authorityGate = "RecruitCommandAuthority.canDirectlyControl(player, leader)"; + String handlerMutation = "leader.setWaitTimeInMin(this.time)"; + + int gateIndex = src.indexOf(authorityGate); + int mutationIndex = src.indexOf(handlerMutation); + + assertTrue(gateIndex >= 0, "Wait-time handler must use the canonical recruit authority gate"); + assertTrue(mutationIndex >= 0, "Wait-time handler must still mutate wait time for authorized leaders"); + assertTrue(gateIndex < mutationIndex, + "Forged packet for a foreign nearby leader must fail authority before wait-time state can change"); + assertEquals(mutationIndex, src.lastIndexOf(handlerMutation), + "The guarded handler path must be the only wait-time mutation entry point"); + } } diff --git a/src/test/java/com/talhanation/bannermod/events/ClaimProtectionFeedbackTest.java b/src/test/java/com/talhanation/bannermod/settlement/runtime/ClaimProtectionFeedbackTest.java similarity index 94% rename from src/test/java/com/talhanation/bannermod/events/ClaimProtectionFeedbackTest.java rename to src/test/java/com/talhanation/bannermod/settlement/runtime/ClaimProtectionFeedbackTest.java index 1c8a3079..ef28aa81 100644 --- a/src/test/java/com/talhanation/bannermod/events/ClaimProtectionFeedbackTest.java +++ b/src/test/java/com/talhanation/bannermod/settlement/runtime/ClaimProtectionFeedbackTest.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events; +package com.talhanation.bannermod.settlement.runtime; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/talhanation/bannermod/events/civilian/SettlementContainerHookPolicyTest.java b/src/test/java/com/talhanation/bannermod/settlement/runtime/SettlementContainerHookPolicyTest.java similarity index 93% rename from src/test/java/com/talhanation/bannermod/events/civilian/SettlementContainerHookPolicyTest.java rename to src/test/java/com/talhanation/bannermod/settlement/runtime/SettlementContainerHookPolicyTest.java index 615ddaa4..aa7a8993 100644 --- a/src/test/java/com/talhanation/bannermod/events/civilian/SettlementContainerHookPolicyTest.java +++ b/src/test/java/com/talhanation/bannermod/settlement/runtime/SettlementContainerHookPolicyTest.java @@ -1,4 +1,4 @@ -package com.talhanation.bannermod.events.civilian; +package com.talhanation.bannermod.settlement.runtime; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/talhanation/bannermod/settlement/runtime/SettlementTreasuryDerivationServiceTest.java b/src/test/java/com/talhanation/bannermod/settlement/runtime/SettlementTreasuryDerivationServiceTest.java new file mode 100644 index 00000000..ace89729 --- /dev/null +++ b/src/test/java/com/talhanation/bannermod/settlement/runtime/SettlementTreasuryDerivationServiceTest.java @@ -0,0 +1,150 @@ +package com.talhanation.bannermod.settlement.runtime; + +import com.talhanation.bannermod.governance.BannerModGovernorHeartbeat; +import com.talhanation.bannermod.governance.BannerModGovernorSnapshot; +import com.talhanation.bannermod.governance.BannerModTreasuryLedgerSnapshot; +import com.talhanation.bannermod.governance.BannerModTreasuryManager; +import com.talhanation.bannermod.shared.logistics.BannerModSupplyStatus; +import com.talhanation.bannermod.shared.settlement.BannerModSettlementBinding; +import net.minecraft.world.level.ChunkPos; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +class SettlementTreasuryDerivationServiceTest { + @Test + void collectedTaxesDeriveClaimLedgerAndFiscalRollup() { + UUID claimUuid = UUID.randomUUID(); + BannerModGovernorSnapshot snapshot = BannerModGovernorSnapshot.create(claimUuid, new ChunkPos(4, 7), "blueguild"); + BannerModTreasuryManager treasuryManager = new BannerModTreasuryManager(); + BannerModSettlementBinding.Binding binding = new BannerModSettlementBinding.Binding(BannerModSettlementBinding.Status.FRIENDLY_CLAIM, "blueguild", "blueguild"); + BannerModGovernorHeartbeat.HeartbeatReport report = new BannerModGovernorHeartbeat.HeartbeatReport(6, 12, 12, List.of(), List.of(), 100L, 100L); + + BannerModTreasuryLedgerSnapshot.FiscalRollup rollup = SettlementTreasuryDerivationService.deriveHeartbeatAccounting( + treasuryManager, + snapshot, + binding, + report, + stableRecruitSupply() + ); + + BannerModTreasuryLedgerSnapshot ledger = treasuryManager.getLedger(claimUuid); + assertNotNull(ledger); + assertNotNull(rollup); + assertEquals("blueguild", ledger.settlementFactionId()); + assertEquals(12, ledger.accruedTaxes()); + assertEquals(0, ledger.spentArmyUpkeep()); + assertEquals(12, ledger.lastDepositAmount()); + assertEquals(0, ledger.lastArmyUpkeepDebitAmount()); + assertEquals(100L, ledger.lastDepositTick()); + assertEquals(12, rollup.treasuryBalance()); + assertEquals(12, rollup.lastNetChange()); + assertEquals(24, rollup.projectedNextBalance()); + } + + @Test + void emptyHeartbeatDoesNotCreateTreasuryLedger() { + UUID claimUuid = UUID.randomUUID(); + BannerModGovernorSnapshot snapshot = BannerModGovernorSnapshot.create(claimUuid, new ChunkPos(2, 9), "blueguild"); + BannerModTreasuryManager treasuryManager = new BannerModTreasuryManager(); + + BannerModTreasuryLedgerSnapshot.FiscalRollup rollup = SettlementTreasuryDerivationService.deriveHeartbeatAccounting( + treasuryManager, + snapshot, + new BannerModSettlementBinding.Binding(BannerModSettlementBinding.Status.HOSTILE_CLAIM, "blueguild", "redguild"), + new BannerModGovernorHeartbeat.HeartbeatReport(4, 8, 0, List.of(), List.of(), 120L, 90L), + stableRecruitSupply() + ); + + assertNull(treasuryManager.getLedger(claimUuid)); + assertNotNull(rollup); + assertEquals(0, rollup.treasuryBalance()); + assertEquals(0, rollup.lastNetChange()); + } + + @Test + void unpaidRecruitUpkeepDerivesTreasuryDebitInSameAccountingPass() { + UUID claimUuid = UUID.randomUUID(); + BannerModGovernorSnapshot snapshot = BannerModGovernorSnapshot.create(claimUuid, new ChunkPos(4, 7), "blueguild"); + BannerModTreasuryManager treasuryManager = new BannerModTreasuryManager(); + BannerModSettlementBinding.Binding binding = new BannerModSettlementBinding.Binding(BannerModSettlementBinding.Status.FRIENDLY_CLAIM, "blueguild", "blueguild"); + BannerModGovernorHeartbeat.HeartbeatReport report = new BannerModGovernorHeartbeat.HeartbeatReport(6, 12, 12, List.of(), List.of(), 100L, 100L); + + BannerModTreasuryLedgerSnapshot.FiscalRollup rollup = SettlementTreasuryDerivationService.deriveHeartbeatAccounting( + treasuryManager, + snapshot, + binding, + report, + unpaidRecruitSupply() + ); + + BannerModTreasuryLedgerSnapshot ledger = treasuryManager.getLedger(claimUuid); + assertNotNull(ledger); + assertNotNull(rollup); + assertEquals(12, ledger.accruedTaxes()); + assertEquals(1, ledger.spentArmyUpkeep()); + assertEquals(11, ledger.treasuryBalance()); + assertEquals(1, ledger.lastArmyUpkeepDebitAmount()); + assertEquals(100L, ledger.lastArmyUpkeepDebitTick()); + assertEquals(11, rollup.treasuryBalance()); + assertEquals(11, rollup.lastNetChange()); + assertEquals(22, rollup.projectedNextBalance()); + assertEquals(100L, rollup.accountingTick()); + } + + @Test + void fiscalRollupCanBeAppliedToGovernorSnapshot() { + UUID claimUuid = UUID.randomUUID(); + BannerModGovernorSnapshot snapshot = BannerModGovernorSnapshot.create(claimUuid, new ChunkPos(4, 7), "blueguild"); + BannerModGovernorHeartbeat.HeartbeatReport report = new BannerModGovernorHeartbeat.HeartbeatReport(6, 12, 12, List.of(), List.of(), 100L, 100L); + + BannerModTreasuryLedgerSnapshot.FiscalRollup rollup = SettlementTreasuryDerivationService.deriveHeartbeatAccounting( + new BannerModTreasuryManager(), + snapshot, + new BannerModSettlementBinding.Binding(BannerModSettlementBinding.Status.FRIENDLY_CLAIM, "blueguild", "blueguild"), + report, + unpaidRecruitSupply() + ); + + BannerModGovernorSnapshot updatedSnapshot = snapshot.withHeartbeatReport( + report.heartbeatTick(), + report.collectionTick(), + report.citizenCount(), + report.taxesDue(), + report.taxesCollected(), + List.of(), + List.of() + ).withFiscalRollup(rollup); + + assertEquals(11, updatedSnapshot.treasuryBalance()); + assertEquals(11, updatedSnapshot.lastTreasuryNet()); + assertEquals(22, updatedSnapshot.projectedTreasuryBalance()); + } + + private static BannerModSupplyStatus.RecruitSupplyStatus stableRecruitSupply() { + return new BannerModSupplyStatus.RecruitSupplyStatus( + BannerModSupplyStatus.RecruitSupplyState.READY, + false, + false, + false, + null, + BannerModSupplyStatus.armyUpkeepStatus(false, false, 100.0F) + ); + } + + private static BannerModSupplyStatus.RecruitSupplyStatus unpaidRecruitSupply() { + return new BannerModSupplyStatus.RecruitSupplyStatus( + BannerModSupplyStatus.RecruitSupplyState.NEEDS_PAYMENT, + true, + false, + true, + "recruit_upkeep_missing_payment", + BannerModSupplyStatus.armyUpkeepStatus(true, false, 100.0F) + ); + } +}