From 6dcb0c07af2da2281a95d5069b1697afc6c41bc7 Mon Sep 17 00:00:00 2001 From: Mr-Artillery Date: Sun, 26 Apr 2026 12:06:47 -0400 Subject: [PATCH 1/3] feat(governor): advisory text, auto-manage toggle, and settlement automation - Add BannerModGovernorAdvisory: incident/recommendation tokens to tutorial text - Add autoManage flag to BannerModGovernorSnapshot (persisted, default false) - Add BannerModGovernorService.updateAutoManage with authority/rules guards - Add MessageToggleGovernorAutoManage packet and register in network bootstrap - Extend MessageToClientUpdateGovernorScreen to carry autoManage flag - GovernorScreen: advisory panel, first-contact hint, auto-manage toggle button - Wire updateGovernorAutoManage through RecruitEvents -> RecruitGovernorWorkflow --- .../client/military/gui/GovernorScreen.java | 79 ++++++++++--- .../bannermod/events/RecruitEvents.java | 4 + .../events/RecruitGovernorWorkflow.java | 14 +++ .../governance/BannerModGovernorAdvisory.java | 110 ++++++++++++++++++ .../governance/BannerModGovernorService.java | 19 +++ .../governance/BannerModGovernorSnapshot.java | 34 ++++++ .../network/BannerModNetworkBootstrap.java | 1 + .../MessageToClientUpdateGovernorScreen.java | 7 +- .../MessageToggleGovernorAutoManage.java | 52 +++++++++ 9 files changed, 303 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/talhanation/bannermod/governance/BannerModGovernorAdvisory.java create mode 100644 src/main/java/com/talhanation/bannermod/network/messages/military/MessageToggleGovernorAutoManage.java diff --git a/src/main/java/com/talhanation/bannermod/client/military/gui/GovernorScreen.java b/src/main/java/com/talhanation/bannermod/client/military/gui/GovernorScreen.java index 3eedd539..d621517d 100644 --- a/src/main/java/com/talhanation/bannermod/client/military/gui/GovernorScreen.java +++ b/src/main/java/com/talhanation/bannermod/client/military/gui/GovernorScreen.java @@ -3,13 +3,14 @@ import com.mojang.blaze3d.systems.RenderSystem; import com.talhanation.bannermod.bootstrap.BannerModMain; import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; +import com.talhanation.bannermod.governance.BannerModGovernorAdvisory; import com.talhanation.bannermod.governance.BannerModGovernorPolicy; import com.talhanation.bannermod.inventory.military.GovernorContainer; import com.talhanation.bannermod.network.messages.military.MessageOpenGovernorScreen; +import com.talhanation.bannermod.network.messages.military.MessageToggleGovernorAutoManage; import com.talhanation.bannermod.network.messages.military.MessageUpdateGovernorPolicy; import de.maxhenkel.corelib.inventory.ScreenBase; import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.components.Button; import net.minecraft.client.renderer.GameRenderer; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; @@ -30,8 +31,8 @@ public class GovernorScreen extends ScreenBase { public GovernorScreen(GovernorContainer container, Inventory playerInventory, Component title) { super(TEXTURE, container, playerInventory, title); - this.imageWidth = 195; - this.imageHeight = 160; + this.imageWidth = 240; + this.imageHeight = 230; this.player = container.getPlayerEntity(); this.recruit = container.getRecruit(); } @@ -44,9 +45,22 @@ protected void init() { addPolicyButtons(BannerModGovernorPolicy.GARRISON_PRIORITY, 88); addPolicyButtons(BannerModGovernorPolicy.FORTIFICATION_PRIORITY, 112); addPolicyButtons(BannerModGovernorPolicy.TAX_PRESSURE, 136); + addRenderableWidget(new ExtendedButton(this.leftPos + 150, this.topPos + 160, 80, 16, + Component.literal(autoManageLabel()), + button -> toggleAutoManage(button))); BannerModMain.SIMPLE_CHANNEL.sendToServer(new MessageOpenGovernorScreen(this.recruit.getUUID(), false)); } + private String autoManageLabel() { + return latestState.autoManage ? "[Auto-manage: ON]" : "[Auto-manage: OFF]"; + } + + private void toggleAutoManage(net.minecraft.client.gui.components.Button button) { + boolean next = !latestState.autoManage; + button.setMessage(Component.literal(next ? "[Auto-manage: ON]" : "[Auto-manage: OFF]")); + BannerModMain.SIMPLE_CHANNEL.sendToServer(new MessageToggleGovernorAutoManage(this.recruit.getUUID(), next)); + } + private void addPolicyButtons(BannerModGovernorPolicy policy, int yOffset) { addRenderableWidget(new ExtendedButton(this.leftPos + 150, this.topPos + yOffset, 16, 16, Component.literal("-"), button -> stepPolicy(policy, -1))); addRenderableWidget(new ExtendedButton(this.leftPos + 170, this.topPos + yOffset, 16, 16, Component.literal("+"), button -> stepPolicy(policy, 1))); @@ -73,7 +87,8 @@ public void renderForeground(GuiGraphics guiGraphics, int mouseX, int mouseY, fl GovernorViewState state = latestState; int x = leftPos + 10; int y = topPos + 10; - guiGraphics.drawString(font, Component.literal("Governor: " + recruit.getName().getString()), x, y, 4210752, false); + + guiGraphics.drawString(font, Component.literal("Governor: " + recruit.getName().getString()), x, y, 0x4E3320, false); y += 12; guiGraphics.drawString(font, Component.literal("Settlement: " + state.settlementStatus), x, y, 4210752, false); y += 12; @@ -84,33 +99,40 @@ public void renderForeground(GuiGraphics guiGraphics, int mouseX, int mouseY, fl guiGraphics.drawString(font, Component.literal("Treasury: " + state.treasuryBalance + " (net " + state.lastTreasuryNet + ")"), x, y, 4210752, false); y += 12; guiGraphics.drawString(font, Component.literal("Projected: " + state.projectedTreasuryBalance), x, y, 4210752, false); - y += 12; - guiGraphics.drawString(font, Component.literal("Heartbeat: " + state.lastHeartbeatTick), x, y, 4210752, false); - y += 12; + + y += 16; guiGraphics.drawString(font, Component.literal("Garrison guidance: " + readableToken(state.garrisonRecommendation)), x, y, 4210752, false); y += 12; guiGraphics.drawString(font, Component.literal("Fortification advice: " + readableToken(state.fortificationRecommendation)), x, y, 4210752, false); + y += 16; - guiGraphics.drawString(font, Component.literal("Incidents:"), x, y, 4210752, false); + guiGraphics.drawString(font, Component.literal("Incidents:"), x, y, 0x8B0000, false); y += 12; for (String incident : state.incidents) { - guiGraphics.drawString(font, Component.literal("- " + incident), x, y, 4210752, false); + guiGraphics.drawString(font, Component.literal("- " + incident.replace('_', ' ')), x, y, 0xCC3300, false); y += 10; } if (state.incidents.isEmpty()) { guiGraphics.drawString(font, Component.literal("- none"), x, y, 4210752, false); y += 10; } + y += 6; - guiGraphics.drawString(font, Component.literal("Recommendations:"), x, y, 4210752, false); + guiGraphics.drawString(font, Component.literal("Governor says:"), x, y, 0x205020, false); y += 12; - for (String recommendation : state.recommendations) { - guiGraphics.drawString(font, Component.literal("- " + recommendation), x, y, 4210752, false); - y += 10; - } - if (state.recommendations.isEmpty()) { - guiGraphics.drawString(font, Component.literal("- none"), x, y, 4210752, false); + List advisoryLines = state.lastHeartbeatTick == 0 + ? List.of(BannerModGovernorAdvisory.firstContactHint()) + : BannerModGovernorAdvisory.buildAdvisoryLines(state.incidents, state.recommendations); + for (String line : advisoryLines) { + for (String wrapped : wrapText(line, 220)) { + guiGraphics.drawString(font, Component.literal(wrapped), x, y, 0x305030, false); + y += 10; + } } + + y += 6; + guiGraphics.drawString(font, Component.literal(BannerModGovernorAdvisory.autoManageTip(state.autoManage)), x, y, 0x444444, false); + guiGraphics.drawString(font, Component.literal("garrison priority: " + BannerModGovernorPolicy.GARRISON_PRIORITY.valueLabel(state.garrisonPriority)), leftPos + 10, topPos + 92, 4210752, false); guiGraphics.drawString(font, Component.literal("fortification priority: " + BannerModGovernorPolicy.FORTIFICATION_PRIORITY.valueLabel(state.fortificationPriority)), leftPos + 10, topPos + 116, 4210752, false); guiGraphics.drawString(font, Component.literal("tax pressure: " + BannerModGovernorPolicy.TAX_PRESSURE.valueLabel(state.taxPressure)), leftPos + 10, topPos + 140, 4210752, false); @@ -130,11 +152,13 @@ public static void applyUpdate(UUID recruitId, int treasuryBalance, int lastTreasuryNet, int projectedTreasuryBalance, + boolean autoManage, List incidents, List recommendations) { latestState = new GovernorViewState(recruitId, settlementStatus, citizenCount, taxesDue, taxesCollected, lastHeartbeatTick, garrisonRecommendation, fortificationRecommendation, garrisonPriority, fortificationPriority, taxPressure, treasuryBalance, lastTreasuryNet, projectedTreasuryBalance, + autoManage, new ArrayList<>(incidents), new ArrayList<>(recommendations)); } @@ -142,6 +166,27 @@ private static String readableToken(String token) { return token == null || token.isBlank() ? "none" : token.replace('_', ' '); } + private static List wrapText(String text, int maxPixelWidth) { + List lines = new ArrayList<>(); + if (text == null || text.isBlank()) { + return lines; + } + int charsPerLine = Math.max(10, maxPixelWidth / 6); + String[] words = text.split(" "); + StringBuilder current = new StringBuilder(); + for (String word : words) { + if (current.length() + word.length() + 1 > charsPerLine) { + lines.add(current.toString().trim()); + current = new StringBuilder(); + } + current.append(word).append(' '); + } + if (!current.toString().isBlank()) { + lines.add(current.toString().trim()); + } + return lines; + } + private record GovernorViewState(UUID recruitId, String settlementStatus, int citizenCount, @@ -156,6 +201,7 @@ private record GovernorViewState(UUID recruitId, int treasuryBalance, int lastTreasuryNet, int projectedTreasuryBalance, + boolean autoManage, List incidents, List recommendations) { private static GovernorViewState empty() { @@ -163,6 +209,7 @@ private static GovernorViewState empty() { "hold_course", "hold_course", BannerModGovernorPolicy.DEFAULT_VALUE, BannerModGovernorPolicy.DEFAULT_VALUE, BannerModGovernorPolicy.DEFAULT_VALUE, 0, 0, 0, + false, List.of(), List.of()); } } diff --git a/src/main/java/com/talhanation/bannermod/events/RecruitEvents.java b/src/main/java/com/talhanation/bannermod/events/RecruitEvents.java index 32e343a2..cfd67265 100644 --- a/src/main/java/com/talhanation/bannermod/events/RecruitEvents.java +++ b/src/main/java/com/talhanation/bannermod/events/RecruitEvents.java @@ -135,6 +135,10 @@ public static void updateGovernorPolicy(ServerPlayer player, AbstractRecruitEnti RecruitGovernorWorkflow.updateGovernorPolicy(player, recruit, policy, value); } + public static void updateGovernorAutoManage(ServerPlayer player, AbstractRecruitEntity recruit, boolean autoManage) { + RecruitGovernorWorkflow.updateGovernorAutoManage(player, recruit, autoManage); + } + public static void handleGroupBackwardCompatibility(AbstractRecruitEntity recruit, int oldGroupNumber) { RecruitWorldLifecycleService.handleLegacyGroup(recruit, oldGroupNumber, server, recruitsGroupsManager); } diff --git a/src/main/java/com/talhanation/bannermod/events/RecruitGovernorWorkflow.java b/src/main/java/com/talhanation/bannermod/events/RecruitGovernorWorkflow.java index 76e0824a..d67aab1f 100644 --- a/src/main/java/com/talhanation/bannermod/events/RecruitGovernorWorkflow.java +++ b/src/main/java/com/talhanation/bannermod/events/RecruitGovernorWorkflow.java @@ -108,6 +108,7 @@ static void syncGovernorScreen(ServerPlayer player, AbstractRecruitEntity recrui snapshot == null ? 0 : snapshot.treasuryBalance(), snapshot == null ? 0 : snapshot.lastTreasuryNet(), snapshot == null ? 0 : snapshot.projectedTreasuryBalance(), + snapshot != null && snapshot.autoManage(), snapshot == null ? java.util.List.of() : snapshot.incidentTokens(), snapshot == null ? java.util.List.of() : snapshot.recommendationTokens() )); @@ -126,6 +127,19 @@ static void updateGovernorPolicy(ServerPlayer player, AbstractRecruitEntity recr syncGovernorScreen(player, recruit); } + static void updateGovernorAutoManage(ServerPlayer player, AbstractRecruitEntity recruit, boolean autoManage) { + if (!(recruit.getCommandSenderWorld() instanceof ServerLevel serverLevel)) { + return; + } + BannerModGovernorService.OperationResult result = governorService(serverLevel) + .updateAutoManage(resolveClaim(recruit), BannerModGovernorAuthority.actor(player), autoManage); + if (!result.allowed()) { + player.sendSystemMessage(Component.literal("Governor auto-manage update denied: " + result.governorDecision().name().toLowerCase())); + return; + } + syncGovernorScreen(player, recruit); + } + private static BannerModGovernorService governorService(ServerLevel level) { return new BannerModGovernorService(BannerModGovernorManager.get(level)); } diff --git a/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorAdvisory.java b/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorAdvisory.java new file mode 100644 index 00000000..4f78bd0f --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorAdvisory.java @@ -0,0 +1,110 @@ +package com.talhanation.bannermod.governance; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +/** + * Translates raw {@link BannerModGovernorIncident} and {@link BannerModGovernorRecommendation} + * tokens into player-readable advisory lines. Used by the Governor screen to give new players + * actionable guidance about mod mechanics rather than opaque token strings. + * + *

All methods are pure (no side effects, no external state) so they can be called freely + * from both client and server without synchronisation concerns.

+ */ +public final class BannerModGovernorAdvisory { + + private BannerModGovernorAdvisory() { + } + + /** + * Returns a short, tutorial-style sentence explaining the given incident. + * Shown beneath the incident list in the Governor screen. + */ + public static String incidentTip(BannerModGovernorIncident incident) { + return switch (incident) { + case HOSTILE_CLAIM -> + "Another faction controls this land. Capture the claim banner to reclaim it for your settlement."; + case DEGRADED_SETTLEMENT -> + "This settlement is in a degraded state. Assign workers and ensure your claim flag is planted."; + case UNCLAIMED_SETTLEMENT -> + "No claim has been placed here. Right-click a Claim Banner on the ground to establish ownership."; + case UNDER_SIEGE -> + "Your settlement is under siege! Tax collection is suspended until the threat is repelled."; + case WORKER_SHORTAGE -> + "No workers are assigned. Hire workers from your settlement menu to produce resources and grow citizens."; + case SUPPLY_BLOCKED -> + "Worker supply chains are blocked. Check that haul routes are clear and storage is not full."; + case RECRUIT_UPKEEP_BLOCKED -> + "Your garrison cannot be paid. Increase treasury income or reduce troop count to restore upkeep."; + }; + } + + /** + * Returns an actionable advisory sentence for the given recommendation. + * Shown in the 'Governor says:' panel of the Governor screen. + */ + public static String recommendationTip(BannerModGovernorRecommendation recommendation) { + return switch (recommendation) { + case HOLD_COURSE -> + "Settlement is stable. Continue current operations."; + case INCREASE_GARRISON -> + "Citizen count exceeds garrison size. Recruit more soldiers or set garrison priority to High."; + case STRENGTHEN_FORTIFICATIONS -> + "Defences are thin. Build walls or place watchtowers inside your claimed chunks."; + case RELIEVE_SUPPLY_PRESSURE -> + "Supply lines are strained. Clear blocked routes or expand storage near the settlement core."; + }; + } + + /** + * Produces a combined advisory text block for display, one line per active item. + * Returns at least one line (the hold-course tip) so the panel is never blank. + */ + public static List buildAdvisoryLines(List incidentTokens, + List recommendationTokens) { + List lines = new ArrayList<>(); + + EnumSet activeIncidents = EnumSet.noneOf(BannerModGovernorIncident.class); + for (String token : incidentTokens) { + for (BannerModGovernorIncident incident : BannerModGovernorIncident.values()) { + if (incident.token().equals(token)) { + activeIncidents.add(incident); + } + } + } + for (BannerModGovernorIncident incident : activeIncidents) { + lines.add(incidentTip(incident)); + } + + for (String token : recommendationTokens) { + for (BannerModGovernorRecommendation rec : BannerModGovernorRecommendation.values()) { + if (rec.token().equals(token)) { + lines.add(recommendationTip(rec)); + } + } + } + + if (lines.isEmpty()) { + lines.add(recommendationTip(BannerModGovernorRecommendation.HOLD_COURSE)); + } + return lines; + } + + /** + * Returns a one-line intro shown at the top of the Governor screen for players who + * have not yet interacted with a Governor (indicated by a zero heartbeat tick). + */ + public static String firstContactHint() { + return "I am your Governor. I will manage this settlement and advise you on its needs."; + } + + /** + * Returns a short tutorial tooltip explaining the auto-manage toggle. + */ + public static String autoManageTip(boolean enabled) { + return enabled + ? "Auto-manage ON: I will issue garrison hold orders automatically when troops are needed." + : "Auto-manage OFF: I will advise only. You issue all orders manually."; + } +} diff --git a/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorService.java b/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorService.java index 04c17ceb..15da0fa9 100644 --- a/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorService.java +++ b/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorService.java @@ -138,6 +138,25 @@ public OperationResult updatePolicy(@Nullable RecruitsClaim claim, return new OperationResult(true, authorityDecision, governorDecision, updated); } + public OperationResult updateAutoManage(@Nullable RecruitsClaim claim, + BannerModGovernorAuthority.ActorContext actor, + boolean autoManage) { + BannerModGovernorSnapshot snapshot = claim == null ? null : getOrCreateGovernorSnapshot(claim); + BannerModGovernorAuthority.Decision authorityDecision = BannerModGovernorAuthority.revokeDecision( + actor, + snapshot == null ? null : snapshot.governorOwnerUuid(), + snapshot == null ? null : snapshot.settlementFactionId() + ); + BannerModSettlementBinding.Binding binding = resolveBinding(claim, snapshot == null ? null : snapshot.settlementFactionId(), true); + BannerModGovernorRules.Decision governorDecision = BannerModGovernorRules.controlDecision(binding, snapshot); + if (!BannerModGovernorAuthority.isAllowed(authorityDecision) || !BannerModGovernorRules.isAllowed(governorDecision) || snapshot == null) { + return new OperationResult(false, authorityDecision, governorDecision, snapshot); + } + BannerModGovernorSnapshot updated = snapshot.withAutoManage(autoManage); + this.manager.putSnapshot(updated); + return new OperationResult(true, authorityDecision, governorDecision, updated); + } + private BannerModGovernorSnapshot getOrCreateSnapshot(RecruitsClaim claim, @Nullable String settlementFactionId) { ChunkPos anchorChunk = resolveAnchorChunk(claim); BannerModGovernorSnapshot fallback = BannerModGovernorSnapshot.create( diff --git a/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorSnapshot.java b/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorSnapshot.java index 4d3cab5b..234a349a 100644 --- a/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorSnapshot.java +++ b/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorSnapshot.java @@ -30,6 +30,7 @@ public record BannerModGovernorSnapshot( int garrisonPriority, int fortificationPriority, int taxPressure, + boolean autoManage, List incidentTokens, List recommendationTokens ) { @@ -65,6 +66,7 @@ public BannerModGovernorSnapshot withGovernor(@Nullable UUID governorRecruitUuid this.garrisonPriority, this.fortificationPriority, this.taxPressure, + this.autoManage, this.incidentTokens, this.recommendationTokens ); @@ -89,6 +91,7 @@ public BannerModGovernorSnapshot withSettlementFactionId(@Nullable String settle this.garrisonPriority, this.fortificationPriority, this.taxPressure, + this.autoManage, this.incidentTokens, this.recommendationTokens ); @@ -113,6 +116,32 @@ public BannerModGovernorSnapshot withPolicies(int garrisonPriority, int fortific BannerModGovernorPolicy.GARRISON_PRIORITY.clamp(garrisonPriority), BannerModGovernorPolicy.FORTIFICATION_PRIORITY.clamp(fortificationPriority), BannerModGovernorPolicy.TAX_PRESSURE.clamp(taxPressure), + this.autoManage, + this.incidentTokens, + this.recommendationTokens + ); + } + + public BannerModGovernorSnapshot withAutoManage(boolean autoManage) { + return new BannerModGovernorSnapshot( + this.claimUuid, + this.anchorChunkX, + this.anchorChunkZ, + this.settlementFactionId, + this.governorRecruitUuid, + this.governorOwnerUuid, + this.lastHeartbeatTick, + this.lastCollectionTick, + this.citizenCount, + this.taxesDue, + this.taxesCollected, + this.treasuryBalance, + this.lastTreasuryNet, + this.projectedTreasuryBalance, + this.garrisonPriority, + this.fortificationPriority, + this.taxPressure, + autoManage, this.incidentTokens, this.recommendationTokens ); @@ -140,6 +169,7 @@ public BannerModGovernorSnapshot withHeartbeatReport(long heartbeatTick, long co this.garrisonPriority, this.fortificationPriority, this.taxPressure, + this.autoManage, incidentTokens, recommendationTokens ); @@ -164,6 +194,7 @@ public BannerModGovernorSnapshot withFiscalRollup(@Nullable BannerModTreasuryLed this.garrisonPriority, this.fortificationPriority, this.taxPressure, + this.autoManage, this.incidentTokens, this.recommendationTokens ); @@ -194,6 +225,7 @@ public CompoundTag toTag() { tag.putInt("GarrisonPriority", this.garrisonPriority); tag.putInt("FortificationPriority", this.fortificationPriority); tag.putInt("TaxPressure", this.taxPressure); + tag.putBoolean("AutoManage", this.autoManage); tag.put("IncidentTokens", writeTokens(this.incidentTokens)); tag.put("RecommendationTokens", writeTokens(this.recommendationTokens)); return tag; @@ -218,6 +250,7 @@ public static BannerModGovernorSnapshot create(UUID claimUuid, ChunkPos anchorCh BannerModGovernorPolicy.DEFAULT_VALUE, BannerModGovernorPolicy.DEFAULT_VALUE, BannerModGovernorPolicy.DEFAULT_VALUE, + false, List.of(), List.of() ); @@ -246,6 +279,7 @@ public static BannerModGovernorSnapshot fromTag(CompoundTag tag) { tag.contains("GarrisonPriority", Tag.TAG_INT) ? tag.getInt("GarrisonPriority") : BannerModGovernorPolicy.DEFAULT_VALUE, tag.contains("FortificationPriority", Tag.TAG_INT) ? tag.getInt("FortificationPriority") : BannerModGovernorPolicy.DEFAULT_VALUE, tag.contains("TaxPressure", Tag.TAG_INT) ? tag.getInt("TaxPressure") : BannerModGovernorPolicy.DEFAULT_VALUE, + tag.contains("AutoManage", Tag.TAG_BYTE) && tag.getBoolean("AutoManage"), readTokens(tag.getList("IncidentTokens", Tag.TAG_STRING)), readTokens(tag.getList("RecommendationTokens", Tag.TAG_STRING)) ); diff --git a/src/main/java/com/talhanation/bannermod/network/BannerModNetworkBootstrap.java b/src/main/java/com/talhanation/bannermod/network/BannerModNetworkBootstrap.java index 18aa2e8b..39f261a1 100644 --- a/src/main/java/com/talhanation/bannermod/network/BannerModNetworkBootstrap.java +++ b/src/main/java/com/talhanation/bannermod/network/BannerModNetworkBootstrap.java @@ -122,6 +122,7 @@ public class BannerModNetworkBootstrap { MessageOpenGovernorScreen.class, MessageToClientUpdateGovernorScreen.class, MessageUpdateGovernorPolicy.class, + MessageToggleGovernorAutoManage.class, MessageDebugGui.class, MessageDebugScreen.class, MessageSelectRecruits.class, diff --git a/src/main/java/com/talhanation/bannermod/network/messages/military/MessageToClientUpdateGovernorScreen.java b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageToClientUpdateGovernorScreen.java index 9255acf9..9d0b8ae5 100644 --- a/src/main/java/com/talhanation/bannermod/network/messages/military/MessageToClientUpdateGovernorScreen.java +++ b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageToClientUpdateGovernorScreen.java @@ -25,6 +25,7 @@ public class MessageToClientUpdateGovernorScreen implements Message incidents; private List recommendations; @@ -45,6 +46,7 @@ public MessageToClientUpdateGovernorScreen(UUID recruit, int treasuryBalance, int lastTreasuryNet, int projectedTreasuryBalance, + boolean autoManage, List incidents, List recommendations) { this.recruit = recruit; @@ -61,6 +63,7 @@ public MessageToClientUpdateGovernorScreen(UUID recruit, this.treasuryBalance = treasuryBalance; this.lastTreasuryNet = lastTreasuryNet; this.projectedTreasuryBalance = projectedTreasuryBalance; + this.autoManage = autoManage; this.incidents = incidents; this.recommendations = recommendations; } @@ -75,7 +78,7 @@ public void executeClientSide(NetworkEvent.Context context) { GovernorScreen.applyUpdate(recruit, settlementStatus, citizenCount, taxesDue, taxesCollected, lastHeartbeatTick, garrisonRecommendation, fortificationRecommendation, garrisonPriority, fortificationPriority, taxPressure, treasuryBalance, lastTreasuryNet, projectedTreasuryBalance, - incidents, recommendations); + autoManage, incidents, recommendations); } @Override @@ -94,6 +97,7 @@ public MessageToClientUpdateGovernorScreen fromBytes(FriendlyByteBuf buf) { this.treasuryBalance = buf.readInt(); this.lastTreasuryNet = buf.readInt(); this.projectedTreasuryBalance = buf.readInt(); + this.autoManage = buf.readBoolean(); this.incidents = new ArrayList<>(buf.readList(FriendlyByteBuf::readUtf)); this.recommendations = new ArrayList<>(buf.readList(FriendlyByteBuf::readUtf)); return this; @@ -115,6 +119,7 @@ public void toBytes(FriendlyByteBuf buf) { buf.writeInt(this.treasuryBalance); buf.writeInt(this.lastTreasuryNet); buf.writeInt(this.projectedTreasuryBalance); + buf.writeBoolean(this.autoManage); buf.writeCollection(this.incidents, FriendlyByteBuf::writeUtf); buf.writeCollection(this.recommendations, FriendlyByteBuf::writeUtf); } diff --git a/src/main/java/com/talhanation/bannermod/network/messages/military/MessageToggleGovernorAutoManage.java b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageToggleGovernorAutoManage.java new file mode 100644 index 00000000..5c72f8f5 --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageToggleGovernorAutoManage.java @@ -0,0 +1,52 @@ +package com.talhanation.bannermod.network.messages.military; + +import com.talhanation.bannermod.events.RecruitEvents; +import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; +import de.maxhenkel.corelib.net.Message; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.network.NetworkEvent; + +import java.util.Objects; +import java.util.UUID; + +public class MessageToggleGovernorAutoManage implements Message { + private UUID recruit; + private boolean autoManage; + + public MessageToggleGovernorAutoManage() { + } + + public MessageToggleGovernorAutoManage(UUID recruit, boolean autoManage) { + this.recruit = recruit; + this.autoManage = autoManage; + } + + @Override + public Dist getExecutingSide() { + return Dist.DEDICATED_SERVER; + } + + @Override + public void executeServerSide(NetworkEvent.Context context) { + ServerPlayer player = Objects.requireNonNull(context.getSender()); + AbstractRecruitEntity recruitEntity = RecruitMessageEntityResolver.resolveRecruitInInflatedBox(player, this.recruit, 16.0D); + if (recruitEntity != null) { + RecruitEvents.updateGovernorAutoManage(player, recruitEntity, this.autoManage); + } + } + + @Override + public MessageToggleGovernorAutoManage fromBytes(FriendlyByteBuf buf) { + this.recruit = buf.readUUID(); + this.autoManage = buf.readBoolean(); + return this; + } + + @Override + public void toBytes(FriendlyByteBuf buf) { + buf.writeUUID(this.recruit); + buf.writeBoolean(this.autoManage); + } +} From 1e64701fc3cbd6efba533cf63f8bd6bfcf9ce670 Mon Sep 17 00:00:00 2001 From: Mr-Artillery Date: Sun, 26 Apr 2026 13:04:09 -0400 Subject: [PATCH 2/3] feat(governor): contract system, 1-to-1 enforcement, and hybrid auto-posting Contract system (hybrid: Governor auto-posts + owner can pin/cancel): - BannerModGovernorContractType: SUPPLY_RESOURCES, CLEAR_HOSTILES, RECRUIT_WORKERS - BannerModGovernorContractStatus: OPEN, ACCEPTED, COMPLETED, CANCELLED, EXPIRED - BannerModGovernorContract: immutable record with full NBT round-trip - BannerModContractManager: SavedData keyed by contractId, per-claim queries, expiry sweep - BannerModContractPolicy: reward = min(base * urgency_mult, min(ownerCap, serverCap)); autoPost() maps heartbeat incidents to contract types, skips existing types, caps at max - BannerModGovernorContractConfig: server config for base rewards, cap, max-open, deadline Snapshot: add maxContractReward field (0 = use server default); all with* methods updated; withMaxContractReward() clamps to >= 0; NBT backward-compatible (missing key -> 0) Governor service: - 1-to-1 enforcement: assignGovernor auto-revokes from old claim before assigning to new - updateMaxContractReward(): same authority + rules guards as other policy operations Heartbeat: runGovernedClaimHeartbeatBatch gains optional BannerModContractManager param; calls expireOldContracts + BannerModContractPolicy.autoPost after each snapshot update ClaimEvents: passes BannerModContractManager.get(level) to heartbeat batch Network (6 new messages registered in bootstrap): MessageOpenContractBoard, MessageToClientUpdateContractBoard (ContractDto inner record), MessageAcceptContract, MessageCancelContract, MessagePinContract, MessageSetContractMaxReward Server handlers wired: RecruitEvents -> RecruitGovernorWorkflow -> openContractBoard, acceptContract, cancelContract, pinContract, setContractMaxReward Client: GovernorContractScreen (non-container screen, opened by packet) shows contract list with Accept/Cancel/Pin buttons; EditBox for owner max-reward cap with Set button GovernorScreen: add [Contracts] button that sends MessageOpenContractBoard Config: BannerModGovernorContractConfig registered in BannerModMain --- .../bannermod/bootstrap/BannerModMain.java | 2 + .../military/gui/GovernorContractScreen.java | 169 ++++++++++++ .../client/military/gui/GovernorScreen.java | 4 + .../BannerModGovernorContractConfig.java | 45 ++++ .../bannermod/events/ClaimEvents.java | 2 + .../bannermod/events/RecruitEvents.java | 20 ++ .../events/RecruitGovernorWorkflow.java | 84 ++++++ .../governance/BannerModContractManager.java | 81 ++++++ .../governance/BannerModContractPolicy.java | 103 ++++++++ .../governance/BannerModGovernorContract.java | 72 ++++++ .../BannerModGovernorContractStatus.java | 13 + .../BannerModGovernorContractType.java | 28 ++ .../BannerModGovernorHeartbeat.java | 13 +- .../governance/BannerModGovernorService.java | 30 +++ .../governance/BannerModGovernorSnapshot.java | 244 ++++++------------ .../network/BannerModNetworkBootstrap.java | 6 + .../military/MessageAcceptContract.java | 52 ++++ .../military/MessageCancelContract.java | 52 ++++ .../military/MessageOpenContractBoard.java | 48 ++++ .../messages/military/MessagePinContract.java | 56 ++++ .../military/MessageSetContractMaxReward.java | 52 ++++ .../MessageToClientUpdateContractBoard.java | 92 +++++++ 22 files changed, 1097 insertions(+), 171 deletions(-) create mode 100644 src/main/java/com/talhanation/bannermod/client/military/gui/GovernorContractScreen.java create mode 100644 src/main/java/com/talhanation/bannermod/config/BannerModGovernorContractConfig.java create mode 100644 src/main/java/com/talhanation/bannermod/governance/BannerModContractManager.java create mode 100644 src/main/java/com/talhanation/bannermod/governance/BannerModContractPolicy.java create mode 100644 src/main/java/com/talhanation/bannermod/governance/BannerModGovernorContract.java create mode 100644 src/main/java/com/talhanation/bannermod/governance/BannerModGovernorContractStatus.java create mode 100644 src/main/java/com/talhanation/bannermod/governance/BannerModGovernorContractType.java create mode 100644 src/main/java/com/talhanation/bannermod/network/messages/military/MessageAcceptContract.java create mode 100644 src/main/java/com/talhanation/bannermod/network/messages/military/MessageCancelContract.java create mode 100644 src/main/java/com/talhanation/bannermod/network/messages/military/MessageOpenContractBoard.java create mode 100644 src/main/java/com/talhanation/bannermod/network/messages/military/MessagePinContract.java create mode 100644 src/main/java/com/talhanation/bannermod/network/messages/military/MessageSetContractMaxReward.java create mode 100644 src/main/java/com/talhanation/bannermod/network/messages/military/MessageToClientUpdateContractBoard.java diff --git a/src/main/java/com/talhanation/bannermod/bootstrap/BannerModMain.java b/src/main/java/com/talhanation/bannermod/bootstrap/BannerModMain.java index 1071de91..13992ef8 100644 --- a/src/main/java/com/talhanation/bannermod/bootstrap/BannerModMain.java +++ b/src/main/java/com/talhanation/bannermod/bootstrap/BannerModMain.java @@ -73,6 +73,8 @@ public BannerModMain() { ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, WorkersServerConfig.SERVER, "bannermod-workers-server.toml"); // Register war/RP config — own filename to avoid the SERVER filename collision called out above. ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, WarServerConfig.SERVER, "bannermod-war-server.toml"); + // Register governor contract config. + ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, com.talhanation.bannermod.config.BannerModGovernorContractConfig.SERVER, "bannermod-governor-contracts-server.toml"); // Lifecycle modEventBus.addListener(this::setup); diff --git a/src/main/java/com/talhanation/bannermod/client/military/gui/GovernorContractScreen.java b/src/main/java/com/talhanation/bannermod/client/military/gui/GovernorContractScreen.java new file mode 100644 index 00000000..b2c7fa88 --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/client/military/gui/GovernorContractScreen.java @@ -0,0 +1,169 @@ +package com.talhanation.bannermod.client.military.gui; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.talhanation.bannermod.bootstrap.BannerModMain; +import com.talhanation.bannermod.governance.BannerModContractPolicy; +import com.talhanation.bannermod.network.messages.military.MessageAcceptContract; +import com.talhanation.bannermod.network.messages.military.MessageCancelContract; +import com.talhanation.bannermod.network.messages.military.MessagePinContract; +import com.talhanation.bannermod.network.messages.military.MessageSetContractMaxReward; +import com.talhanation.bannermod.network.messages.military.MessageToClientUpdateContractBoard; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.client.gui.widget.ExtendedButton; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class GovernorContractScreen extends Screen { + private static final ResourceLocation TEXTURE = new ResourceLocation(BannerModMain.MOD_ID, "textures/gui/professions/blank_gui.png"); + + private static ContractBoardState latestState = ContractBoardState.empty(); + + private final int imageWidth = 260; + private final int imageHeight = 240; + private int leftPos; + private int topPos; + + private EditBox maxRewardBox; + + public GovernorContractScreen() { + super(Component.literal("Contract Board")); + } + + public static void applyUpdate(UUID governorRecruitId, UUID claimUuid, boolean isOwner, + int maxContractReward, long currentTick, + List contracts) { + latestState = new ContractBoardState(governorRecruitId, claimUuid, isOwner, maxContractReward, currentTick, + new ArrayList<>(contracts)); + Minecraft mc = Minecraft.getInstance(); + if (!(mc.screen instanceof GovernorContractScreen)) { + mc.setScreen(new GovernorContractScreen()); + } + } + + @Override + protected void init() { + this.leftPos = (this.width - imageWidth) / 2; + this.topPos = (this.height - imageHeight) / 2; + + ContractBoardState state = latestState; + + if (state.isOwner) { + maxRewardBox = new EditBox(this.font, leftPos + 130, topPos + 14, 60, 12, + Component.literal("max reward")); + maxRewardBox.setMaxLength(6); + maxRewardBox.setValue(state.maxContractReward > 0 ? String.valueOf(state.maxContractReward) : ""); + addRenderableWidget(maxRewardBox); + + addRenderableWidget(new ExtendedButton(leftPos + 195, topPos + 12, 50, 14, + Component.literal("Set"), + btn -> commitMaxReward())); + } + + int yBase = topPos + 40; + for (int i = 0; i < state.contracts.size() && i < 6; i++) { + MessageToClientUpdateContractBoard.ContractDto dto = state.contracts.get(i); + int rowY = yBase + i * 32; + int col = leftPos + 6; + + if (state.isOwner) { + addRenderableWidget(new ExtendedButton(col + 200, rowY + 2, 50, 12, + Component.literal(dto.pinned() ? "Unpin" : "Pin"), + btn -> sendPin(dto.contractId(), !dto.pinned()))); + if ("open".equalsIgnoreCase(dto.status())) { + addRenderableWidget(new ExtendedButton(col + 200, rowY + 16, 50, 12, + Component.literal("Cancel"), + btn -> sendCancel(dto.contractId()))); + } + } else if ("open".equalsIgnoreCase(dto.status()) && !dto.hasAcceptor()) { + addRenderableWidget(new ExtendedButton(col + 200, rowY + 8, 50, 14, + Component.literal("Accept"), + btn -> sendAccept(dto.contractId()))); + } + } + } + + private void commitMaxReward() { + if (maxRewardBox == null) return; + try { + int value = Integer.parseInt(maxRewardBox.getValue().trim()); + BannerModMain.SIMPLE_CHANNEL.sendToServer( + new MessageSetContractMaxReward(latestState.governorRecruitId, Math.max(0, value))); + } catch (NumberFormatException ignored) { + } + } + + private void sendAccept(UUID contractId) { + BannerModMain.SIMPLE_CHANNEL.sendToServer( + new MessageAcceptContract(latestState.governorRecruitId, contractId)); + } + + private void sendCancel(UUID contractId) { + BannerModMain.SIMPLE_CHANNEL.sendToServer( + new MessageCancelContract(latestState.governorRecruitId, contractId)); + } + + private void sendPin(UUID contractId, boolean pin) { + BannerModMain.SIMPLE_CHANNEL.sendToServer( + new MessagePinContract(latestState.governorRecruitId, contractId, pin)); + } + + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) { + renderBackground(guiGraphics); + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderColor(1F, 1F, 1F, 1F); + RenderSystem.setShaderTexture(0, TEXTURE); + guiGraphics.blit(TEXTURE, leftPos, topPos, 0, 0, imageWidth, imageHeight); + + ContractBoardState state = latestState; + int x = leftPos + 6; + int y = topPos + 6; + + guiGraphics.drawString(font, Component.literal("Contract Board — " + state.claimUuid.toString().substring(0, 8) + "…"), x, y, 0x4E3320, false); + + if (state.isOwner) { + guiGraphics.drawString(font, Component.literal("Max reward cap: "), x, topPos + 16, 4210752, false); + } + + if (state.contracts.isEmpty()) { + guiGraphics.drawString(font, Component.literal("No contracts posted yet."), x, topPos + 50, 4210752, false); + } + + int yBase = topPos + 40; + for (int i = 0; i < state.contracts.size() && i < 6; i++) { + MessageToClientUpdateContractBoard.ContractDto dto = state.contracts.get(i); + int rowY = yBase + i * 32; + guiGraphics.fill(leftPos + 4, rowY, leftPos + imageWidth - 4, rowY + 30, 0x22000000); + guiGraphics.drawString(font, Component.literal(dto.type().replace('_', ' ')), x + 2, rowY + 2, 0x205020, false); + guiGraphics.drawString(font, Component.literal(BannerModContractPolicy.contractDescription( + com.talhanation.bannermod.governance.BannerModGovernorContractType.fromToken(dto.type()) + )), x + 2, rowY + 12, 4210752, false); + String deadline = BannerModContractPolicy.deadlineLabel(dto.deadlineTick(), state.currentTick, dto.pinned()); + guiGraphics.drawString(font, Component.literal("Reward: " + dto.reward() + " | " + dto.status() + " | " + deadline), + x + 2, rowY + 20, 0x666666, false); + } + + super.render(guiGraphics, mouseX, mouseY, delta); + } + + @Override + public boolean isPauseScreen() { + return false; + } + + private record ContractBoardState(UUID governorRecruitId, UUID claimUuid, boolean isOwner, + int maxContractReward, long currentTick, + List contracts) { + static ContractBoardState empty() { + return new ContractBoardState(new UUID(0, 0), new UUID(0, 0), false, 0, 0L, List.of()); + } + } +} diff --git a/src/main/java/com/talhanation/bannermod/client/military/gui/GovernorScreen.java b/src/main/java/com/talhanation/bannermod/client/military/gui/GovernorScreen.java index d621517d..e3cd773c 100644 --- a/src/main/java/com/talhanation/bannermod/client/military/gui/GovernorScreen.java +++ b/src/main/java/com/talhanation/bannermod/client/military/gui/GovernorScreen.java @@ -6,6 +6,7 @@ import com.talhanation.bannermod.governance.BannerModGovernorAdvisory; import com.talhanation.bannermod.governance.BannerModGovernorPolicy; import com.talhanation.bannermod.inventory.military.GovernorContainer; +import com.talhanation.bannermod.network.messages.military.MessageOpenContractBoard; import com.talhanation.bannermod.network.messages.military.MessageOpenGovernorScreen; import com.talhanation.bannermod.network.messages.military.MessageToggleGovernorAutoManage; import com.talhanation.bannermod.network.messages.military.MessageUpdateGovernorPolicy; @@ -48,6 +49,9 @@ protected void init() { addRenderableWidget(new ExtendedButton(this.leftPos + 150, this.topPos + 160, 80, 16, Component.literal(autoManageLabel()), button -> toggleAutoManage(button))); + addRenderableWidget(new ExtendedButton(this.leftPos + 150, this.topPos + 180, 80, 16, + Component.literal("[Contracts]"), + button -> BannerModMain.SIMPLE_CHANNEL.sendToServer(new MessageOpenContractBoard(this.recruit.getUUID())))); BannerModMain.SIMPLE_CHANNEL.sendToServer(new MessageOpenGovernorScreen(this.recruit.getUUID(), false)); } diff --git a/src/main/java/com/talhanation/bannermod/config/BannerModGovernorContractConfig.java b/src/main/java/com/talhanation/bannermod/config/BannerModGovernorContractConfig.java new file mode 100644 index 00000000..99b82fbb --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/config/BannerModGovernorContractConfig.java @@ -0,0 +1,45 @@ +package com.talhanation.bannermod.config; + +import net.minecraftforge.common.ForgeConfigSpec; + +public class BannerModGovernorContractConfig { + private static final ForgeConfigSpec.Builder BUILDER = new ForgeConfigSpec.Builder(); + public static final ForgeConfigSpec SERVER; + + public static final ForgeConfigSpec.IntValue DEFAULT_MAX_REWARD; + public static final ForgeConfigSpec.IntValue SUPPLY_RESOURCES_BASE_REWARD; + public static final ForgeConfigSpec.IntValue CLEAR_HOSTILES_BASE_REWARD; + public static final ForgeConfigSpec.IntValue RECRUIT_WORKERS_BASE_REWARD; + public static final ForgeConfigSpec.IntValue MAX_OPEN_CONTRACTS_PER_CLAIM; + public static final ForgeConfigSpec.LongValue CONTRACT_DEADLINE_TICKS; + + static { + BUILDER.comment("BannerMod Governor Contract System configuration"); + + DEFAULT_MAX_REWARD = BUILDER + .comment("Server default max reward cap when the claim owner has not set one (treasury units)") + .defineInRange("DefaultMaxReward", 100, 1, 10000); + + SUPPLY_RESOURCES_BASE_REWARD = BUILDER + .comment("Base reward paid for completing a supply-resources contract") + .defineInRange("SupplyResourcesBaseReward", 20, 1, 10000); + + CLEAR_HOSTILES_BASE_REWARD = BUILDER + .comment("Base reward paid for clearing hostile mobs or NPCs from a claim") + .defineInRange("ClearHostilesBaseReward", 50, 1, 10000); + + RECRUIT_WORKERS_BASE_REWARD = BUILDER + .comment("Base reward paid for a worker-recruitment contract") + .defineInRange("RecruitWorkersBaseReward", 30, 1, 10000); + + MAX_OPEN_CONTRACTS_PER_CLAIM = BUILDER + .comment("Maximum number of OPEN contracts allowed per claim at the same time") + .defineInRange("MaxOpenContractsPerClaim", 5, 1, 20); + + CONTRACT_DEADLINE_TICKS = BUILDER + .comment("How many ticks before an unpinned open contract expires (72000 = 3 in-game days)") + .defineInRange("ContractDeadlineTicks", 72000L, 1L, Long.MAX_VALUE); + + SERVER = BUILDER.build(); + } +} diff --git a/src/main/java/com/talhanation/bannermod/events/ClaimEvents.java b/src/main/java/com/talhanation/bannermod/events/ClaimEvents.java index 2ce5cc6c..78e89fa8 100644 --- a/src/main/java/com/talhanation/bannermod/events/ClaimEvents.java +++ b/src/main/java/com/talhanation/bannermod/events/ClaimEvents.java @@ -1,6 +1,7 @@ package com.talhanation.bannermod.events; import com.talhanation.bannermod.army.command.CommandIntentQueueRuntime; +import com.talhanation.bannermod.governance.BannerModContractManager; import com.talhanation.bannermod.governance.BannerModGovernorHeartbeat; import com.talhanation.bannermod.governance.BannerModGovernorManager; import com.talhanation.bannermod.governance.BannerModTreasuryManager; @@ -133,6 +134,7 @@ private void tickGovernorMaintenance(ServerLevel level) { recruitsClaimManager, governorManager, BannerModTreasuryManager.get(level), + BannerModContractManager.get(level), governorMaintenanceCursor, GOVERNOR_HEARTBEAT_BATCH_SIZE ); diff --git a/src/main/java/com/talhanation/bannermod/events/RecruitEvents.java b/src/main/java/com/talhanation/bannermod/events/RecruitEvents.java index cfd67265..3eb04d2c 100644 --- a/src/main/java/com/talhanation/bannermod/events/RecruitEvents.java +++ b/src/main/java/com/talhanation/bannermod/events/RecruitEvents.java @@ -139,6 +139,26 @@ public static void updateGovernorAutoManage(ServerPlayer player, AbstractRecruit RecruitGovernorWorkflow.updateGovernorAutoManage(player, recruit, autoManage); } + public static void openContractBoard(ServerPlayer player, AbstractRecruitEntity recruit) { + RecruitGovernorWorkflow.openContractBoard(player, recruit); + } + + public static void acceptContract(ServerPlayer player, AbstractRecruitEntity recruit, java.util.UUID contractId) { + RecruitGovernorWorkflow.acceptContract(player, recruit, contractId); + } + + public static void cancelContract(ServerPlayer player, AbstractRecruitEntity recruit, java.util.UUID contractId) { + RecruitGovernorWorkflow.cancelContract(player, recruit, contractId); + } + + public static void pinContract(ServerPlayer player, AbstractRecruitEntity recruit, java.util.UUID contractId, boolean pinned) { + RecruitGovernorWorkflow.pinContract(player, recruit, contractId, pinned); + } + + public static void setContractMaxReward(ServerPlayer player, AbstractRecruitEntity recruit, int maxReward) { + RecruitGovernorWorkflow.setContractMaxReward(player, recruit, maxReward); + } + public static void handleGroupBackwardCompatibility(AbstractRecruitEntity recruit, int oldGroupNumber) { RecruitWorldLifecycleService.handleLegacyGroup(recruit, oldGroupNumber, server, recruitsGroupsManager); } diff --git a/src/main/java/com/talhanation/bannermod/events/RecruitGovernorWorkflow.java b/src/main/java/com/talhanation/bannermod/events/RecruitGovernorWorkflow.java index d67aab1f..2a44a92d 100644 --- a/src/main/java/com/talhanation/bannermod/events/RecruitGovernorWorkflow.java +++ b/src/main/java/com/talhanation/bannermod/events/RecruitGovernorWorkflow.java @@ -2,7 +2,10 @@ import com.talhanation.bannermod.bootstrap.BannerModMain; import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; +import com.talhanation.bannermod.governance.BannerModContractManager; import com.talhanation.bannermod.governance.BannerModGovernorAuthority; +import com.talhanation.bannermod.governance.BannerModGovernorContract; +import com.talhanation.bannermod.governance.BannerModGovernorContractStatus; import com.talhanation.bannermod.governance.BannerModGovernorManager; import com.talhanation.bannermod.governance.BannerModGovernorPolicy; import com.talhanation.bannermod.governance.BannerModGovernorRecommendation; @@ -10,6 +13,7 @@ import com.talhanation.bannermod.governance.BannerModGovernorSnapshot; import com.talhanation.bannermod.inventory.military.GovernorContainer; import com.talhanation.bannermod.network.messages.military.MessageOpenGovernorScreen; +import com.talhanation.bannermod.network.messages.military.MessageToClientUpdateContractBoard; import com.talhanation.bannermod.network.messages.military.MessageToClientUpdateGovernorScreen; import com.talhanation.bannermod.persistence.military.RecruitsClaim; import com.talhanation.bannermod.shared.settlement.BannerModSettlementBinding; @@ -150,6 +154,86 @@ private static RecruitsClaim resolveClaim(AbstractRecruitEntity recruit) { : ClaimEvents.recruitsClaimManager.getClaim(new ChunkPos(recruit.blockPosition())); } + static void openContractBoard(ServerPlayer player, AbstractRecruitEntity recruit) { + if (!(recruit.getCommandSenderWorld() instanceof ServerLevel serverLevel)) return; + RecruitsClaim claim = resolveClaim(recruit); + if (claim == null) { + player.sendSystemMessage(Component.literal("No claim found at governor's position.")); + return; + } + BannerModGovernorSnapshot snapshot = governorService(serverLevel).getOrCreateGovernorSnapshot(claim); + boolean isOwner = player.getUUID().equals(snapshot.governorOwnerUuid()); + BannerModContractManager contractManager = BannerModContractManager.get(serverLevel); + java.util.List dtos = contractManager + .getContractsForClaim(claim.getUUID()).stream() + .map(c -> new MessageToClientUpdateContractBoard.ContractDto( + c.contractId(), c.type().token(), c.calculatedReward(), + c.status().name().toLowerCase(), c.deadlineTick(), c.pinned(), + c.acceptedByUuid() != null)) + .collect(java.util.stream.Collectors.toList()); + BannerModMain.SIMPLE_CHANNEL.send(PacketDistributor.PLAYER.with(() -> player), + new MessageToClientUpdateContractBoard(recruit.getUUID(), claim.getUUID(), + isOwner, snapshot.maxContractReward(), serverLevel.getGameTime(), dtos)); + } + + static void acceptContract(ServerPlayer player, AbstractRecruitEntity recruit, java.util.UUID contractId) { + if (!(recruit.getCommandSenderWorld() instanceof ServerLevel serverLevel)) return; + RecruitsClaim claim = resolveClaim(recruit); + if (claim == null) return; + BannerModContractManager contractManager = BannerModContractManager.get(serverLevel); + BannerModGovernorContract contract = contractManager.getContract(contractId); + if (contract == null || !contract.isOpen()) { + player.sendSystemMessage(Component.literal("Contract is no longer available.")); + return; + } + contractManager.putContract(contract.withAcceptedBy(player.getUUID())); + player.sendSystemMessage(Component.literal("Contract accepted. Complete the task and return to claim completion.")); + openContractBoard(player, recruit); + } + + static void cancelContract(ServerPlayer player, AbstractRecruitEntity recruit, java.util.UUID contractId) { + if (!(recruit.getCommandSenderWorld() instanceof ServerLevel serverLevel)) return; + RecruitsClaim claim = resolveClaim(recruit); + if (claim == null) return; + BannerModGovernorSnapshot snapshot = governorService(serverLevel).getOrCreateGovernorSnapshot(claim); + if (!player.getUUID().equals(snapshot.governorOwnerUuid()) && !player.hasPermissions(2)) { + player.sendSystemMessage(Component.literal("Only the claim owner can cancel contracts.")); + return; + } + BannerModContractManager contractManager = BannerModContractManager.get(serverLevel); + BannerModGovernorContract contract = contractManager.getContract(contractId); + if (contract != null) { + contractManager.putContract(contract.withStatus(BannerModGovernorContractStatus.CANCELLED)); + } + openContractBoard(player, recruit); + } + + static void pinContract(ServerPlayer player, AbstractRecruitEntity recruit, java.util.UUID contractId, boolean pinned) { + if (!(recruit.getCommandSenderWorld() instanceof ServerLevel serverLevel)) return; + RecruitsClaim claim = resolveClaim(recruit); + if (claim == null) return; + BannerModGovernorSnapshot snapshot = governorService(serverLevel).getOrCreateGovernorSnapshot(claim); + if (!player.getUUID().equals(snapshot.governorOwnerUuid()) && !player.hasPermissions(2)) { + player.sendSystemMessage(Component.literal("Only the claim owner can pin contracts.")); + return; + } + BannerModContractManager contractManager = BannerModContractManager.get(serverLevel); + BannerModGovernorContract contract = contractManager.getContract(contractId); + if (contract != null) contractManager.putContract(contract.withPinned(pinned)); + openContractBoard(player, recruit); + } + + static void setContractMaxReward(ServerPlayer player, AbstractRecruitEntity recruit, int maxReward) { + if (!(recruit.getCommandSenderWorld() instanceof ServerLevel serverLevel)) return; + BannerModGovernorService.OperationResult result = governorService(serverLevel) + .updateMaxContractReward(resolveClaim(recruit), BannerModGovernorAuthority.actor(player), maxReward); + if (!result.allowed()) { + player.sendSystemMessage(Component.literal("Max reward update denied.")); + return; + } + openContractBoard(player, recruit); + } + private static String recommendationLabel(BannerModGovernorSnapshot snapshot, boolean garrison) { if (snapshot == null) { return BannerModGovernorRecommendation.HOLD_COURSE.token(); diff --git a/src/main/java/com/talhanation/bannermod/governance/BannerModContractManager.java b/src/main/java/com/talhanation/bannermod/governance/BannerModContractManager.java new file mode 100644 index 00000000..ed4052d0 --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/governance/BannerModContractManager.java @@ -0,0 +1,81 @@ +package com.talhanation.bannermod.governance; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.saveddata.SavedData; + +import javax.annotation.Nullable; +import java.util.*; +import java.util.stream.Collectors; + +public class BannerModContractManager extends SavedData { + private static final String FILE_ID = "bannermodContracts"; + + private final Map contracts = new LinkedHashMap<>(); + + public static BannerModContractManager get(ServerLevel level) { + return level.getDataStorage().computeIfAbsent(BannerModContractManager::load, BannerModContractManager::new, FILE_ID); + } + + public static BannerModContractManager load(CompoundTag tag) { + BannerModContractManager manager = new BannerModContractManager(); + if (tag.contains("Contracts", Tag.TAG_LIST)) { + ListTag list = tag.getList("Contracts", Tag.TAG_COMPOUND); + for (Tag entry : list) { + BannerModGovernorContract contract = BannerModGovernorContract.fromTag((CompoundTag) entry); + manager.contracts.put(contract.contractId(), contract); + } + } + return manager; + } + + @Override + public CompoundTag save(CompoundTag tag) { + ListTag list = new ListTag(); + for (BannerModGovernorContract c : contracts.values()) { + list.add(c.toTag()); + } + tag.put("Contracts", list); + return tag; + } + + public void putContract(BannerModGovernorContract contract) { + BannerModGovernorContract previous = contracts.put(contract.contractId(), contract); + if (!contract.equals(previous)) setDirty(); + } + + @Nullable + public BannerModGovernorContract getContract(UUID contractId) { + return contracts.get(contractId); + } + + public List getContractsForClaim(UUID claimUuid) { + return contracts.values().stream() + .filter(c -> c.claimUuid().equals(claimUuid)) + .collect(Collectors.toList()); + } + + public List getOpenContractsForClaim(UUID claimUuid) { + return contracts.values().stream() + .filter(c -> c.claimUuid().equals(claimUuid) && c.isOpen()) + .collect(Collectors.toList()); + } + + public Collection getAllContracts() { + return contracts.values(); + } + + public void expireOldContracts(long currentTick) { + boolean changed = false; + for (Map.Entry entry : new ArrayList<>(contracts.entrySet())) { + BannerModGovernorContract c = entry.getValue(); + if (c.isExpired(currentTick)) { + contracts.put(entry.getKey(), c.withStatus(BannerModGovernorContractStatus.EXPIRED)); + changed = true; + } + } + if (changed) setDirty(); + } +} diff --git a/src/main/java/com/talhanation/bannermod/governance/BannerModContractPolicy.java b/src/main/java/com/talhanation/bannermod/governance/BannerModContractPolicy.java new file mode 100644 index 00000000..c3555a99 --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/governance/BannerModContractPolicy.java @@ -0,0 +1,103 @@ +package com.talhanation.bannermod.governance; + +import com.talhanation.bannermod.config.BannerModGovernorContractConfig; + +import javax.annotation.Nullable; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +public final class BannerModContractPolicy { + + private BannerModContractPolicy() { + } + + public static int calculateReward(BannerModGovernorContractType type, int ownerMaxReward, boolean urgent) { + int serverCap = BannerModGovernorContractConfig.DEFAULT_MAX_REWARD.get(); + int effectiveCap = ownerMaxReward > 0 ? Math.min(ownerMaxReward, serverCap) : serverCap; + int base = switch (type) { + case SUPPLY_RESOURCES -> BannerModGovernorContractConfig.SUPPLY_RESOURCES_BASE_REWARD.get(); + case CLEAR_HOSTILES -> BannerModGovernorContractConfig.CLEAR_HOSTILES_BASE_REWARD.get(); + case RECRUIT_WORKERS -> BannerModGovernorContractConfig.RECRUIT_WORKERS_BASE_REWARD.get(); + }; + int raw = urgent ? (int) (base * 1.5) : base; + return Math.min(raw, effectiveCap); + } + + /** + * Auto-posts contracts based on active incidents in the heartbeat report. + * Skips posting if the claim already has an open contract of that type or is at the cap. + * Called from the Governor heartbeat batch after each snapshot is updated. + */ + public static void autoPost(BannerModGovernorSnapshot snapshot, + BannerModGovernorHeartbeat.HeartbeatReport report, + BannerModContractManager contractManager, + long gameTime) { + UUID claimUuid = snapshot.claimUuid(); + int maxOpen = BannerModGovernorContractConfig.MAX_OPEN_CONTRACTS_PER_CLAIM.get(); + + List open = contractManager.getOpenContractsForClaim(claimUuid); + if (open.size() >= maxOpen) return; + + Set existingTypes = new HashSet<>(); + for (BannerModGovernorContract c : open) { + existingTypes.add(c.type()); + } + + int maxReward = snapshot.maxContractReward(); + long deadline = gameTime + BannerModGovernorContractConfig.CONTRACT_DEADLINE_TICKS.get(); + + for (BannerModGovernorIncident incident : report.incidents()) { + BannerModGovernorContractType type = incidentToContractType(incident); + if (type == null || existingTypes.contains(type)) continue; + if (open.size() + existingTypes.size() >= maxOpen) break; + + boolean urgent = incident == BannerModGovernorIncident.UNDER_SIEGE + || incident == BannerModGovernorIncident.HOSTILE_CLAIM; + + BannerModGovernorContract contract = new BannerModGovernorContract( + UUID.randomUUID(), + claimUuid, + type, + calculateReward(type, maxReward, urgent), + maxReward, + null, + gameTime, + deadline, + false, + BannerModGovernorContractStatus.OPEN + ); + contractManager.putContract(contract); + existingTypes.add(type); + } + } + + @Nullable + private static BannerModGovernorContractType incidentToContractType(BannerModGovernorIncident incident) { + return switch (incident) { + case SUPPLY_BLOCKED, WORKER_SHORTAGE -> BannerModGovernorContractType.SUPPLY_RESOURCES; + case HOSTILE_CLAIM, UNDER_SIEGE -> BannerModGovernorContractType.CLEAR_HOSTILES; + case RECRUIT_UPKEEP_BLOCKED -> BannerModGovernorContractType.RECRUIT_WORKERS; + default -> null; + }; + } + + /** Returns a player-readable description of what a contract requires. */ + public static String contractDescription(BannerModGovernorContractType type) { + return switch (type) { + case SUPPLY_RESOURCES -> "Deliver needed resources to the settlement's storage."; + case CLEAR_HOSTILES -> "Clear hostile mobs and enemy NPCs from within the claim."; + case RECRUIT_WORKERS -> "Recruit and assign workers to the settlement's buildings."; + }; + } + + public static String deadlineLabel(long deadlineTick, long currentTick, boolean pinned) { + if (pinned) return "no expiry (pinned)"; + long remaining = deadlineTick - currentTick; + if (remaining <= 0) return "expired"; + long days = remaining / 24000L; + long hours = (remaining % 24000L) / 1000L; + return days > 0 ? days + "d " + hours + "h" : hours + "h"; + } +} diff --git a/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorContract.java b/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorContract.java new file mode 100644 index 00000000..c2c70daa --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorContract.java @@ -0,0 +1,72 @@ +package com.talhanation.bannermod.governance; + +import net.minecraft.nbt.CompoundTag; + +import javax.annotation.Nullable; +import java.util.UUID; + +public record BannerModGovernorContract( + UUID contractId, + UUID claimUuid, + BannerModGovernorContractType type, + int calculatedReward, + int maxReward, + @Nullable UUID acceptedByUuid, + long postedTick, + long deadlineTick, + boolean pinned, + BannerModGovernorContractStatus status +) { + public boolean isOpen() { + return this.status == BannerModGovernorContractStatus.OPEN; + } + + public boolean isExpired(long currentTick) { + return !pinned && currentTick > deadlineTick && status == BannerModGovernorContractStatus.OPEN; + } + + public BannerModGovernorContract withStatus(BannerModGovernorContractStatus newStatus) { + return new BannerModGovernorContract(contractId, claimUuid, type, calculatedReward, maxReward, + acceptedByUuid, postedTick, deadlineTick, pinned, newStatus); + } + + public BannerModGovernorContract withAcceptedBy(UUID playerUuid) { + return new BannerModGovernorContract(contractId, claimUuid, type, calculatedReward, maxReward, + playerUuid, postedTick, deadlineTick, pinned, BannerModGovernorContractStatus.ACCEPTED); + } + + public BannerModGovernorContract withPinned(boolean newPinned) { + return new BannerModGovernorContract(contractId, claimUuid, type, calculatedReward, maxReward, + acceptedByUuid, postedTick, deadlineTick, newPinned, status); + } + + public CompoundTag toTag() { + CompoundTag tag = new CompoundTag(); + tag.putUUID("ContractId", contractId); + tag.putUUID("ClaimUuid", claimUuid); + tag.putString("Type", type.token()); + tag.putInt("CalculatedReward", calculatedReward); + tag.putInt("MaxReward", maxReward); + if (acceptedByUuid != null) tag.putUUID("AcceptedByUuid", acceptedByUuid); + tag.putLong("PostedTick", postedTick); + tag.putLong("DeadlineTick", deadlineTick); + tag.putBoolean("Pinned", pinned); + tag.putString("Status", status.name()); + return tag; + } + + public static BannerModGovernorContract fromTag(CompoundTag tag) { + return new BannerModGovernorContract( + tag.getUUID("ContractId"), + tag.getUUID("ClaimUuid"), + BannerModGovernorContractType.fromToken(tag.getString("Type")), + tag.getInt("CalculatedReward"), + tag.getInt("MaxReward"), + tag.hasUUID("AcceptedByUuid") ? tag.getUUID("AcceptedByUuid") : null, + tag.getLong("PostedTick"), + tag.getLong("DeadlineTick"), + tag.contains("Pinned") && tag.getBoolean("Pinned"), + BannerModGovernorContractStatus.fromName(tag.getString("Status")) + ); + } +} diff --git a/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorContractStatus.java b/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorContractStatus.java new file mode 100644 index 00000000..9b59fb02 --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorContractStatus.java @@ -0,0 +1,13 @@ +package com.talhanation.bannermod.governance; + +public enum BannerModGovernorContractStatus { + OPEN, ACCEPTED, COMPLETED, CANCELLED, EXPIRED; + + public static BannerModGovernorContractStatus fromName(String name) { + try { + return valueOf(name); + } catch (IllegalArgumentException e) { + return OPEN; + } + } +} diff --git a/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorContractType.java b/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorContractType.java new file mode 100644 index 00000000..699445d7 --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorContractType.java @@ -0,0 +1,28 @@ +package com.talhanation.bannermod.governance; + +public enum BannerModGovernorContractType { + SUPPLY_RESOURCES("supply_resources"), + CLEAR_HOSTILES("clear_hostiles"), + RECRUIT_WORKERS("recruit_workers"); + + private final String token; + + BannerModGovernorContractType(String token) { + this.token = token; + } + + public String token() { + return this.token; + } + + public String displayName() { + return this.token.replace('_', ' '); + } + + public static BannerModGovernorContractType fromToken(String token) { + for (BannerModGovernorContractType type : values()) { + if (type.token.equals(token)) return type; + } + return SUPPLY_RESOURCES; + } +} diff --git a/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorHeartbeat.java b/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorHeartbeat.java index 430498be..83418da7 100644 --- a/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorHeartbeat.java +++ b/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorHeartbeat.java @@ -120,13 +120,14 @@ public static void runGovernedClaimHeartbeat(ServerLevel level, RecruitsClaimManager claimManager, BannerModGovernorManager governorManager, @Nullable BannerModTreasuryManager treasuryManager) { - runGovernedClaimHeartbeatBatch(level, claimManager, governorManager, treasuryManager, 0, Integer.MAX_VALUE); + runGovernedClaimHeartbeatBatch(level, claimManager, governorManager, treasuryManager, null, 0, Integer.MAX_VALUE); } public static BatchResult runGovernedClaimHeartbeatBatch(ServerLevel level, RecruitsClaimManager claimManager, BannerModGovernorManager governorManager, @Nullable BannerModTreasuryManager treasuryManager, + @Nullable BannerModContractManager contractManager, int startIndex, int maxSnapshots) { if (level == null || claimManager == null || governorManager == null) { @@ -200,7 +201,7 @@ public static BatchResult runGovernedClaimHeartbeatBatch(ServerLevel level, recruitSupplyStatus ); - governorManager.putSnapshot(snapshot.withHeartbeatReport( + BannerModGovernorSnapshot updated = snapshot.withHeartbeatReport( report.heartbeatTick(), report.collectionTick(), report.citizenCount(), @@ -208,7 +209,13 @@ public static BatchResult runGovernedClaimHeartbeatBatch(ServerLevel level, report.taxesCollected(), incidentTokens(report.incidents()), recommendationTokens(report.recommendations()) - ).withFiscalRollup(fiscalRollup)); + ).withFiscalRollup(fiscalRollup); + governorManager.putSnapshot(updated); + + if (contractManager != null) { + contractManager.expireOldContracts(level.getGameTime()); + BannerModContractPolicy.autoPost(updated, report, contractManager, level.getGameTime()); + } } return recordBatchResult("settlement.heartbeat.governor_batch", new BatchResult(clampedStart, endIndex, total, endIndex >= total), startNanos); diff --git a/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorService.java b/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorService.java index 15da0fa9..4316fb37 100644 --- a/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorService.java +++ b/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorService.java @@ -43,6 +43,17 @@ public OperationResult assignGovernor(@Nullable RecruitsClaim claim, return new OperationResult(false, authorityDecision, governorDecision, claim == null ? null : this.manager.getSnapshot(claim.getUUID())); } + // 1-to-1: if this recruit is already governing another claim, revoke from it first. + if (recruit != null) { + for (BannerModGovernorSnapshot existing : this.manager.getAllSnapshots()) { + if (existing != null + && recruit.recruitUuid().equals(existing.governorRecruitUuid()) + && !existing.claimUuid().equals(claim.getUUID())) { + this.manager.putSnapshot(existing.withGovernor(null, null)); + } + } + } + BannerModGovernorSnapshot snapshot = getOrCreateSnapshot(claim, settlementFactionId); BannerModGovernorSnapshot updated = snapshot .withSettlementFactionId(normalizeFactionId(settlementFactionId)) @@ -138,6 +149,25 @@ public OperationResult updatePolicy(@Nullable RecruitsClaim claim, return new OperationResult(true, authorityDecision, governorDecision, updated); } + public OperationResult updateMaxContractReward(@Nullable RecruitsClaim claim, + BannerModGovernorAuthority.ActorContext actor, + int maxReward) { + BannerModGovernorSnapshot snapshot = claim == null ? null : getOrCreateGovernorSnapshot(claim); + BannerModGovernorAuthority.Decision authorityDecision = BannerModGovernorAuthority.revokeDecision( + actor, + snapshot == null ? null : snapshot.governorOwnerUuid(), + snapshot == null ? null : snapshot.settlementFactionId() + ); + BannerModSettlementBinding.Binding binding = resolveBinding(claim, snapshot == null ? null : snapshot.settlementFactionId(), true); + BannerModGovernorRules.Decision governorDecision = BannerModGovernorRules.controlDecision(binding, snapshot); + if (!BannerModGovernorAuthority.isAllowed(authorityDecision) || !BannerModGovernorRules.isAllowed(governorDecision) || snapshot == null) { + return new OperationResult(false, authorityDecision, governorDecision, snapshot); + } + BannerModGovernorSnapshot updated = snapshot.withMaxContractReward(maxReward); + this.manager.putSnapshot(updated); + return new OperationResult(true, authorityDecision, governorDecision, updated); + } + public OperationResult updateAutoManage(@Nullable RecruitsClaim claim, BannerModGovernorAuthority.ActorContext actor, boolean autoManage) { diff --git a/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorSnapshot.java b/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorSnapshot.java index 234a349a..7bfc77f0 100644 --- a/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorSnapshot.java +++ b/src/main/java/com/talhanation/bannermod/governance/BannerModGovernorSnapshot.java @@ -31,6 +31,7 @@ public record BannerModGovernorSnapshot( int fortificationPriority, int taxPressure, boolean autoManage, + int maxContractReward, List incidentTokens, List recommendationTokens ) { @@ -49,101 +50,58 @@ public boolean hasGovernor() { public BannerModGovernorSnapshot withGovernor(@Nullable UUID governorRecruitUuid, @Nullable UUID governorOwnerUuid) { return new BannerModGovernorSnapshot( - this.claimUuid, - this.anchorChunkX, - this.anchorChunkZ, - this.settlementFactionId, - governorRecruitUuid, - governorOwnerUuid, - this.lastHeartbeatTick, - this.lastCollectionTick, - this.citizenCount, - this.taxesDue, - this.taxesCollected, - this.treasuryBalance, - this.lastTreasuryNet, - this.projectedTreasuryBalance, - this.garrisonPriority, - this.fortificationPriority, - this.taxPressure, - this.autoManage, - this.incidentTokens, - this.recommendationTokens + claimUuid, anchorChunkX, anchorChunkZ, settlementFactionId, + governorRecruitUuid, governorOwnerUuid, + lastHeartbeatTick, lastCollectionTick, citizenCount, taxesDue, taxesCollected, + treasuryBalance, lastTreasuryNet, projectedTreasuryBalance, + garrisonPriority, fortificationPriority, taxPressure, + autoManage, maxContractReward, incidentTokens, recommendationTokens ); } public BannerModGovernorSnapshot withSettlementFactionId(@Nullable String settlementFactionId) { return new BannerModGovernorSnapshot( - this.claimUuid, - this.anchorChunkX, - this.anchorChunkZ, - settlementFactionId, - this.governorRecruitUuid, - this.governorOwnerUuid, - this.lastHeartbeatTick, - this.lastCollectionTick, - this.citizenCount, - this.taxesDue, - this.taxesCollected, - this.treasuryBalance, - this.lastTreasuryNet, - this.projectedTreasuryBalance, - this.garrisonPriority, - this.fortificationPriority, - this.taxPressure, - this.autoManage, - this.incidentTokens, - this.recommendationTokens + claimUuid, anchorChunkX, anchorChunkZ, settlementFactionId, + governorRecruitUuid, governorOwnerUuid, + lastHeartbeatTick, lastCollectionTick, citizenCount, taxesDue, taxesCollected, + treasuryBalance, lastTreasuryNet, projectedTreasuryBalance, + garrisonPriority, fortificationPriority, taxPressure, + autoManage, maxContractReward, incidentTokens, recommendationTokens ); } public BannerModGovernorSnapshot withPolicies(int garrisonPriority, int fortificationPriority, int taxPressure) { return new BannerModGovernorSnapshot( - this.claimUuid, - this.anchorChunkX, - this.anchorChunkZ, - this.settlementFactionId, - this.governorRecruitUuid, - this.governorOwnerUuid, - this.lastHeartbeatTick, - this.lastCollectionTick, - this.citizenCount, - this.taxesDue, - this.taxesCollected, - this.treasuryBalance, - this.lastTreasuryNet, - this.projectedTreasuryBalance, + claimUuid, anchorChunkX, anchorChunkZ, settlementFactionId, + governorRecruitUuid, governorOwnerUuid, + lastHeartbeatTick, lastCollectionTick, citizenCount, taxesDue, taxesCollected, + treasuryBalance, lastTreasuryNet, projectedTreasuryBalance, BannerModGovernorPolicy.GARRISON_PRIORITY.clamp(garrisonPriority), BannerModGovernorPolicy.FORTIFICATION_PRIORITY.clamp(fortificationPriority), BannerModGovernorPolicy.TAX_PRESSURE.clamp(taxPressure), - this.autoManage, - this.incidentTokens, - this.recommendationTokens + autoManage, maxContractReward, incidentTokens, recommendationTokens ); } public BannerModGovernorSnapshot withAutoManage(boolean autoManage) { return new BannerModGovernorSnapshot( - this.claimUuid, - this.anchorChunkX, - this.anchorChunkZ, - this.settlementFactionId, - this.governorRecruitUuid, - this.governorOwnerUuid, - this.lastHeartbeatTick, - this.lastCollectionTick, - this.citizenCount, - this.taxesDue, - this.taxesCollected, - this.treasuryBalance, - this.lastTreasuryNet, - this.projectedTreasuryBalance, - this.garrisonPriority, - this.fortificationPriority, - this.taxPressure, - autoManage, - this.incidentTokens, - this.recommendationTokens + claimUuid, anchorChunkX, anchorChunkZ, settlementFactionId, + governorRecruitUuid, governorOwnerUuid, + lastHeartbeatTick, lastCollectionTick, citizenCount, taxesDue, taxesCollected, + treasuryBalance, lastTreasuryNet, projectedTreasuryBalance, + garrisonPriority, fortificationPriority, taxPressure, + autoManage, maxContractReward, incidentTokens, recommendationTokens + ); + } + + public BannerModGovernorSnapshot withMaxContractReward(int maxContractReward) { + return new BannerModGovernorSnapshot( + claimUuid, anchorChunkX, anchorChunkZ, settlementFactionId, + governorRecruitUuid, governorOwnerUuid, + lastHeartbeatTick, lastCollectionTick, citizenCount, taxesDue, taxesCollected, + treasuryBalance, lastTreasuryNet, projectedTreasuryBalance, + garrisonPriority, fortificationPriority, taxPressure, + autoManage, Math.max(0, maxContractReward), incidentTokens, recommendationTokens ); } @@ -152,107 +110,62 @@ public BannerModGovernorSnapshot withHeartbeatReport(long heartbeatTick, long co List incidentTokens, List recommendationTokens) { return new BannerModGovernorSnapshot( - this.claimUuid, - this.anchorChunkX, - this.anchorChunkZ, - this.settlementFactionId, - this.governorRecruitUuid, - this.governorOwnerUuid, - heartbeatTick, - collectionTick, - citizenCount, - taxesDue, - taxesCollected, - this.treasuryBalance, - this.lastTreasuryNet, - this.projectedTreasuryBalance, - this.garrisonPriority, - this.fortificationPriority, - this.taxPressure, - this.autoManage, - incidentTokens, - recommendationTokens + claimUuid, anchorChunkX, anchorChunkZ, settlementFactionId, + governorRecruitUuid, governorOwnerUuid, + heartbeatTick, collectionTick, citizenCount, taxesDue, taxesCollected, + treasuryBalance, lastTreasuryNet, projectedTreasuryBalance, + garrisonPriority, fortificationPriority, taxPressure, + autoManage, maxContractReward, incidentTokens, recommendationTokens ); } public BannerModGovernorSnapshot withFiscalRollup(@Nullable BannerModTreasuryLedgerSnapshot.FiscalRollup fiscalRollup) { return new BannerModGovernorSnapshot( - this.claimUuid, - this.anchorChunkX, - this.anchorChunkZ, - this.settlementFactionId, - this.governorRecruitUuid, - this.governorOwnerUuid, - this.lastHeartbeatTick, - this.lastCollectionTick, - this.citizenCount, - this.taxesDue, - this.taxesCollected, + claimUuid, anchorChunkX, anchorChunkZ, settlementFactionId, + governorRecruitUuid, governorOwnerUuid, + lastHeartbeatTick, lastCollectionTick, citizenCount, taxesDue, taxesCollected, fiscalRollup == null ? 0 : fiscalRollup.treasuryBalance(), fiscalRollup == null ? 0 : fiscalRollup.lastNetChange(), fiscalRollup == null ? 0 : fiscalRollup.projectedNextBalance(), - this.garrisonPriority, - this.fortificationPriority, - this.taxPressure, - this.autoManage, - this.incidentTokens, - this.recommendationTokens + garrisonPriority, fortificationPriority, taxPressure, + autoManage, maxContractReward, incidentTokens, recommendationTokens ); } public CompoundTag toTag() { CompoundTag tag = new CompoundTag(); - tag.putUUID("ClaimUuid", this.claimUuid); - tag.putInt("AnchorChunkX", this.anchorChunkX); - tag.putInt("AnchorChunkZ", this.anchorChunkZ); - if (this.settlementFactionId != null && !this.settlementFactionId.isBlank()) { - tag.putString("SettlementFactionId", this.settlementFactionId); + tag.putUUID("ClaimUuid", claimUuid); + tag.putInt("AnchorChunkX", anchorChunkX); + tag.putInt("AnchorChunkZ", anchorChunkZ); + if (settlementFactionId != null && !settlementFactionId.isBlank()) { + tag.putString("SettlementFactionId", settlementFactionId); } - if (this.governorRecruitUuid != null) { - tag.putUUID("GovernorRecruitUuid", this.governorRecruitUuid); - } - if (this.governorOwnerUuid != null) { - tag.putUUID("GovernorOwnerUuid", this.governorOwnerUuid); - } - tag.putLong("LastHeartbeatTick", this.lastHeartbeatTick); - tag.putLong("LastCollectionTick", this.lastCollectionTick); - tag.putInt("CitizenCount", this.citizenCount); - tag.putInt("TaxesDue", this.taxesDue); - tag.putInt("TaxesCollected", this.taxesCollected); - tag.putInt("TreasuryBalance", this.treasuryBalance); - tag.putInt("LastTreasuryNet", this.lastTreasuryNet); - tag.putInt("ProjectedTreasuryBalance", this.projectedTreasuryBalance); - tag.putInt("GarrisonPriority", this.garrisonPriority); - tag.putInt("FortificationPriority", this.fortificationPriority); - tag.putInt("TaxPressure", this.taxPressure); - tag.putBoolean("AutoManage", this.autoManage); - tag.put("IncidentTokens", writeTokens(this.incidentTokens)); - tag.put("RecommendationTokens", writeTokens(this.recommendationTokens)); + if (governorRecruitUuid != null) tag.putUUID("GovernorRecruitUuid", governorRecruitUuid); + if (governorOwnerUuid != null) tag.putUUID("GovernorOwnerUuid", governorOwnerUuid); + tag.putLong("LastHeartbeatTick", lastHeartbeatTick); + tag.putLong("LastCollectionTick", lastCollectionTick); + tag.putInt("CitizenCount", citizenCount); + tag.putInt("TaxesDue", taxesDue); + tag.putInt("TaxesCollected", taxesCollected); + tag.putInt("TreasuryBalance", treasuryBalance); + tag.putInt("LastTreasuryNet", lastTreasuryNet); + tag.putInt("ProjectedTreasuryBalance", projectedTreasuryBalance); + tag.putInt("GarrisonPriority", garrisonPriority); + tag.putInt("FortificationPriority", fortificationPriority); + tag.putInt("TaxPressure", taxPressure); + tag.putBoolean("AutoManage", autoManage); + tag.putInt("MaxContractReward", maxContractReward); + tag.put("IncidentTokens", writeTokens(incidentTokens)); + tag.put("RecommendationTokens", writeTokens(recommendationTokens)); return tag; } public static BannerModGovernorSnapshot create(UUID claimUuid, ChunkPos anchorChunk, @Nullable String settlementFactionId) { return new BannerModGovernorSnapshot( - claimUuid, - anchorChunk.x, - anchorChunk.z, - settlementFactionId, - null, - null, - 0L, - 0L, - 0, - 0, - 0, - 0, - 0, - 0, - BannerModGovernorPolicy.DEFAULT_VALUE, - BannerModGovernorPolicy.DEFAULT_VALUE, - BannerModGovernorPolicy.DEFAULT_VALUE, - false, - List.of(), - List.of() + claimUuid, anchorChunk.x, anchorChunk.z, settlementFactionId, + null, null, 0L, 0L, 0, 0, 0, 0, 0, 0, + BannerModGovernorPolicy.DEFAULT_VALUE, BannerModGovernorPolicy.DEFAULT_VALUE, BannerModGovernorPolicy.DEFAULT_VALUE, + false, 0, List.of(), List.of() ); } @@ -280,6 +193,7 @@ public static BannerModGovernorSnapshot fromTag(CompoundTag tag) { tag.contains("FortificationPriority", Tag.TAG_INT) ? tag.getInt("FortificationPriority") : BannerModGovernorPolicy.DEFAULT_VALUE, tag.contains("TaxPressure", Tag.TAG_INT) ? tag.getInt("TaxPressure") : BannerModGovernorPolicy.DEFAULT_VALUE, tag.contains("AutoManage", Tag.TAG_BYTE) && tag.getBoolean("AutoManage"), + tag.contains("MaxContractReward", Tag.TAG_INT) ? tag.getInt("MaxContractReward") : 0, readTokens(tag.getList("IncidentTokens", Tag.TAG_STRING)), readTokens(tag.getList("RecommendationTokens", Tag.TAG_STRING)) ); @@ -287,17 +201,13 @@ public static BannerModGovernorSnapshot fromTag(CompoundTag tag) { private static ListTag writeTokens(List tokens) { ListTag list = new ListTag(); - for (String token : copyTokens(tokens)) { - list.add(StringTag.valueOf(token)); - } + for (String token : copyTokens(tokens)) list.add(StringTag.valueOf(token)); return list; } private static List readTokens(ListTag list) { List tokens = new ArrayList<>(); - for (Tag tag : list) { - tokens.add(tag.getAsString()); - } + for (Tag tag : list) tokens.add(tag.getAsString()); return Collections.unmodifiableList(tokens); } @@ -305,9 +215,7 @@ private static List copyTokens(List tokens) { List copied = new ArrayList<>(); if (tokens != null) { for (String token : tokens) { - if (token != null && !token.isBlank()) { - copied.add(token); - } + if (token != null && !token.isBlank()) copied.add(token); } } return Collections.unmodifiableList(copied); diff --git a/src/main/java/com/talhanation/bannermod/network/BannerModNetworkBootstrap.java b/src/main/java/com/talhanation/bannermod/network/BannerModNetworkBootstrap.java index 39f261a1..05eb65e3 100644 --- a/src/main/java/com/talhanation/bannermod/network/BannerModNetworkBootstrap.java +++ b/src/main/java/com/talhanation/bannermod/network/BannerModNetworkBootstrap.java @@ -123,6 +123,12 @@ public class BannerModNetworkBootstrap { MessageToClientUpdateGovernorScreen.class, MessageUpdateGovernorPolicy.class, MessageToggleGovernorAutoManage.class, + MessageOpenContractBoard.class, + MessageToClientUpdateContractBoard.class, + MessageAcceptContract.class, + MessageCancelContract.class, + MessagePinContract.class, + MessageSetContractMaxReward.class, MessageDebugGui.class, MessageDebugScreen.class, MessageSelectRecruits.class, diff --git a/src/main/java/com/talhanation/bannermod/network/messages/military/MessageAcceptContract.java b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageAcceptContract.java new file mode 100644 index 00000000..7a9cc45e --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageAcceptContract.java @@ -0,0 +1,52 @@ +package com.talhanation.bannermod.network.messages.military; + +import com.talhanation.bannermod.events.RecruitEvents; +import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; +import de.maxhenkel.corelib.net.Message; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.network.NetworkEvent; + +import java.util.Objects; +import java.util.UUID; + +public class MessageAcceptContract implements Message { + private UUID recruit; + private UUID contractId; + + public MessageAcceptContract() { + } + + public MessageAcceptContract(UUID recruit, UUID contractId) { + this.recruit = recruit; + this.contractId = contractId; + } + + @Override + public Dist getExecutingSide() { + return Dist.DEDICATED_SERVER; + } + + @Override + public void executeServerSide(NetworkEvent.Context context) { + ServerPlayer player = Objects.requireNonNull(context.getSender()); + AbstractRecruitEntity recruitEntity = RecruitMessageEntityResolver.resolveRecruitInInflatedBox(player, this.recruit, 32.0D); + if (recruitEntity != null) { + RecruitEvents.acceptContract(player, recruitEntity, this.contractId); + } + } + + @Override + public MessageAcceptContract fromBytes(FriendlyByteBuf buf) { + this.recruit = buf.readUUID(); + this.contractId = buf.readUUID(); + return this; + } + + @Override + public void toBytes(FriendlyByteBuf buf) { + buf.writeUUID(recruit); + buf.writeUUID(contractId); + } +} diff --git a/src/main/java/com/talhanation/bannermod/network/messages/military/MessageCancelContract.java b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageCancelContract.java new file mode 100644 index 00000000..09dfbc35 --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageCancelContract.java @@ -0,0 +1,52 @@ +package com.talhanation.bannermod.network.messages.military; + +import com.talhanation.bannermod.events.RecruitEvents; +import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; +import de.maxhenkel.corelib.net.Message; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.network.NetworkEvent; + +import java.util.Objects; +import java.util.UUID; + +public class MessageCancelContract implements Message { + private UUID recruit; + private UUID contractId; + + public MessageCancelContract() { + } + + public MessageCancelContract(UUID recruit, UUID contractId) { + this.recruit = recruit; + this.contractId = contractId; + } + + @Override + public Dist getExecutingSide() { + return Dist.DEDICATED_SERVER; + } + + @Override + public void executeServerSide(NetworkEvent.Context context) { + ServerPlayer player = Objects.requireNonNull(context.getSender()); + AbstractRecruitEntity recruitEntity = RecruitMessageEntityResolver.resolveRecruitInInflatedBox(player, this.recruit, 16.0D); + if (recruitEntity != null) { + RecruitEvents.cancelContract(player, recruitEntity, this.contractId); + } + } + + @Override + public MessageCancelContract fromBytes(FriendlyByteBuf buf) { + this.recruit = buf.readUUID(); + this.contractId = buf.readUUID(); + return this; + } + + @Override + public void toBytes(FriendlyByteBuf buf) { + buf.writeUUID(recruit); + buf.writeUUID(contractId); + } +} diff --git a/src/main/java/com/talhanation/bannermod/network/messages/military/MessageOpenContractBoard.java b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageOpenContractBoard.java new file mode 100644 index 00000000..f52a4e1c --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageOpenContractBoard.java @@ -0,0 +1,48 @@ +package com.talhanation.bannermod.network.messages.military; + +import com.talhanation.bannermod.events.RecruitEvents; +import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; +import de.maxhenkel.corelib.net.Message; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.network.NetworkEvent; + +import java.util.Objects; +import java.util.UUID; + +public class MessageOpenContractBoard implements Message { + private UUID recruit; + + public MessageOpenContractBoard() { + } + + public MessageOpenContractBoard(UUID recruit) { + this.recruit = recruit; + } + + @Override + public Dist getExecutingSide() { + return Dist.DEDICATED_SERVER; + } + + @Override + public void executeServerSide(NetworkEvent.Context context) { + ServerPlayer player = Objects.requireNonNull(context.getSender()); + AbstractRecruitEntity recruitEntity = RecruitMessageEntityResolver.resolveRecruitInInflatedBox(player, this.recruit, 16.0D); + if (recruitEntity != null) { + RecruitEvents.openContractBoard(player, recruitEntity); + } + } + + @Override + public MessageOpenContractBoard fromBytes(FriendlyByteBuf buf) { + this.recruit = buf.readUUID(); + return this; + } + + @Override + public void toBytes(FriendlyByteBuf buf) { + buf.writeUUID(this.recruit); + } +} diff --git a/src/main/java/com/talhanation/bannermod/network/messages/military/MessagePinContract.java b/src/main/java/com/talhanation/bannermod/network/messages/military/MessagePinContract.java new file mode 100644 index 00000000..72495ec0 --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/network/messages/military/MessagePinContract.java @@ -0,0 +1,56 @@ +package com.talhanation.bannermod.network.messages.military; + +import com.talhanation.bannermod.events.RecruitEvents; +import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; +import de.maxhenkel.corelib.net.Message; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.network.NetworkEvent; + +import java.util.Objects; +import java.util.UUID; + +public class MessagePinContract implements Message { + private UUID recruit; + private UUID contractId; + private boolean pinned; + + public MessagePinContract() { + } + + public MessagePinContract(UUID recruit, UUID contractId, boolean pinned) { + this.recruit = recruit; + this.contractId = contractId; + this.pinned = pinned; + } + + @Override + public Dist getExecutingSide() { + return Dist.DEDICATED_SERVER; + } + + @Override + public void executeServerSide(NetworkEvent.Context context) { + ServerPlayer player = Objects.requireNonNull(context.getSender()); + AbstractRecruitEntity recruitEntity = RecruitMessageEntityResolver.resolveRecruitInInflatedBox(player, this.recruit, 16.0D); + if (recruitEntity != null) { + RecruitEvents.pinContract(player, recruitEntity, this.contractId, this.pinned); + } + } + + @Override + public MessagePinContract fromBytes(FriendlyByteBuf buf) { + this.recruit = buf.readUUID(); + this.contractId = buf.readUUID(); + this.pinned = buf.readBoolean(); + return this; + } + + @Override + public void toBytes(FriendlyByteBuf buf) { + buf.writeUUID(recruit); + buf.writeUUID(contractId); + buf.writeBoolean(pinned); + } +} diff --git a/src/main/java/com/talhanation/bannermod/network/messages/military/MessageSetContractMaxReward.java b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageSetContractMaxReward.java new file mode 100644 index 00000000..3541d62e --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageSetContractMaxReward.java @@ -0,0 +1,52 @@ +package com.talhanation.bannermod.network.messages.military; + +import com.talhanation.bannermod.events.RecruitEvents; +import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; +import de.maxhenkel.corelib.net.Message; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.network.NetworkEvent; + +import java.util.Objects; +import java.util.UUID; + +public class MessageSetContractMaxReward implements Message { + private UUID recruit; + private int maxReward; + + public MessageSetContractMaxReward() { + } + + public MessageSetContractMaxReward(UUID recruit, int maxReward) { + this.recruit = recruit; + this.maxReward = maxReward; + } + + @Override + public Dist getExecutingSide() { + return Dist.DEDICATED_SERVER; + } + + @Override + public void executeServerSide(NetworkEvent.Context context) { + ServerPlayer player = Objects.requireNonNull(context.getSender()); + AbstractRecruitEntity recruitEntity = RecruitMessageEntityResolver.resolveRecruitInInflatedBox(player, this.recruit, 16.0D); + if (recruitEntity != null) { + RecruitEvents.setContractMaxReward(player, recruitEntity, this.maxReward); + } + } + + @Override + public MessageSetContractMaxReward fromBytes(FriendlyByteBuf buf) { + this.recruit = buf.readUUID(); + this.maxReward = buf.readInt(); + return this; + } + + @Override + public void toBytes(FriendlyByteBuf buf) { + buf.writeUUID(recruit); + buf.writeInt(maxReward); + } +} diff --git a/src/main/java/com/talhanation/bannermod/network/messages/military/MessageToClientUpdateContractBoard.java b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageToClientUpdateContractBoard.java new file mode 100644 index 00000000..0efd1b33 --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageToClientUpdateContractBoard.java @@ -0,0 +1,92 @@ +package com.talhanation.bannermod.network.messages.military; + +import com.talhanation.bannermod.client.military.gui.GovernorContractScreen; +import com.talhanation.bannermod.governance.BannerModGovernorContractStatus; +import com.talhanation.bannermod.governance.BannerModGovernorContractType; +import de.maxhenkel.corelib.net.Message; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.network.NetworkEvent; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class MessageToClientUpdateContractBoard implements Message { + private UUID governorRecruitId; + private UUID claimUuid; + private boolean isOwner; + private int maxContractReward; + private long currentTick; + private List contracts; + + public MessageToClientUpdateContractBoard() { + } + + public MessageToClientUpdateContractBoard(UUID governorRecruitId, UUID claimUuid, boolean isOwner, + int maxContractReward, long currentTick, + List contracts) { + this.governorRecruitId = governorRecruitId; + this.claimUuid = claimUuid; + this.isOwner = isOwner; + this.maxContractReward = maxContractReward; + this.currentTick = currentTick; + this.contracts = contracts; + } + + @Override + public Dist getExecutingSide() { + return Dist.CLIENT; + } + + @Override + public void executeClientSide(NetworkEvent.Context context) { + GovernorContractScreen.applyUpdate(governorRecruitId, claimUuid, isOwner, maxContractReward, currentTick, contracts); + } + + @Override + public MessageToClientUpdateContractBoard fromBytes(FriendlyByteBuf buf) { + this.governorRecruitId = buf.readUUID(); + this.claimUuid = buf.readUUID(); + this.isOwner = buf.readBoolean(); + this.maxContractReward = buf.readInt(); + this.currentTick = buf.readLong(); + int count = buf.readInt(); + this.contracts = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + this.contracts.add(ContractDto.fromBuf(buf)); + } + return this; + } + + @Override + public void toBytes(FriendlyByteBuf buf) { + buf.writeUUID(governorRecruitId); + buf.writeUUID(claimUuid); + buf.writeBoolean(isOwner); + buf.writeInt(maxContractReward); + buf.writeLong(currentTick); + buf.writeInt(contracts == null ? 0 : contracts.size()); + if (contracts != null) { + for (ContractDto dto : contracts) dto.toBuf(buf); + } + } + + public record ContractDto(UUID contractId, String type, int reward, String status, + long deadlineTick, boolean pinned, boolean hasAcceptor) { + public static ContractDto fromBuf(FriendlyByteBuf buf) { + return new ContractDto(buf.readUUID(), buf.readUtf(), buf.readInt(), + buf.readUtf(), buf.readLong(), buf.readBoolean(), buf.readBoolean()); + } + + public void toBuf(FriendlyByteBuf buf) { + buf.writeUUID(contractId); + buf.writeUtf(type); + buf.writeInt(reward); + buf.writeUtf(status); + buf.writeLong(deadlineTick); + buf.writeBoolean(pinned); + buf.writeBoolean(hasAcceptor); + } + } +} From 4ace0c54b8b8eadb386dedc12c77d0d7eba54a2d Mon Sep 17 00:00:00 2001 From: Mr-Artillery Date: Sun, 26 Apr 2026 13:14:26 -0400 Subject: [PATCH 3/3] feat(governor): add debug summon command for in-game testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New GovernorDebugCommands registered under /recruits admin governorDebug - /recruits admin governorDebug summon — spawns a RecruitEntity with XP=7 (governor-eligible) at the player's position, owned by the player, and auto-assigns it to the claim at that location when one exists - /recruits admin governorDebug info — lists all active governor snapshots with claim UUID, governor UUID, incident count, and autoManage flag - Wired BannerModLifecycle.onRegisterCommands to actually register RecruitsAdminCommands (was previously a stub comment) --- .../bootstrap/BannerModLifecycle.java | 3 +- .../military/GovernorDebugCommands.java | 102 ++++++++++++++++++ .../military/RecruitsAdminCommands.java | 3 +- 3 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/talhanation/bannermod/commands/military/GovernorDebugCommands.java diff --git a/src/main/java/com/talhanation/bannermod/bootstrap/BannerModLifecycle.java b/src/main/java/com/talhanation/bannermod/bootstrap/BannerModLifecycle.java index 82f89f15..644880b4 100644 --- a/src/main/java/com/talhanation/bannermod/bootstrap/BannerModLifecycle.java +++ b/src/main/java/com/talhanation/bannermod/bootstrap/BannerModLifecycle.java @@ -1,5 +1,6 @@ package com.talhanation.bannermod.bootstrap; +import com.talhanation.bannermod.commands.military.RecruitsAdminCommands; import net.minecraftforge.event.RegisterCommandsEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; @@ -11,6 +12,6 @@ public class BannerModLifecycle { @SubscribeEvent public void onRegisterCommands(RegisterCommandsEvent event) { - // Recruit and worker command registration will be filled in by wave 4 (events + commands wave) + RecruitsAdminCommands.register(event.getDispatcher()); } } diff --git a/src/main/java/com/talhanation/bannermod/commands/military/GovernorDebugCommands.java b/src/main/java/com/talhanation/bannermod/commands/military/GovernorDebugCommands.java new file mode 100644 index 00000000..a2b85b56 --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/commands/military/GovernorDebugCommands.java @@ -0,0 +1,102 @@ +package com.talhanation.bannermod.commands.military; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; +import com.talhanation.bannermod.events.ClaimEvents; +import com.talhanation.bannermod.governance.BannerModGovernorManager; +import com.talhanation.bannermod.governance.BannerModGovernorService; +import com.talhanation.bannermod.governance.BannerModGovernorSnapshot; +import com.talhanation.bannermod.persistence.military.RecruitsClaim; +import com.talhanation.bannermod.registry.military.ModEntityTypes; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.phys.Vec3; + +import java.util.Optional; + +final class GovernorDebugCommands { + private GovernorDebugCommands() { + } + + static LiteralArgumentBuilder create() { + return Commands.literal("governorDebug") + .then(Commands.literal("summon") + .executes(ctx -> summonGovernor(ctx.getSource()))) + .then(Commands.literal("info") + .executes(ctx -> printInfo(ctx.getSource()))); + } + + private static int summonGovernor(CommandSourceStack source) { + ServerLevel level = source.getLevel(); + ServerPlayer player; + try { + player = source.getPlayerOrException(); + } catch (Exception e) { + source.sendFailure(Component.literal("Must be run by a player.")); + return 0; + } + + AbstractRecruitEntity recruit = ModEntityTypes.RECRUIT.get().create(level); + if (recruit == null) { + source.sendFailure(Component.literal("Failed to create recruit entity.")); + return 0; + } + + Vec3 pos = player.position(); + recruit.moveTo(pos.x, pos.y, pos.z, player.getYRot(), 0f); + recruit.setCustomName(Component.literal("Debug Governor")); + recruit.setPersistenceRequired(); + recruit.setHunger(50); + recruit.setMoral(50); + recruit.setListen(true); + recruit.setXpLevel(7); + recruit.setOwnerUUID(Optional.of(player.getUUID())); + + level.addFreshEntityWithPassengers(recruit); + + // Try to auto-assign as governor to the claim at the player's location + RecruitsClaim claim = resolveClaim(level, pos); + if (claim != null) { + BannerModGovernorService service = new BannerModGovernorService(BannerModGovernorManager.get(level)); + BannerModGovernorService.OperationResult result = service.assignGovernor(claim, player, recruit); + if (result.allowed()) { + source.sendSuccess(() -> Component.literal( + "Debug Governor spawned and assigned to claim \"" + claim.getName() + "\". Right-click to open."), false); + } else { + source.sendSuccess(() -> Component.literal( + "Debug Governor spawned (XP=7) but not auto-assigned: " + result.governorDecision().name().toLowerCase() + + ". Right-click → Promote → Governor to assign manually."), false); + } + } else { + source.sendSuccess(() -> Component.literal( + "Debug Governor spawned (XP=7). No claim here — right-click → Promote → Governor to assign."), false); + } + + return 1; + } + + private static int printInfo(CommandSourceStack source) { + ServerLevel level = source.getLevel(); + BannerModGovernorManager manager = BannerModGovernorManager.get(level); + int count = manager.getAllSnapshots().size(); + source.sendSuccess(() -> Component.literal("Active governor snapshots: " + count), false); + + for (BannerModGovernorSnapshot snap : manager.getAllSnapshots()) { + source.sendSuccess(() -> Component.literal( + " claim=" + snap.claimUuid() + " governor=" + snap.governorRecruitUuid() + + " incidents=" + snap.incidentTokens().size() + " autoManage=" + snap.autoManage()), false); + } + return 1; + } + + private static RecruitsClaim resolveClaim(ServerLevel level, Vec3 pos) { + if (ClaimEvents.recruitsClaimManager == null) return null; + ChunkPos chunk = new ChunkPos(BlockPos.containing(pos)); + return ClaimEvents.recruitsClaimManager.getClaim(chunk); + } +} diff --git a/src/main/java/com/talhanation/bannermod/commands/military/RecruitsAdminCommands.java b/src/main/java/com/talhanation/bannermod/commands/military/RecruitsAdminCommands.java index 2ecf4fc3..125c9b1d 100644 --- a/src/main/java/com/talhanation/bannermod/commands/military/RecruitsAdminCommands.java +++ b/src/main/java/com/talhanation/bannermod/commands/military/RecruitsAdminCommands.java @@ -23,7 +23,8 @@ private static LiteralArgumentBuilder createAdminCommand() { .then(UnitsManagerAdminCommands.create()) .then(ClaimManagerAdminCommands.create()) .then(NobleVillagerManagerAdminCommands.create()) - .then(DebugManagerAdminCommands.create()); + .then(DebugManagerAdminCommands.create()) + .then(GovernorDebugCommands.create()); } private static LiteralArgumentBuilder createTeleportToOwnerCommand() {