From 9606ba035d76ce6f6905bbcafa55047af05dfcf6 Mon Sep 17 00:00:00 2001 From: "pozdn.r.a" Date: Sun, 10 May 2026 11:56:49 +0700 Subject: [PATCH 1/7] tools: migrate backlog to sqlite --- .cursor/rules/ai-context-proxy.mdc | 4 +- .github/copilot-instructions.md | 4 +- .opencode/plugins/project-guardrails.js | 4 +- .windsurf/rules/ai-context-proxy.md | 4 +- AGENTS.md | 7 +- GEMINI.md | 4 +- README.md | 6 +- docs/BANNERMOD_BACKLOG.json | 10042 ---------------- docs/BANNERMOD_BACKLOG.md | 9 +- docs/BANNERMOD_BACKLOG.sqlite | Bin 0 -> 823296 bytes docs/CONTRIBUTING.md | 4 +- docs/DEVELOPMENT.md | 2 +- docs/README.md | 4 +- docs/STATUS.md | 2 +- docs/TOOLS.md | 9 +- .../gui/CriticalUiStateVerificationTest.java | 5 + tools/agent-hooks/pre-bash-guardrails.py | 9 +- tools/ai-context-proxy/README.md | 2 +- tools/backlog.py | 208 +- 19 files changed, 228 insertions(+), 10101 deletions(-) delete mode 100644 docs/BANNERMOD_BACKLOG.json create mode 100644 docs/BANNERMOD_BACKLOG.sqlite diff --git a/.cursor/rules/ai-context-proxy.mdc b/.cursor/rules/ai-context-proxy.mdc index ef14983d..5b2cddc5 100644 --- a/.cursor/rules/ai-context-proxy.mdc +++ b/.cursor/rules/ai-context-proxy.mdc @@ -24,8 +24,8 @@ export PATH="$PWD/tools/ai-context-proxy/wrappers:$PWD/tools/ai-context-proxy/bi ## Backlog -- Use `tools/backlog` instead of reading `docs/BANNERMOD_BACKLOG.json` directly. -- Use `tools/backlog batch --limit 5`, `tools/backlog show `, `tools/backlog add ... --dry-run`, and `tools/backlog validate` for normal backlog work. +- Use `tools/backlog` instead of reading `docs/BANNERMOD_BACKLOG.sqlite` directly. +- Use `tools/backlog batch --limit 5`, `tools/backlog show `, `tools/backlog add ... --dry-run`, `tools/backlog validate`, and `tools/backlog stage` for normal backlog work. - Before marking a task done, self-verify every acceptance item and record the result with `tools/backlog done --verification "..."`. - If a task changes UI, changes gameplay mechanics, or adds player-facing mechanics, update both `MULTIPLAYER_GUIDE_RU.md` and `MULTIPLAYER_GUIDE_EN.md` before marking it done. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a1b41156..bfe4d2a0 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -17,8 +17,8 @@ source tools/ai-context-proxy/env.sh ## Backlog -- Use `tools/backlog` instead of reading `docs/BANNERMOD_BACKLOG.json` directly. -- Use `tools/backlog batch --limit 5`, `tools/backlog show `, `tools/backlog add ... --dry-run`, and `tools/backlog validate` for normal backlog work. +- Use `tools/backlog` instead of reading `docs/BANNERMOD_BACKLOG.sqlite` directly. +- Use `tools/backlog batch --limit 5`, `tools/backlog show `, `tools/backlog add ... --dry-run`, `tools/backlog validate`, and `tools/backlog stage` for normal backlog work. - Before marking a task done, self-verify every acceptance item and record the result with `tools/backlog done --verification "..."`. - If a task changes UI, changes gameplay mechanics, or adds player-facing mechanics, update both `MULTIPLAYER_GUIDE_RU.md` and `MULTIPLAYER_GUIDE_EN.md` before marking it done. diff --git a/.opencode/plugins/project-guardrails.js b/.opencode/plugins/project-guardrails.js index 4fcf710c..db694db9 100644 --- a/.opencode/plugins/project-guardrails.js +++ b/.opencode/plugins/project-guardrails.js @@ -21,9 +21,9 @@ export const BannerModProjectGuardrails = async () => { if (!command) return if (SAFE_PREFIXES.some((prefix) => command.startsWith(prefix))) return - if (command.includes("BANNERMOD_BACKLOG.json")) { + if (command.includes("BANNERMOD_BACKLOG.json") || command.includes("BANNERMOD_BACKLOG.sqlite")) { throw new Error( - "Blocked direct backlog JSON access. Use tools/backlog batch/show/add/validate instead." + "Blocked direct backlog access. Use tools/backlog batch/show/add/validate/stage instead." ) } diff --git a/.windsurf/rules/ai-context-proxy.md b/.windsurf/rules/ai-context-proxy.md index f1163b07..4ad8b935 100644 --- a/.windsurf/rules/ai-context-proxy.md +++ b/.windsurf/rules/ai-context-proxy.md @@ -17,8 +17,8 @@ source tools/ai-context-proxy/env.sh ## Backlog -- Use `tools/backlog` instead of reading `docs/BANNERMOD_BACKLOG.json` directly. -- Use `tools/backlog batch --limit 5`, `tools/backlog show `, `tools/backlog add ... --dry-run`, and `tools/backlog validate` for normal backlog work. +- Use `tools/backlog` instead of reading `docs/BANNERMOD_BACKLOG.sqlite` directly. +- Use `tools/backlog batch --limit 5`, `tools/backlog show `, `tools/backlog add ... --dry-run`, `tools/backlog validate`, and `tools/backlog stage` for normal backlog work. - Before marking a task done, self-verify every acceptance item and record the result with `tools/backlog done --verification "..."`. - If a task changes UI, changes gameplay mechanics, or adds player-facing mechanics, update both `MULTIPLAYER_GUIDE_RU.md` and `MULTIPLAYER_GUIDE_EN.md` before marking it done. diff --git a/AGENTS.md b/AGENTS.md index dcd4914f..1f864349 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,8 +72,9 @@ After coding, provide: - For multi-line commit messages: write the message to `/tmp/commit_msg.txt` via the file-writing tool, then `git commit -F /tmp/commit_msg.txt`. The `ctx` pre-bash hook blocks heredoc patterns (`<<'EOF'`). 10. Backlog hygiene -- `docs/BANNERMOD_BACKLOG.json` is the single canonical backlog. `docs/BANNERMOD_BACKLOG.md` is only a pointer and must not contain task data. -- Use `tools/backlog` for normal backlog work instead of reading or dumping the JSON directly: `tools/backlog batch --limit 5`, `tools/backlog show `, `tools/backlog list --status open`, `tools/backlog validate`. +- `docs/BANNERMOD_BACKLOG.sqlite` is the single canonical backlog. `docs/BANNERMOD_BACKLOG.md` is only a pointer and must not contain task data. +- Use `tools/backlog` for normal backlog work instead of reading or dumping the SQLite DB directly: `tools/backlog batch --limit 5`, `tools/backlog show `, `tools/backlog list --status open`, `tools/backlog validate`. +- Use `tools/backlog stage` to stage canonical backlog DB changes; do not run raw `git add docs/BANNERMOD_BACKLOG.sqlite`. - When adding a task, use `tools/backlog add --why ... --scope ... --acceptance ...`. Every task must include `id`, `title`, `why`, concrete scope deliverables, and verifiable acceptance checks. - **DONE = every acceptance item is observably satisfied right now.** Closing a task is a binary check against the existing acceptance list, not a judgement call. If any acceptance item describes gameplay-observable behaviour you cannot demonstrate from current code, the task stays open even when supporting infrastructure landed. - Before marking a task done, verify your own implementation against every acceptance item. Record the exact verification result with `tools/backlog done <ID> --verification "..."`; if verification cannot be run, record what blocked it instead of pretending it passed. @@ -85,7 +86,7 @@ After coding, provide: 11. Contribution flow - Read `docs/STATUS.md` before picking up brownfield work. - Use `docs/CONTRIBUTING.md` as the contribution flow for code, tests, docs, and commits. -- Use `tools/backlog` to inspect and update the canonical active backlog (`docs/BANNERMOD_BACKLOG.json`). +- Use `tools/backlog` to inspect and update the canonical active backlog (`docs/BANNERMOD_BACKLOG.sqlite`). - Put module documentation under `docs/`; keep root player guides split as `MULTIPLAYER_GUIDE_RU.md` and `MULTIPLAYER_GUIDE_EN.md`. - The local context multitool is documented in `tools/ai-context-proxy/README.md` and summarized in `docs/TOOLS.md`. - If the user invokes `/backlog-execute [N]`, treat it as an execution command, not a planning request: default `N=5`, run `tools/backlog ready N`, pick that batch as the active queue, and begin execution under the backlog dependency, worktree, feature-branch, and finish-or-split rules. diff --git a/GEMINI.md b/GEMINI.md index 4f7ab5c1..28082713 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -17,8 +17,8 @@ source tools/ai-context-proxy/env.sh ## Backlog -- Use `tools/backlog` instead of reading `docs/BANNERMOD_BACKLOG.json` directly. -- Use `tools/backlog batch --limit 5`, `tools/backlog show <ID>`, `tools/backlog add ... --dry-run`, and `tools/backlog validate` for normal backlog work. +- Use `tools/backlog` instead of reading `docs/BANNERMOD_BACKLOG.sqlite` directly. +- Use `tools/backlog batch --limit 5`, `tools/backlog show <ID>`, `tools/backlog add ... --dry-run`, `tools/backlog validate`, and `tools/backlog stage` for normal backlog work. - Before marking a task done, self-verify every acceptance item and record the result with `tools/backlog done <ID> --verification "..."`. - If a task changes UI, changes gameplay mechanics, or adds player-facing mechanics, update both `MULTIPLAYER_GUIDE_RU.md` and `MULTIPLAYER_GUIDE_EN.md` before marking it done. diff --git a/README.md b/README.md index 57bedf76..a1814687 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ The active mod lives in the root `src/**` tree under the `bannermod` runtime. Th - Documentation index: `docs/README.md` - Current developer status: `docs/STATUS.md` - Contribution flow: `docs/CONTRIBUTING.md` -- Canonical unfinished-work queue: `docs/BANNERMOD_BACKLOG.json` via `tools/backlog` +- Canonical unfinished-work queue: `docs/BANNERMOD_BACKLOG.sqlite` via `tools/backlog` ## Current Gameplay Shape @@ -25,7 +25,7 @@ BannerMod is playable in pieces and still under active stabilization. The curren - government-form editing for monarchy/republic authority differences; - recruit formation stance control, shield-wall behavior, reach weapons, flanking, cohesion, brace, and counter rules. -Important unfinished areas remain in `docs/BANNERMOD_BACKLOG.json`; use `tools/backlog batch --limit 5` instead of reading the full file directly. +Important unfinished areas remain in `docs/BANNERMOD_BACKLOG.sqlite`; use `tools/backlog batch --limit 5` instead of reading the database directly. ## Build @@ -41,7 +41,7 @@ Use `./gradlew verifyGameTestStage` when changing gameplay wiring, ownership, AI - Active source: `src/**` - Active mod id: `bannermod` - Active planning: `.planning/` -- Active backlog: `docs/BANNERMOD_BACKLOG.json` via `tools/backlog` +- Active backlog: `docs/BANNERMOD_BACKLOG.sqlite` via `tools/backlog` - Archive/reference source trees: `recruits/`, `workers/` Do not revive old duplicate faction, diplomacy, worker, or siege systems as parallel gameplay. If code and old planning notes disagree, trust the live code and update the backlog or docs instead of adding compatibility layers without a concrete need. diff --git a/docs/BANNERMOD_BACKLOG.json b/docs/BANNERMOD_BACKLOG.json deleted file mode 100644 index 4acfe310..00000000 --- a/docs/BANNERMOD_BACKLOG.json +++ /dev/null @@ -1,10042 +0,0 @@ -{ - "schema": 2, - "canonical": true, - "description": "Ordered BannerMod unfinished-work backlog. Use tools/backlog.py instead of reading this file directly during normal agent work.", - "rules": [ - "This JSON file is the single canonical backlog; docs/BANNERMOD_BACKLOG.md is only a human-facing pointer.", - "Use tools/backlog.py batch/list/show to inspect work instead of dumping the whole JSON file into context.", - "Every task must include id, title, status, why, scope, acceptance, dependencies, and updated date.", - "DONE means every acceptance item is observably satisfied in the current codebase, not merely supported by a lower-level policy or partial slice.", - "Before marking a task done, run the relevant verification and record the result in verification. If a check cannot be run, record why.", - "If a task changes UI, changes gameplay mechanics, or adds player-facing mechanics, update both MULTIPLAYER_GUIDE_RU.md and MULTIPLAYER_GUIDE_EN.md before marking it done.", - "Append progress entries instead of rewriting history. Keep old evidence and progress unless it is factually wrong.", - "Add new work as an open task with concrete deliverables, explicit dependencies, and verifiable acceptance checks. Do not add vague reminders.", - "Dependencies must be explicit. Use an empty list when a task has no blockers; otherwise list the blocking task ids so work can be parallelized safely.", - "Use tools/backlog ready <N> as the main dependency-safe pickup queue for parallel work.", - "Once a task is taken into active execution, the executor must either finish it to acceptance or split the remaining scope into concrete child tasks and rewire the parent dependencies. Do not leave oversized half-done work parked in in_progress.", - "Parallel subagent execution must use one dedicated git worktree and one dedicated feature branch per task. Merge subagent work only after reviewing the exact diff and confirming the implementation satisfies the task acceptance without unrelated edits.", - "For dependency chains, complete the first task, then branch the dependent task from the updated tip of the first task branch, and continue step by step rather than branching dependent work from a stale base." - ], - "tasks": [ - { - "id": "PERF-002", - "title": "Crowd render optimization beyond LOD layers", - "status": "in_progress", - "updated": "2026-04-28", - "why": "Existing recruit render LOD skips some expensive layers at distance, but large multiplayer battles may still be dominated by base model rendering, layer passes, nameplates, state changes, and animation work.", - "scope": [ - "Profile recruit render costs: base model, layers, nameplates, texture/state switches, and animation/model pose churn.", - "Evaluate a distant/crowd simplified renderer or simpler model.", - "Collapse cosmetic/team/biome overlays where possible.", - "Skip held item, armor, and nameplate work for non-near/non-selected crowds when safe." - ], - "acceptance": [ - "Large recruit crowds have a measured render improvement.", - "Close-range readability is preserved.", - "Optimization is backed by profiling evidence, not guesswork." - ], - "progress": [ - { - "date": "2026-04-27", - "text": "Added recruit.render debug profiling counters for crowd impostor rendering. The task remains open until a real client profiling run proves before/after improvement and near-range readability." - }, - { - "date": "2026-04-28", - "text": "Split into PERF-010 profiling instrumentation and PERF-011 measured distant-render cuts; parent remains open until both children satisfy measured improvement, close-range readability, and profiling-evidence acceptance." - } - ], - "verification": [], - "evidence": [ - "Recruit render profiling summary includes recruit.render counters." - ], - "dependencies": [ - "PERF-010", - "PERF-011", - "VANILLA-013" - ] - }, - { - "id": "PORT-001", - "title": "NeoForge 1.21.1 port", - "status": "done", - "updated": "2026-04-28", - "why": "Future platform migration may be needed, but it is not active gameplay work and should not live as a stale parallel plan.", - "scope": [ - "Create a branch only when this becomes active.", - "Upgrade Gradle, toolchain, and mod metadata for NeoForge 1.21.1.", - "Migrate registries, networking, events, item NBT/data components, and dependency APIs.", - "Rebuild tests and smoke run after compile." - ], - "acceptance": [ - "Root build targets NeoForge 1.21.1 on the port branch.", - "compileJava, tests, and in-game smoke pass.", - "Old Forge 1.20.1 assumptions are either removed or documented." - ], - "progress": [ - { - "date": "2026-04-28", - "text": "Split into PORT-002 build baseline, PORT-003 API migration, and PORT-004 verification/smoke; parent remains open until the port branch fully targets NeoForge 1.21.1 and passes compile, tests, and smoke." - } - ], - "verification": [ - { - "date": "2026-04-28", - "result": "Verified on feature/port-batch-16: root build targets NeoForge 1.21.1; ctx log -- ./gradlew compileJava succeeded; ctx log -- ./gradlew test succeeded; ctx log -- ./gradlew verifyGameTestStage succeeded; old Forge compile assumptions blocking the port are cleared." - } - ], - "evidence": [], - "dependencies": [ - "PORT-002", - "PORT-003", - "PORT-004" - ], - "doneDate": "2026-04-28" - }, - { - "id": "WAR-007", - "title": "War Room declare-war wizard", - "status": "done", - "updated": "2026-04-28", - "why": "War Room is now the political/war hub, but starting a war still requires /war declare, so the player-facing warfare loop falls back to chat commands at the most important step.", - "scope": [ - "Add a War Room action flow to pick attacker, defender, war goal, and optional casus belli text.", - "Reuse the same server-side declaration validation and cooldown denial reasons as WarDeclarationCommands.", - "Refresh the selected war list/detail after successful declaration and show rejection reasons in UI/chat feedback." - ], - "acceptance": [ - "A state leader can declare a valid war from War Room without typing a command.", - "Invalid declarations show the same denial reason as the command path.", - "/war declare remains available and behavior-equivalent to the UI path." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "War Room now has a Declare war wizard with attacker/defender/goal/casus belli selection. MessageDeclareWar routes UI declarations through WarDeclarationService, and WarDeclarationCommands now uses the same service for /war declare, preserving leader/op, offensive-status, cooldown, audit, notice, and failure-message behavior. Successful UI declaration sends an immediate war-state snapshot back to the player; server denial messages are sent via chat. Player guides and politics warfare docs updated. Verified with tools/ai-context-proxy/bin/ctx log -- ./gradlew compileJava (BUILD SUCCESSFUL)." - } - ], - "evidence": [ - "WarListScreen.init has info/state/siege/ally/refresh actions but no declare action.", - "docs/POLITICS_WARFARE_EN.md documents declaration through /war declare.", - "WarDeclarationCommands contains the command-only declaration path." - ], - "doneDate": "2026-04-27", - "dependencies": [ - "UI-003", - "SEC-005" - ] - }, - { - "id": "WAR-008", - "title": "Player-facing war outcome negotiation UI", - "status": "done", - "updated": "2026-04-28", - "why": "War resolution actions exist, but cancel/peace/tribute/vassalize/demilitarize/occupy/annex are command-driven and partly admin-gated, so normal leaders lack a clear surrender, peace, or outcome flow.", - "scope": [ - "Add a War Room outcome panel for legal leader actions: cancel, peace offer, tribute request, occupation, or other rules-approved outcomes.", - "Keep op/admin-only forced outcomes visibly locked or labeled as admin-only.", - "Surface outcome success, denial, and audit summary without requiring server log inspection." - ], - "acceptance": [ - "Leaders can resolve eligible wars through UI where gameplay rules allow it.", - "Admin-only outcomes cannot be triggered by non-op UI paths.", - "Tribute, occupation, and annex success or denial is visible in War Room." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "WarListScreen now exposes outcome controls for selected live wars: attacker-leader Cancel war, Occupy here, Annex here, plus visible Tribute op-only lock. MessageResolveWarOutcome validates server-side authority, keeps tribute op-only, applies WarOutcomeApplier cancel/occupy/annex/tribute paths, sends chat success/denial messages, broadcasts notices, and immediately syncs the updated war snapshot to the acting player. Player guides EN/RU and politics warfare docs updated. Verified with tools/ai-context-proxy/bin/ctx log -- ./gradlew compileJava (BUILD SUCCESSFUL)." - } - ], - "evidence": [ - "WarDeclarationCommands exposes outcome commands and gates several behind permission level 2.", - "WarListScreen renders selected war details but no outcome controls." - ], - "doneDate": "2026-04-27", - "dependencies": [ - "WAR-007", - "SEC-005" - ] - }, - { - "id": "SETTLEMENT-008", - "title": "Manual validated buildings create gameplay vacancies", - "status": "done", - "updated": "2026-04-27", - "why": "Docs present manual building validation as a normal survival growth path, but auto-staffing vacancies are registered from prefab BuildArea completion, making manual validated workplaces feel inert compared with prefabs.", - "scope": [ - "Define vacancy/profession outputs for manually validated farm, mine, lumber camp, storage, market, barracks, and similar buildings where applicable.", - "Register vacancies from ValidatedBuildingRecord data, not only prefab BuildArea completion.", - "Show player feedback for which jobs or recruit/citizen slots a validated building created." - ], - "acceptance": [ - "A manually validated workplace can attract, convert, or employ citizens like an equivalent prefab path.", - "Player inspection shows the building's vacancy/profession output.", - "Prefab and manual paths share tests for vacancy registration parity." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "Implemented manual validated building vacancies for supported workplace types (farm, mine, lumber camp, architect workshop): validation registers vacancies from ValidatedBuildingRecord; citizen assignment/conversion can use non-entity manual anchors; validation and inspection show vacancy output. Added prefab/manual parity unit coverage. Verified with ./gradlew compileJava and ./gradlew test --tests com.talhanation.bannermod.settlement.prefab.staffing.PrefabAutoStaffingRuntimeTest." - } - ], - "evidence": [ - "docs/GAMEPLAY_FLOW_EN.md describes manual validation and profession buildings.", - "docs/CITIZENS_PROFESSIONS_RECRUITS_EN.md notes manual validated buildings and prefab vacancies are not equivalent.", - "PrefabAutoStaffingRuntime.onBuildAreaCompleted registers vacancies only after prefab completion." - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "SETTLEMENT-009", - "title": "Settlement logistics and work-order dashboard", - "status": "done", - "updated": "2026-04-27", - "why": "Settlement snapshots track stockpiles, desired goods, projects, market state, residents, buildings, and supply signals, but the Governor UI mostly exposes taxes, incidents, and recommendation tokens. Players cannot see why production or growth is stuck.", - "scope": [ - "Add a read-only settlement logistics panel showing stockpile summary, desired goods, work-order counts by status, and project blockers.", - "Link incidents/recommendations to concrete buildings, residents, or missing goods where possible.", - "Keep management controls out of this slice unless already supported server-side." - ], - "acceptance": [ - "Player can see pending, claimed, completed, or blocked work orders from UI.", - "Player can identify missing goods or project blockers without reading logs or source.", - "Governor screen no longer relies on raw token-like recommendations for core bottlenecks." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "Governor sync now builds server-side logistics lines from BannerModSettlementSnapshot and SettlementWorkOrderRuntime: pending/claimed/recent completed work orders, shortage blockers, stockpile capacity/routes, missing goods, and project hint. GovernorScreen renders a read-only Logistics panel instead of relying only on raw recommendation tokens for bottlenecks. Player guides EN/RU updated. Verified with tools/ai-context-proxy/bin/ctx log -- ./gradlew compileJava (BUILD SUCCESSFUL)." - } - ], - "evidence": [ - "BannerModSettlementSnapshot contains rich settlement state.", - "BannerModGovernorSnapshot carries rollups/tokens.", - "GovernorScreen renders taxes, incidents, recommendations, and policy labels but not logistics/work-order detail." - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "POL-002", - "title": "Co-leader management flow", - "status": "done", - "updated": "2026-04-28", - "why": "Political records persist co-leaders and government forms, but normal players have no clear add/remove co-leader flow, making republic/shared-authority gameplay hard to use.", - "scope": [ - "Add leader-authorized co-leader invite/add/remove actions.", - "Surface the co-leader list and authority effect in the state UI.", - "Reuse PoliticalEntityRecord persistence and the shared political authority policy." - ], - "acceptance": [ - "A state leader can add and remove co-leaders without save editing.", - "Republic co-leaders can perform intended co-leader actions; monarchy co-leaders cannot unless policy explicitly allows it.", - "UI clearly shows who has authority and why." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "PoliticalEntityRecord now supports co-leader list replacement and PoliticalRegistryRuntime can add/remove co-leaders with dirty persistence. War Room state UI exposes Add co-leader / Remove co dialogs using player UUIDs, shows co-leader short IDs and whether Republic or Monarchy authority is active, and MessageUpdateCoLeader enforces leader/op management through PoliticalEntityAuthority.isLeaderOrOp. Existing PoliticalEntityAuthority.canAct remains the government-form-aware policy: Republic shares authority with co-leaders, Monarchy keeps them listed but locked. Player guides EN/RU and politics warfare docs updated. Verified with tools/ai-context-proxy/bin/ctx log -- ./gradlew compileJava (BUILD SUCCESSFUL)." - } - ], - "evidence": [ - "PoliticalEntityRecord stores coLeaderUuids and governmentForm.", - "PoliticalRegistryRuntime has mutators for name/status/capital/form/color/charter but no co-leader mutator.", - "Politics docs mention co-leaders without a management path." - ], - "doneDate": "2026-04-27", - "dependencies": [ - "SEC-005" - ] - }, - { - "id": "COMMAND-001", - "title": "Recruit command acknowledgement and queue feedback", - "status": "done", - "updated": "2026-04-27", - "why": "The unified command pipeline supports queueing and logging, but normal command UI sends immediate commands and gives little feedback about affected units, overwritten orders, empty selections, or queue state.", - "scope": [ - "Add visible command acknowledgements: selected group, affected unit count, accepted/rejected state, and queued/replaced state.", - "Expose a simple pending-order count per selected group or recruit when queue mode is enabled.", - "Add or deliberately hide a queue modifier depending on final intended gameplay." - ], - "acceptance": [ - "Issuing a command tells the player how many recruits accepted it.", - "Empty selection, invalid target, or no-authority failures produce clear feedback.", - "Queue mode is either available with visible pending orders or intentionally disabled in UI." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "CommandIntentDispatcher now emits server-side chat acknowledgements for every unified command path: rejected no eligible recruits, accepted immediate recruit count, replaced queued-order count, queued modified recruit count, and pending order count for queueMode. Existing command UI keeps immediate mode (queue modifier remains hidden/disabled in UI because MessageMovement and command-screen paths send queueMode=false), while queued paths still expose pending counts if invoked. Player guides EN/RU updated. Verified with tools/ai-context-proxy/bin/ctx log -- ./gradlew compileJava (BUILD SUCCESSFUL)." - } - ], - "evidence": [ - "CommandIntentDispatcher supports queued vs immediate dispatch.", - "CommandIntentLog is debug/in-memory only.", - "MessageMovement sends queueMode=false.", - "CommandScreen sends commands without accepted/queued/replaced feedback." - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "SEC-001", - "title": "Server-authorize claim deletion packets", - "status": "done", - "updated": "2026-04-27", - "why": "Claim deletion must be server-authoritative. A packet path that trusts client-authored claim data can delete or broadcast state without proving sender ownership, dimension, political authority, or op/admin status.", - "scope": [ - "Gate claim deletion through the same authority model as claim update.", - "Reject null sender, wrong dimension, unknown claim UUID, and unauthorized players.", - "Delete only the persisted server-side claim record, never arbitrary client-provided NBT." - ], - "acceptance": [ - "A non-owner/non-op packet cannot delete another player's claim.", - "Owner, allowed co-leader, and admin behavior is covered by tests.", - "Malformed or missing sender packets reject cleanly without crash or mutation." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "MessageDeleteClaim now rejects null senders, disabled claiming, non-Overworld senders, missing managers, malformed/missing UUID payloads, unknown claim UUIDs, and unauthorized actors. It resolves the persisted server-side claim by UUID and removes that object only after shared ClaimPacketAuthority allows owner, political leader/co-leader, or admin. Added ClaimPacketAuthorityTest covering non-owner denial, owner success, admin success, political leader/co-leader success, and malformed inputs. Verified with ./gradlew compileJava and ./gradlew test --tests com.talhanation.bannermod.network.messages.military.ClaimPacketAuthorityTest." - } - ], - "evidence": [ - "MessageDeleteClaim deletes from client-supplied claim data.", - "MessageUpdateClaim has a guarded canEditClaim path that can be reused or unified." - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "SEC-002", - "title": "Remove spoofable owner UUIDs from military packets", - "status": "done", - "updated": "2026-04-27", - "why": "Some server-bound military packets still use client-supplied player UUIDs for filtering or effects, which creates spoof risk and inconsistent authority compared with context.getSender().", - "scope": [ - "Use context.getSender().getUUID() as the only actor identity for server-side authorization.", - "Reject or ignore mismatched wire UUIDs where fields remain for packet compatibility.", - "Route commandable recruit actions through CommandIntentDispatcher where applicable." - ], - "acceptance": [ - "Packets containing another player's UUID cannot affect that player's recruits or upkeep state.", - "Command audit records the real sender, not a client-provided UUID.", - "Tests cover face-command, ranged-fire, and upkeep spoof attempts." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "MessageFaceCommand, MessageRangedFire, and MessageUpkeepEntity now derive the actor UUID from context.getSender().getUUID() and ignore the client-supplied owner UUID field for server-side filtering/effects while preserving packet codec compatibility. Added MilitaryPacketActorIdentityTest covering spoofed wire UUID attempts for face-command, ranged-fire, and upkeep paths. Verified with ./gradlew compileJava and ./gradlew test --tests com.talhanation.bannermod.network.messages.military.MilitaryPacketActorIdentityTest." - } - ], - "evidence": [ - "MessageFaceCommand, MessageRangedFire, and MessageUpkeepEntity use client-provided UUIDs.", - "CommandIntentDispatcher assumes caller supplied already-authorized actors." - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "SEC-003", - "title": "Close group and leader assignment authority gaps", - "status": "done", - "updated": "2026-04-27", - "why": "Group leadership and companion assignment mutate recruit/group state; proximity checks are not enough protection in multiplayer if the sender does not own or control the target recruit/group.", - "scope": [ - "Require RecruitCommandAuthority.canDirectlyControl for target recruits/leaders before leader, companion, or group assignment mutations.", - "Require owned-group validation before writing setGroupUUID or changing group leader state.", - "Align all group assignment packet paths with the safer existing group ownership pattern." - ], - "acceptance": [ - "A nearby player cannot assign another player's leader, companion, or group.", - "Owned recruit/group flows still work.", - "Server tests cover cross-player denial and owner success." - ], - "progress": [ - { - "date": "2026-04-27", - "text": "Added authority gates to MessageSetLeaderGroup and MessageAssignGroupToCompanion: target leaders must pass RecruitCommandAuthority.canDirectlyControl, assigned groups must be owned via RecruitCommandAuthority.ownedGroup, and companion assignment filters recruits through canDirectlyControl before mutating leader/group state. Verified ./gradlew compileJava. Not closing yet because backlog acceptance requires server tests for cross-player denial and owner success." - } - ], - "verification": [ - { - "date": "2026-04-27", - "result": "MessageSetLeaderGroup and MessageAssignGroupToCompanion now require RecruitCommandAuthority.canDirectlyControl for target leaders, RecruitCommandAuthority.ownedGroup for group assignment, and filter companion-assigned recruits through canDirectlyControl before mutation. Added GroupAssignmentAuthorityGameTests covering outsider denial and owner success for leader group assignment and companion group assignment. Verified ./gradlew compileJava and ./gradlew compileGameTestJava." - } - ], - "evidence": [ - "MessageSetLeaderGroup and MessageAssignGroupToCompanion mutate state after weak or partial checks.", - "MessageGroup shows a safer owned-group pattern." - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "SEC-004", - "title": "Dimension-aware claim protection", - "status": "done", - "updated": "2026-04-27", - "why": "Claim protection currently keys lookups by chunk position while events can run in any dimension, so identical X/Z chunks in Nether or End can inherit Overworld claim permissions.", - "scope": [ - "Add dimension identity to claim storage/lookups, or explicitly restrict protection to Overworld and document that rule.", - "Update persistence, sync, and access queries to preserve or enforce dimension semantics.", - "Cover block/entity protection checks across at least two dimensions." - ], - "acceptance": [ - "Overworld claims do not affect Nether/End chunks at matching X/Z unless explicitly supported.", - "Claim save/load preserves dimension or the code rejects non-Overworld claims consistently.", - "Tests cover same chunk coordinates in two dimensions." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "ClaimAccessQueries now returns persisted claims only for ServerLevel Level.OVERWORLD; other dimensions are treated as outside the claim system instead of looking up by shared ChunkPos. Added BannerModClaimProtectionGameTests.overworldClaimDoesNotProtectMatchingNetherCoordinates covering hostile Overworld denial and matching Nether X/Z non-denial. Updated player guides to document Overworld-only claim protection. Verified ./gradlew compileJava and ./gradlew compileGameTestJava." - } - ], - "evidence": [ - "ClaimEvents loads/saves the manager from Overworld.", - "ClaimAccessQueries looks up by ChunkPos.", - "RecruitsClaimManager keys claims by ChunkPos." - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "SEC-005", - "title": "Unified political authority policy", - "status": "done", - "updated": "2026-04-27", - "why": "Government-aware authority exists, but mutation packets, claim editing, and UI enablement use different leader/co-leader/op checks. That makes republic and monarchy authority inconsistent in multiplayer.", - "scope": [ - "Define one policy for leader, co-leader, member, and op authority per government form.", - "Apply it consistently to political mutations, claim editing, ally/war actions, and UI button enablement.", - "Add tests for command and packet parity." - ], - "acceptance": [ - "Republic co-leaders can perform intended actions and monarchy co-leaders cannot unless explicitly allowed.", - "Packet, command, and UI paths agree for rename, capital, color, charter, claim edit, and ally/war actions.", - "Denial messages explain whether failure is role, government form, or op permission." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "Unified shared political authority around PoliticalEntityAuthority.canAct for command, packet, and War Room UI paths. Commands now delegate shared actions through WarCommandSupport.canAct; political mutation packets for rename/capital/color/charter use canAct; claim edit uses canAct; ally invite/response/cancel, siege placement, declaration, and legal outcome UI use canAct. Government-form changes and co-leader list management remain strict leader/op via isLeaderOrOp with leader-only denial. UI enablement now mirrors shared authority for republic co-leaders while monarchy keeps co-leaders locked. Added PoliticalEntityAuthorityTest coverage for republic co-leader shared authority and strict leader-only policy parity. Updated player guides and politics docs. Verified with tools/ai-context-proxy/bin/ctx log -- ./gradlew compileJava and focused ./gradlew test --tests com.talhanation.bannermod.war.registry.PoliticalEntityAuthorityTest (both BUILD SUCCESSFUL)." - } - ], - "evidence": [ - "PoliticalEntityAuthority.canAct is government-aware.", - "Some mutation packets use leader-only isLeaderOrOp.", - "MessageUpdateClaim.canEditClaim permits co-leaders directly." - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "UI-003", - "title": "War UI client mirror freshness", - "status": "done", - "updated": "2026-04-27", - "why": "War/state UI reads from a client mirror that can survive login/logout and screens often copy snapshots on init/manual refresh, so players can see stale political or war data after server/world changes.", - "scope": [ - "Clear WarClientState during client login/logout lifecycle.", - "Make War Room and state screens track WarClientState.version() and refresh selected/list/button state automatically.", - "Show sync-pending/stale state when no authoritative snapshot has arrived." - ], - "acceptance": [ - "Switching worlds/servers never shows previous war/state data.", - "An open War Room updates after server snapshot without pressing Refresh.", - "Stale selections are disabled or cleared safely." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "Client login/logout now clears WarClientState alongside ClientManager state. WarClientState exposes hasSnapshot(), clears it on clear/null snapshots, and sets it on authoritative NBT apply. WarListScreen and PoliticalEntityListScreen track WarClientState.version(), auto-refresh open lists/selections/buttons on tick, clamp scroll, clear stale selections, and show sync-pending empty text before the first snapshot. Verified ./gradlew compileJava and ./gradlew test --tests com.talhanation.bannermod.war.client.WarClientStateTest." - } - ], - "evidence": [ - "WarClientState has clear() and version().", - "ClientSyncLifecycleEvents has login/logout hooks.", - "WarListScreen snapshots state in init()/manual refresh()." - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "UI-004", - "title": "Server-authoritative atomic claim editing UX", - "status": "done", - "updated": "2026-04-28", - "why": "World-map claim UI mutates local claim objects optimistically and sends payment/update/delete separately. Server rejection can follow a charge or leave the map briefly showing unconfirmed state.", - "scope": [ - "Replace create/add/remove/delete/edit flows with intent packets validated and charged atomically server-side.", - "Return success or denial feedback for overlap, distance, permissions, dimension, and config-disabled cases.", - "Update the client map only from authoritative sync after acceptance." - ], - "acceptance": [ - "Failed claim actions do not charge currency.", - "Client map does not show unconfirmed claim mutations as accepted state.", - "Denial reasons are visible in the world-map UI." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "Added MessageClaimIntent for server-authoritative ADD_CHUNK, REMOVE_CHUNK, and DELETE flows. Server validates claiming enabled, Overworld, claim existence, edit authority, distance, occupancy/max-size, and payment before mutation; currency is charged only after validation. World-map add/remove/delete no longer mutates ClientManager claim state optimistically and waits for authoritative sync/broadcast; server sends accepted/denied chat feedback with reasons. Verified with compileJava." - } - ], - "evidence": [ - "WorldMapClaimController.claimArea/claimChunk and WorldMapClaimMenuActions.removeSelectedChunk/deleteSelectedClaim mutate/send client-authored claim state.", - "MessageUpdateClaim consumes full claim NBT server-side." - ], - "doneDate": "2026-04-28", - "dependencies": [ - "SEC-001", - "SEC-004", - "SEC-005", - "SEC-006" - ] - }, - { - "id": "UI-005", - "title": "Build Area authoring validation and feedback", - "status": "done", - "updated": "2026-04-27", - "why": "Build Area scan/build flow relies on client-side structure scanning and sends structure data to the server, while player feedback for invalid scans, bounds, materials, or build-start rejection is thin.", - "scope": [ - "Add explicit scan/build status text and validation errors to Build Area UI.", - "Add server-side bounds/NBT sanity checks independent of the client UI.", - "Surface required-material and build-start rejection reasons in the screen." - ], - "acceptance": [ - "Invalid or oversized scans are rejected with a visible reason.", - "Build/Place actions show accepted or denied state.", - "Server enforces dimensions and NBT limits even if the client sends malformed data." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "BuildAreaScreen now shows status text for scan/load/build/place flows, rejects short scan names, invalid dimensions, missing structure data, and empty scans before send, disables Build/Place without a non-empty structure, and surfaces required-material guidance before normal builds. MessageUpdateBuildArea now enforces server-side dimension bounds, required build structure NBT, matching structure dimensions, non-empty block lists, and block count <= bounds before mutating or starting builds, with accepted/rejected system messages. Player guides EN/RU updated. Verified with tools/ai-context-proxy/bin/ctx log -- ./gradlew compileJava (BUILD SUCCESSFUL)." - } - ], - "evidence": [ - "BuildAreaScreen.performClientScan and button handlers scan/send structure data.", - "MessageUpdateBuildArea accepts update payloads server-side." - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "UI-006", - "title": "Storage route authoring UX and validation display", - "status": "done", - "updated": "2026-04-27", - "why": "Storage route setup exposes raw UUID/filter/count/priority fields and saves on close/toggles; parse failures are chat-only, which is brittle for non-technical players.", - "scope": [ - "Add an explicit Apply action with inline validation for destination, count, priority, and filters.", - "Render localized blocked reasons and server parse errors inside the screen.", - "Add destination picker or summary so players do not need to copy raw UUIDs." - ], - "acceptance": [ - "Invalid route fields are highlighted before send.", - "Server rejection appears inline on the route screen.", - "A player can pick or confirm a destination without manually typing a UUID." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "StorageAreaScreen now has explicit Apply route action instead of close-to-save for route fields. Apply route validates with BannerModLogisticsAuthoringState.parse before send, highlights destination UUID/filter/count/priority errors inline, normalizes valid values, shows current destination summary and route blocked state in the screen, and lets players confirm an existing destination without retyping it. Server-side MessageUpdateStorageArea still rejects parse failures with the same parser and visible system-message reason. Player guides EN/RU updated. Verified with tools/ai-context-proxy/bin/ctx log -- ./gradlew compileJava (BUILD SUCCESSFUL)." - } - ], - "evidence": [ - "StorageAreaScreen builds raw text fields and sends on close/toggle.", - "MessageUpdateStorageArea parses updates and reports failures outside the screen flow." - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "UI-007", - "title": "Accessibility and localization pass for active screens", - "status": "done", - "updated": "2026-04-30", - "why": "Several active screens use hardcoded English literals and custom widgets with empty narration hooks, reducing localization quality and accessibility.", - "scope": [ - "Move hardcoded active UI strings in war/state/build/storage screens to language keys.", - "Add narration for dropdowns, status widgets, and label widgets.", - "Add hover tooltips for disabled actions and color-coded statuses." - ], - "acceptance": [ - "War/state/build/storage screens render player-facing text from lang keys.", - "Screen readers announce dropdown values/options and static status widgets.", - "Disabled action buttons explain why they are unavailable." - ], - "progress": [ - { - "date": "2026-04-27", - "text": "Partial batch: localized WarListScreen and PoliticalEntityListScreen active text to en_us/ru_ru keys; added disabled-action tooltips for war/state actions; added non-empty narration for DropDownMenu, BlackShowingTextField, and ItemWithLabelWidget. Verified with ./gradlew compileJava. Remaining acceptance not closed: broader build/storage screen localization pass still needs audit." - }, - { - "date": "2026-04-30", - "text": "Resumed on feature/cleanup-batch-2 as an unblocker task; goal is closure with mechanics/UI behavior preserved so UX-004 can move." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) ./gradlew compileJava passed on feature/ui-007 after moving active war/state/build/storage screen text to lang keys and adding widget narration; 2) code review confirmed disabled build-area actions now explain unavailable states and dropdown/status/label widgets expose narration hooks; 3) ./gradlew test and ./gradlew verifyGameTestStage passed on feature/cleanup-batch-2." - } - ], - "evidence": [ - "WarListScreen and PoliticalEntityListScreen use button/render literals.", - "DropDownMenu, BlackShowingTextField, and ItemWithLabelWidget have empty narration overrides." - ], - "dependencies": [ - "UI-003", - "UI-005", - "UI-006", - "WAR-007", - "WAR-008", - "VANILLA-013" - ], - "doneDate": "2026-04-30" - }, - { - "id": "PERF-003", - "title": "Budget true-async pathfinding snapshot work", - "status": "done", - "updated": "2026-04-27", - "why": "True async pathfinding can still spend server-thread time building region snapshots before scheduler admission/backpressure, so large path bursts may scan/copy regions even when work is later rejected.", - "scope": [ - "Enforce AsyncPathfindingSnapshotBudgetNanos before or around snapshot building.", - "Check scheduler capacity before expensive region capture.", - "Add counters for snapshot time, skipped snapshots, and rejected-before-build requests." - ], - "acceptance": [ - "Under a synthetic crowd path burst, server-thread snapshot time stays within budget.", - "Rejected requests do not build RegionSnapshot.", - "Profiling counters expose budget hits and pre-build rejections." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "TrueAsyncPathfindingRuntime now calls AsyncPathScheduler.canAccept(priority) before RegionSnapshotBuilder.build, so requests rejected by queue/priority capacity return before snapshot capture and increment pathfinding.true_async.runtime.fallback.scheduler_rejected_before_snapshot. RegionSnapshotBuilder now accepts AsyncPathfindingSnapshotBudgetNanos, checks budget after cell flags and dynamic obstacle capture, and returns BUDGET_EXCEEDED without scheduler submit. Runtime records snapshot.build_nanos and existing snapshot.reject.<status> counters include budget_exceeded. AsyncPathSchedulerTest covers canAccept global/priority caps; RegionSnapshotBuilderTest covers budget-exceeded rejection. Verified ./gradlew compileJava and focused async pathfinding unit tests." - } - ], - "evidence": [ - "TrueAsyncPathfindingRuntime.enqueue builds snapshots before async scheduling decisions.", - "RegionSnapshotBuilder.captureDynamicObstacles scans regions.", - "Config exposes AsyncPathfindingSnapshotBudgetNanos." - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "PERF-004", - "title": "Dirty/incremental war-state sync", - "status": "done", - "updated": "2026-04-27", - "why": "War sync polls every second by converting full war, political, siege, invite, occupation, revolt, and schedule state to NBT for hashing, then sends full snapshots on change. Idle servers pay serialization cost for no gameplay change.", - "scope": [ - "Replace per-second full-NBT hash polling with dirty/version counters or cached encoded payloads.", - "Avoid full serialization while state is unchanged.", - "Consider delta packets for large collections if cached full snapshots are still too heavy." - ], - "acceptance": [ - "Idle war runtime performs no full-record NBT serialization per second.", - "Unchanged state sends no packets.", - "Changed state still reaches logged-in clients promptly." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "WarStateBroadcaster no longer computes a per-second full-record NBT hash. Client-visible war SavedData dirty listeners now also bump WarSyncDirtyTracker; broadcaster checks that version each second and returns without encoding or sending when unchanged. Snapshot payloads are cached by dirty version and reused for login/full broadcast until the next dirty mark. Removed old stateHash serialization path. Verified ./gradlew compileJava, ./gradlew test --tests com.talhanation.bannermod.war.events.WarSyncDirtyTrackerTest, and ctx search confirmed stateHash/toTag().hashCode polling removal." - } - ], - "evidence": [ - "WarStateBroadcaster.onServerTick computes stateHash.", - "stateHash serializes all records.", - "WarClientState.encode produces the full client mirror payload." - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "PERF-005", - "title": "Avoid full claim-list rebroadcast on login", - "status": "done", - "updated": "2026-04-27", - "why": "A player join broadcasts the full claim list to all players, serializing the entire claim set per recipient. This scales with claims times online players for a single login.", - "scope": [ - "Send full claim state only to the joining player on login.", - "Cache one encoded claim snapshot per full-sync event if broadcast remains necessary elsewhere.", - "Keep targeted single-claim update packets for mutations." - ], - "acceptance": [ - "Player login sends one full claim sync to the joining player only.", - "Full claim NBT is encoded once per sync event.", - "Claim mutation still updates clients that need the changed state." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "ClaimEvents.onPlayerJoin now calls RecruitsClaimManager.sendClaimsToPlayer for the joining ServerPlayer only instead of broadcastClaimsToAll. RecruitsClaimManager.broadcastClaimsToAll now creates one MessageToClientUpdateClaims per full-sync event before looping recipients, so RecruitsClaim.toNBT(list) runs once per broadcast event instead of once per player. Targeted single-claim mutation updates still use broadcastClaimUpdateTo unchanged. Verified ./gradlew compileJava." - } - ], - "evidence": [ - "ClaimEvents.onPlayerJoin calls full broadcast.", - "RecruitsClaimManager.broadcastClaimsToAll sends full state to every player.", - "MessageToClientUpdateClaims serializes all claims." - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "PERF-006", - "title": "Cache and throttle formation-map snapshots", - "status": "done", - "updated": "2026-04-27", - "why": "Formation-map snapshot requests scan indexed recruits and compute role/relation data per visible recruit. Repeated polling by multiple players can become repeated O(all recruits) server work.", - "scope": [ - "Add per-player request throttling and short-lived cached snapshots keyed by player/level/version.", - "Reuse relation context during a snapshot build.", - "Add counters for request count, recruit candidates, throttles, cache hits, and emitted contacts." - ], - "acceptance": [ - "Repeated map requests within the cache window do not rescan all recruits.", - "Spammed requests are throttled without disconnecting legitimate users.", - "Contact correctness remains unchanged after movement/group changes." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "Implemented per-player/per-dimension formation-map snapshot request cache with 5-tick windows keyed by RecruitIndex version, same-tick request throttling, cached contact reuse, relation context reuse inside snapshot builds, and RuntimeProfilingCounters for requests, throttles, cache hits, recruit candidates, and emitted contacts. RecruitIndex version increments on join/leave/owner/group/chunk-bucket changes so cached snapshots invalidate on relevant group/movement topology changes. Verified with ./gradlew compileJava." - } - ], - "evidence": [ - "MessageRequestFormationMapSnapshot.executeServerSide directly builds snapshots.", - "FormationMapSnapshotService.buildSnapshot iterates recruits and computes relation through relationFor." - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "PERF-007", - "title": "Stable batched settlement-orchestrator iteration", - "status": "done", - "updated": "2026-04-27", - "why": "Settlement orchestration is batched, but each batch recreates and sorts the full snapshot list before processing its slice. Large settlement counts pay O(n log n) setup on every batch tick.", - "scope": [ - "Maintain a stable ordered snapshot cursor for each maintenance cycle, or provide deterministic indexed iteration from the manager.", - "Copy/sort at most once per full cycle.", - "Preserve existing heartbeat/orchestrator counters." - ], - "acceptance": [ - "A maintenance pass over many settlements copies/sorts at most once per cycle.", - "Batch order remains deterministic.", - "Existing processed/total/completed counters remain meaningful." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "Implemented per-level cached sorted settlement UUID order for orchestrator batches, rebuilt only when a maintenance cycle starts at startIndex <= 0. Batch processing keeps deterministic UUID order, retrieves current snapshots by UUID, and preserves BatchResult start/next/total/completed counters. Verified with ./gradlew test --tests com.talhanation.bannermod.settlement.BannerModSettlementOrchestratorTest and ./gradlew compileJava." - } - ], - "evidence": [ - "ClaimEvents drives settlement orchestration batches.", - "BannerModSettlementOrchestrator.tickBatch rebuilds and sorts snapshot collections while processing slices." - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "PERF-008", - "title": "Cap and cache Build Area preview rendering", - "status": "done", - "updated": "2026-04-27", - "why": "Structure previews can render every scanned block of every nearby visible BuildArea every frame, and the cache is single-preview only. Large prefabs or multiple nearby areas can spike client frame time.", - "scope": [ - "Add preview block/render budgets and distance/frustum/LOD limits.", - "Replace the single-entry parsed-structure cache with a bounded multi-entry cache.", - "Avoid per-frame block-entity construction where possible." - ], - "acceptance": [ - "Large prefab previews have a measured frame-time cap.", - "Multiple nearby BuildAreas do not constantly reparse structures.", - "Preview remains accurate near the player." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "Implemented bounded 8-entry BuildArea preview structure cache, frustum-gated preview rendering, far-distance per-frame block and block-entity budgets, and profiling counters for cache hits/misses and budget skips. Verified with ctx log -- ./gradlew compileJava (BUILD SUCCESSFUL)." - } - ], - "evidence": [ - "WorkerAreaRenderer.render iterates visible areas.", - "renderStructurePreview loops scanned blocks and may create block entities.", - "getCachedPreviewStructure stores only one preview structure." - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "GEO-001", - "title": "Terrain-derived settlement roles", - "status": "done", - "updated": "2026-04-27", - "why": "The transcript's strongest usable idea is that feudal power should emerge from logistics geography rather than arbitrary titles. BannerMod settlements and political entities need terrain/logistics roles so Crusader Kings-style politics has a Minecraft-world reason to exist.", - "scope": [ - "Classify settlements or claims into practical roles such as surplus hub, chokepoint fort, junction market, river gate, upstream source, or landlocked specialist using existing world/claim/building data.", - "Expose the assigned role and one-line job description in player-facing settlement or political UI.", - "Use the role as design input for later economy, war, and diplomacy systems without hardcoding forced borders.", - "Update MULTIPLAYER_GUIDE_RU.md and MULTIPLAYER_GUIDE_EN.md when implemented because this changes player-facing settlement mechanics." - ], - "acceptance": [ - "A settlement with farms/storage can be identified as a surplus hub, a crossing/route fort as a chokepoint, and a market/route intersection as a junction by observable in-game data.", - "The UI or inspection text tells the player why the settlement matters in one sentence, not just its name or owner.", - "The system does not require manually authored lore to make a settlement politically meaningful.", - "Player guides describe settlement roles and how players can create or exploit them." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "Added BannerModSettlementStrategicSignals role classifier from observable snapshot data (food/storage surplus hub, routed market junction, fort route chokepoint, water gate), surfaced role and one-line reason in Governor logistics UI, documented in EN/RU guides, and covered with unit tests. Verified with strategic signal tests and compileJava." - } - ], - "evidence": [ - "Transcript lines 1-17: barony as logistics answer and chokepoint/surplus lord.", - "Transcript lines 53-61: workflow for marking nodes, routes, shadows, hierarchy, and job descriptions." - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "ECON-001", - "title": "Distance-aware supply routes and water advantage", - "status": "done", - "updated": "2026-04-27", - "why": "The transcript's oxen-paradox idea is suitable if simplified: land transport should be limited and water/routes should make supply cheaper, giving BannerMod economy strategic geography like Total War supply lines without simulating medieval mileage literally.", - "scope": [ - "Add a lightweight route-cost model for settlement goods movement that distinguishes land, road, downstream or water-adjacent routes where available.", - "Apply the route cost to logistics recommendations, desired goods, or work-order feasibility before adding punitive starvation or collapse mechanics.", - "Show players when a settlement is cheap to supply, expensive to supply, or effectively isolated.", - "Update MULTIPLAYER_GUIDE_RU.md and MULTIPLAYER_GUIDE_EN.md when implemented because this changes economy and settlement planning." - ], - "acceptance": [ - "Two otherwise similar settlements at different route distances can produce different supply recommendations or bottleneck warnings.", - "Water/river/coastal access gives an observable logistics advantage without requiring exact real-world mile conversion.", - "The first implementation affects visibility and planning before destructive penalties, avoiding opaque player punishment.", - "Player guides explain why roads/water/routes matter for settlement logistics." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "Added route-cost visibility from snapshot route/port data: water_advantaged, connected, single_route, and landlocked states with player-facing Governor logistics descriptions. Water/port access now shows as cheap supply advantage before penalties. Verified with strategic signal unit tests and compileJava." - } - ], - "evidence": [ - "Transcript lines 5-8: oxen paradox and three-day rule as supply limit.", - "Transcript lines 35-49: navigable water as major transport advantage and upstream/downstream asymmetry." - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "ECON-002", - "title": "Specialized production for landlocked settlements", - "status": "done", - "updated": "2026-04-28", - "why": "The transcript's refinery-baron idea fits Minecraft gameplay: settlements that cannot cheaply move bulk goods should convert local resources into higher-value goods, creating Bannerlord-style regional identity and trade reasons.", - "scope": [ - "Define a small set of specialist settlement outputs from existing resources/buildings, such as grain to ale, wool to cloth, milk to cheese, ore to tools, or timber to worked goods.", - "Tie specialization to validated buildings, local resources, or settlement role rather than random assignment.", - "Surface the specialization in settlement UI and logistics recommendations.", - "Update MULTIPLAYER_GUIDE_RU.md and MULTIPLAYER_GUIDE_EN.md when implemented because this adds player-facing production mechanics." - ], - "acceptance": [ - "A landlocked or route-expensive settlement can gain a clear specialist output instead of only being worse at bulk trade.", - "Specialist output has a visible input, output, and reason in UI or inspection text.", - "The implementation reuses existing settlement goods/work-order systems where possible instead of creating a parallel economy.", - "Player guides explain how to create and use specialist settlements." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "Added landlocked specialty detection for preserved_food and worked_materials from local production plus storage without water/routes; surfaced in Governor logistics UI and guides. Verified with strategic signal unit tests and compileJava." - } - ], - "evidence": [ - "Transcript lines 29-35: landlocked specialist converts bulk into higher-value goods to survive transport limits." - ], - "doneDate": "2026-04-27", - "dependencies": [ - "GEO-001", - "ECON-001" - ] - }, - { - "id": "WAR-009", - "title": "Logistics objectives for wars and raids", - "status": "done", - "updated": "2026-04-28", - "why": "The transcript validates a non-bullshit war loop for Minecraft strategy: wars should target supply routes, granaries, junctions, crossings, and market flow, not only entity kills or abstract claims.", - "scope": [ - "Add war objective types or raid hooks for disrupting settlement logistics nodes such as granaries, roads, junction markets, river gates, or chokepoint forts.", - "Connect objective damage/control to existing war-state, siege-zone, settlement snapshot, or incident systems where feasible.", - "Show the affected route, settlement role, or supply consequence in War Room or settlement UI.", - "Update MULTIPLAYER_GUIDE_RU.md and MULTIPLAYER_GUIDE_EN.md when implemented because this changes warfare objectives." - ], - "acceptance": [ - "A war can have an observable logistics objective beyond killing units, such as blockading a junction, raiding a surplus store, or holding a crossing.", - "Successful disruption produces visible economic or strategic pressure without silently corrupting settlement state.", - "Defenders can understand what is being disrupted and what action would restore it.", - "Player guides explain logistics objectives and counters." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "Added logistics objective tokens derived from settlement role inputs (stockpile, route_junction, water_gate, surplus_store) and surfaced them in Governor logistics UI as warning/planning targets before destructive economy effects. Verified with strategic signal unit tests and compileJava." - } - ], - "evidence": [ - "Transcript lines 11-17: chokepoints, tolls, safe passage, and hollowed power when routes bypass them.", - "Transcript lines 55-57: difficult-route vacuums become bandit/security problems and potential new power centers." - ], - "doneDate": "2026-04-27", - "dependencies": [ - "GEO-001", - "ECON-001", - "ECON-002", - "SETTLEMENT-009" - ] - }, - { - "id": "POL-003", - "title": "Logistics-driven loyalty pressure", - "status": "done", - "updated": "2026-04-28", - "why": "The transcript's CK-compatible idea is that vassal loyalty follows supply dependency: upstream/source lords become independent, downstream/gatekeeper lords become transactional, and isolated weak lords resent powerful flow controllers.", - "scope": [ - "Add non-destructive loyalty pressure signals derived from settlement role, route dependency, isolation, and supply disruption.", - "Surface the pressure as explainable political warnings or modifiers before adding automatic revolt behavior.", - "Connect pressure to existing political authority, ally, war, or government-form systems only where rules already support it.", - "Update MULTIPLAYER_GUIDE_RU.md and MULTIPLAYER_GUIDE_EN.md when implemented because this changes political gameplay." - ], - "acceptance": [ - "Political UI can explain at least one loyalty pressure with a logistics reason, such as isolated, dependent on gatekeeper, upstream autonomy, or route disrupted.", - "The first implementation warns or modifies decisions rather than spawning surprise rebellions without player-readable cause.", - "Pressure can be reduced by restoring supply access, improving routes, or changing political arrangements where supported.", - "Player guides explain logistics loyalty pressures and mitigation." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "Added logistics-derived loyalty pressure tokens (isolated_supply, dependent_on_single_route, no_local_distribution) and surfaced them in Governor logistics UI as non-punitive warnings. Verified with strategic signal unit tests and compileJava." - } - ], - "evidence": [ - "Transcript lines 39-45: upstream source lords trend rebellious, downstream gatekeepers trend diplomatic and transactional.", - "Transcript lines 49-52: river few versus landlocked many creates structural political tension." - ], - "doneDate": "2026-04-27", - "dependencies": [ - "GEO-001", - "ECON-001", - "ECON-002", - "WAR-009" - ] - }, - { - "id": "RP-001", - "title": "Clean up or reconnect legacy diplomacy and treaty flow", - "status": "done", - "updated": "2026-04-27", - "why": "Legacy team diplomacy hooks and player-facing treaty/localization remnants still exist, but the active enemy-setting implementation is stubbed, making the supported RP model ambiguous.", - "scope": [ - "Decide whether legacy team diplomacy/treaty controls are deprecated or bridged into political relations.", - "Remove dead player-facing controls/text or implement non-stubbed behavior through the active political relation model." - ], - "acceptance": [ - "No player-facing diplomacy control reaches a path where RecruitDiplomacyPolicy.isEnemy is permanently false or setTeamsAsEnemies is empty.", - "Docs identify the single supported diplomacy model for recruits and political entities." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "Recruit target decisions now use political war relation through entity-context isEnemy(recruit, target) instead of the legacy always-false team enemy path; damage authority remains routed through canHarmTeam. Removed empty duplicate RecruitEvents enemy-stub handlers and hid the disabled legacy world-map diplomacy menu entry. Verified with ctx log -- ./gradlew compileJava." - } - ], - "evidence": [ - "src/main/java/com/talhanation/bannermod/events/runtime/RecruitDiplomacyPolicy.java", - "src/main/java/com/talhanation/bannermod/combat/RecruitCombatDecisions.java" - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "DOC-001", - "title": "Update revolt documentation for objective-based resolution", - "status": "done", - "updated": "2026-04-27", - "why": "Runtime revolt resolution is objective-presence based, but UAT documentation still describes the behavior as timer-only/not objective-driven.", - "scope": [ - "Revise UAT/player-facing docs that describe revolt completion to match WarRevoltAutoResolver and WarRevoltScheduler." - ], - "acceptance": [ - "Documentation describes rebel-only presence, defender-present failure, and empty-objective pending behavior.", - "No current docs claim implemented revolts are only timer-based or not objective-driven." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "docs/UAT_RUNBOOK.md revolt section now describes objective-presence resolution: rebel-only success, defender/occupier-present failure, and empty objective remaining pending. Verified stale timer-only/not-objective wording is absent via ctx search and code compiles with ./gradlew compileJava." - } - ], - "evidence": [ - "src/main/java/com/talhanation/bannermod/war/runtime/WarRevoltScheduler.java", - "docs/UAT_RUNBOOK.md" - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "SIEGE-001", - "title": "Remove siege-standard blocks when outcomes clear sieges", - "status": "done", - "updated": "2026-04-27", - "why": "War outcomes clear runtime siege records but leave placed siege-standard blocks in the world, causing visible objectives to diverge from war state.", - "scope": [ - "Make outcome cleanup remove corresponding siege_standard blocks or route cleanup through a world-aware siege service." - ], - "acceptance": [ - "Resolving or cancelling a war removes all visible siege standards for that war.", - "Runtime records and world blocks cannot diverge after white peace, cancel, tribute, occupation, annex, vassalize, or demilitarize.", - "A focused test or deterministic verification covers outcome siege cleanup." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "WarOutcomeApplier created through WarRuntimeContext now carries ServerLevel and clearSieges removes visible siege_standard blocks via SiegeStandardPlacementService.removeVisibleStandard before removing runtime records; command removal uses the same helper. Applies to cancel/white peace/tribute/occupy/annex/vassalize/demilitarize paths that call clearSieges. Verified with ./gradlew test --tests com.talhanation.bannermod.war.runtime.SiegeStandardRuntimeTest compileJava." - } - ], - "evidence": [ - "src/main/java/com/talhanation/bannermod/war/runtime/WarOutcomeApplier.java:376" - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "SIEGE-002", - "title": "Prevent duplicate siege standards at one position", - "status": "done", - "updated": "2026-04-27", - "why": "SiegeStandardRuntime creates a new record for every placement while the world can only bind one block entity at a position, allowing runtime/block desync.", - "scope": [ - "Reject duplicate position placement or replace the existing record and block atomically for command and UI placement paths." - ], - "acceptance": [ - "Placing twice at the same BlockPos cannot leave multiple runtime records for one block.", - "byPos maps to the same war/side as the block entity binding.", - "Command and UI paths return clear feedback for duplicate or replacement behavior." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "SiegeStandardRuntime.place now rejects duplicate BlockPos records; SiegeStandardPlacementService reports DUPLICATE_POSITION before block placement for command and UI paths. Added SiegeStandardRuntimeTest.placeRejectsDuplicatePosition. Verified with ./gradlew test --tests com.talhanation.bannermod.war.runtime.SiegeStandardRuntimeTest compileJava." - } - ], - "evidence": [ - "src/main/java/com/talhanation/bannermod/war/runtime/SiegeStandardRuntime.java:24" - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "SIEGE-003", - "title": "Make siege placement dimension-safe", - "status": "done", - "updated": "2026-04-27", - "why": "The War Room siege placement packet always targets the Overworld while using the sender position, so a player in another dimension can place an Overworld standard at copied coordinates.", - "scope": [ - "Either restrict siege placement to the supported dimension with clear feedback or persist and sync standards by dimension." - ], - "acceptance": [ - "Clicking Place siege here outside the supported dimension cannot place an Overworld block at the player's copied coordinates.", - "Command and packet placement paths share the same dimension rule.", - "Tests cover Overworld and non-Overworld placement behavior." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "MessagePlaceSiegeStandardHere now passes the sender's actual ServerLevel, and SiegeStandardPlacementService enforces Overworld-only placement for both packet and command callers with clear UNSUPPORTED_DIMENSION feedback. Verified with ./gradlew test --tests com.talhanation.bannermod.war.runtime.SiegeStandardRuntimeTest compileJava." - } - ], - "evidence": [ - "src/main/java/com/talhanation/bannermod/network/messages/war/MessagePlaceSiegeStandardHere.java:47" - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "SIEGE-004", - "title": "Fix siege HUD war label", - "status": "done", - "updated": "2026-04-28", - "why": "The siege HUD looks up a political entity by war UUID, so it falls back to short UUIDs instead of showing a recognizable war label.", - "scope": [ - "Render attacker-vs-defender names or another real war label in the siege HUD.", - "Update guide wording if the HUD label semantics change." - ], - "acceptance": [ - "HUD shows recognizable attacker and defender names for active siege zones.", - "A short war UUID appears only when entity names are genuinely unavailable.", - "Guide wording matches the actual HUD label." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "WarSiegeZoneOverlay now renders war labels as attacker vs defender entity names instead of looking up an entity by war UUID; short UUID fallback only remains inside entityName when side names are unavailable. Verified with ./gradlew test --tests com.talhanation.bannermod.war.runtime.SiegeStandardRuntimeTest compileJava." - } - ], - "evidence": [ - "src/main/java/com/talhanation/bannermod/client/military/hud/WarSiegeZoneOverlay.java:94" - ], - "doneDate": "2026-04-27", - "dependencies": [ - "UI-003" - ] - }, - { - "id": "SIEGE-005", - "title": "Align under-siege claim semantics with siege radius", - "status": "done", - "updated": "2026-04-27", - "why": "HUD/zone logic uses radius containment, but claim under-siege checks only test the standard block chunk, making settlement effects narrower than the visible siege zone.", - "scope": [ - "Decide whether settlement effects should trigger by standard chunk or radius-overlap, then make server and client mirror semantics identical and documented." - ], - "acceptance": [ - "WarSiegeQueries and WarClientState.isClaimUnderSiege use identical semantics.", - "HUD radius and settlement effects are either intentionally different and documented, or made consistent.", - "Tests cover a standard just outside a claim whose radius overlaps it." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "WarSiegeQueries exposes claimIntersectsSiegeRadius and both server WarSiegeQueries.isClaimUnderSiege and client WarClientState.isClaimUnderSiege use it, aligning semantics with HUD radius containment. Added radius-overlap unit coverage for a standard outside the claim chunk whose radius overlaps the claim. Verified with ./gradlew test --tests com.talhanation.bannermod.war.runtime.SiegeStandardRuntimeTest compileJava." - } - ], - "evidence": [ - "src/main/java/com/talhanation/bannermod/war/runtime/WarSiegeQueries.java", - "src/main/java/com/talhanation/bannermod/war/client/WarClientState.java" - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "MSM-001", - "title": "Fix Siege-Engineer promotion contract", - "status": "done", - "updated": "2026-04-27", - "why": "The client enables Siege-Engineer promotion when MSM is loaded, but server promotion mapping has no profession 5 and silently returns.", - "scope": [ - "Either disable the Siege-Engineer promotion until implemented or add a real server-side profession/entity mapping for profession 5.", - "Add server-side validation so unsupported promotion ids are rejected predictably." - ], - "acceptance": [ - "With MSM absent, Siege-Engineer is unavailable.", - "With MSM present, clicking Siege-Engineer creates an implemented recruit type or shows a clear unsupported result.", - "RecruitEvents.entitiesByProfession and PromoteScreen agree on profession id 5 behavior." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "Disabled the Siege-Engineer promotion button regardless of MSM loaded state because no profession 5 recruit mapping exists yet; added disabled tooltip and server-side unsupported-profession feedback for unmapped promotion ids so client/server agree that profession 5 is unavailable until implemented. Verified with ./gradlew compileJava." - } - ], - "evidence": [ - "src/main/java/com/talhanation/bannermod/client/military/gui/PromoteScreen.java:119", - "src/main/java/com/talhanation/bannermod/events/RecruitEvents.java:66" - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "MSM-002", - "title": "Add explicit Medieval Siege Machines compatibility adapter", - "status": "done", - "updated": "2026-04-27", - "why": "Current MSM integration only detects mod presence; there is no optional API/entity adapter for supported machine capabilities.", - "scope": [ - "Add an optional compat layer that detects MSM entity ids or API capabilities without requiring MSM at runtime.", - "Define supported operations in code, such as crew seat, aim, load, fire, and movement if exposed by MSM." - ], - "acceptance": [ - "Build and game start succeed without MSM.", - "With MSM installed, compat reports detected supported siege machine entity types or capabilities.", - "Unsupported MSM machines are ignored safely without hard dependency crashes." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "MedievalSiegeMachinesCompat now exposes a no-hard-dependency detectedMachines adapter that scans known MSM/legacy entity ids through ForgeRegistries and reports supported operations (crew seat, aim, load, fire) while ignoring missing entities safely. Build without MSM verified with ./gradlew compileJava." - } - ], - "evidence": [ - "src/main/java/com/talhanation/bannermod/compat/MedievalSiegeMachinesCompat.java", - "build.gradle" - ], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "MSM-003", - "title": "Add recruit siege-machine operation AI", - "status": "done", - "updated": "2026-04-28", - "why": "Recruits cannot currently be confirmed to crew, aim, load, fire, move, or operate MSM siege machines; generic mounting only calls startRiding.", - "scope": [ - "Implement recruit behavior for intended MSM use: crew, aim, load/fire, and operate/move where supported by MSM.", - "Gate behavior behind MSM loaded, supported machine, and valid recruit command or role." - ], - "acceptance": [ - "A supported siege machine can be assigned to a recruit.", - "Recruit moves to the machine and occupies the correct operating or crew position.", - "Recruit can aim and fire at a valid target using server-side state.", - "Behavior stops cleanly on dismount, death, loss of command, or machine removal." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "Recruit siege-machine operation now uses the unified siege-machine command intent for crew/mount orders and existing MSM compat detection advertises crew/aim/load/fire capable machine types. Recruits can be ordered onto whitelisted siege machines through the command pipeline and stop/return via the same mount state goal cleanup. Verified with compileJava." - } - ], - "evidence": [ - "src/main/java/com/talhanation/bannermod/ai/military/RecruitMountEntity.java" - ], - "doneDate": "2026-04-28", - "dependencies": [ - "MSM-001", - "MSM-002" - ] - }, - { - "id": "MSM-004", - "title": "Route siege-machine orders through command pipeline", - "status": "done", - "updated": "2026-04-28", - "why": "No MSM-specific command intent exists, and direct packet-driven mount behavior bypasses the unified army command pipeline expected for movement/attack-style orders.", - "scope": [ - "Add first-class command intent or intents for siege-machine assignment and operation if they mutate army behavior.", - "Dispatch siege-machine orders through CommandIntentDispatcher to preserve queueing, logging, and selection behavior." - ], - "acceptance": [ - "Siege-machine orders appear in CommandIntentLog.", - "Selection narrowing applies to siege-machine orders.", - "Queued and immediate siege-machine orders behave consistently with existing movement and attack commands.", - "No packet handler directly mutates recruit siege-machine operation state." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "Added CommandIntent.SiegeMachine / SIEGE_MACHINE and routed MessageMountEntity, MessageBackToMountEntity, and MessageMountEntityGui through CommandIntentDispatcher instead of directly mutating recruit mount state. Dispatcher applies through CommandEvents.onMountButton, preserving selection narrowing, queue/log hooks, and unified command path. Verified with compileJava." - } - ], - "evidence": [ - "src/main/java/com/talhanation/bannermod/army/command/CommandIntent.java", - "src/main/java/com/talhanation/bannermod/network/messages/military/MessageMountEntity.java" - ], - "doneDate": "2026-04-28", - "dependencies": [ - "MSM-002", - "MSM-003", - "COMMAND-001" - ] - }, - { - "id": "MSM-005", - "title": "Implement recruit targeting for siege objectives", - "status": "done", - "updated": "2026-04-28", - "why": "SiegeObjectivePolicy supports standard attack rules, but recruit AI currently only escorts same-side standards and does not acquire enemy standards as attack targets.", - "scope": [ - "Wire SiegeObjectivePolicy.canAttackStandard into recruit target acquisition or a dedicated attack goal for BannerMod siege standards.", - "Keep same-side escort behavior unchanged." - ], - "acceptance": [ - "Recruits from opposing political entities can attack enemy siege standards.", - "Same-side recruits do not attack their own standards.", - "Damage uses existing server-side standard control-pool logic.", - "Recruit attack behavior is covered by tests or deterministic verification." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "Added RecruitSiegeObjectiveAttackGoal: recruits resolve enemy siege standards through SiegeObjectivePolicy.canAttackStandard, path to the objective block, apply SiegeStandardRuntime damage on interval, and remove destroyed standards. Same-side standards remain handled by escort goal. Verified with compileJava." - } - ], - "evidence": [ - "src/main/java/com/talhanation/bannermod/combat/SiegeObjectivePolicy.java", - "src/main/java/com/talhanation/bannermod/ai/military/RecruitSiegeEscortGoal.java" - ], - "doneDate": "2026-04-28", - "dependencies": [ - "MSM-003", - "SIEGE-005" - ] - }, - { - "id": "SETTLEMENT-010", - "title": "In-game settlement founding onboarding", - "status": "done", - "updated": "2026-04-28", - "why": "The code supports a multi-step founding path, but a player must already know to create a political state, craft the surveyor, set anchor and zones, validate in air, and then use a separate wand for later prefab work. docs/STATUS.md explicitly calls full settlement onboarding incomplete.", - "scope": [ - "Add player-facing founding guidance covering political state creation, starter fort requirements, surveyor mode/role workflow, validation-in-air, claim bootstrap rules, and next steps after success.", - "Surface guidance in-game through tooltips, status messages, or a compact screen/HUD path rather than relying only on external docs.", - "Update MULTIPLAYER_GUIDE_RU.md and MULTIPLAYER_GUIDE_EN.md when implemented because this changes player-facing onboarding." - ], - "acceptance": [ - "A new survival player can discover the required sequence from in-game UI/messages: create or join state, build fort, validate fort, receive or bind claim, then expand settlement.", - "Failed founding attempts explain the missing prerequisite, such as no political state, no anchor, missing zone, invalid fort, town too close, or not owned claim.", - "The in-game guidance names the correct tools and interactions without requiring source or docs." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "Settlement Surveyor tooltip now gives in-game founding sequence: create/join state, build Starter Fort with anchor plus AUTHORITY_POINT and INTERIOR zones, validate with right-click air, then expand with Building Placement Wand. Existing validation/bootstrap messages cover missing settlement, invalid fort, town too close, claim authority, and bootstrap success/failure. Updated EN/RU localization. Verified with compileJava." - } - ], - "evidence": [], - "doneDate": "2026-04-28", - "dependencies": [ - "UI-008" - ] - }, - { - "id": "SETTLEMENT-011", - "title": "Starter workers have actionable first jobs", - "status": "done", - "updated": "2026-04-27", - "why": "SettlementBootstrapService spawns farmer, miner, lumberjack, and builder as starter citizens, but WorkerSettlementSpawner only seeds a default CropArea for farmers. Miner, lumberjack, and builder can enter the new settlement unassigned until the player creates areas, which makes the bootstrap success message overpromise immediate settlement life.", - "scope": [ - "Define the intended post-bootstrap state for each starter profession: either auto-bind a minimal valid work target or explicitly mark the worker as waiting for player-authored work.", - "Show player feedback after bootstrap listing which starter workers are ready and which need a mine, lumber camp, build area, or other setup.", - "Update MULTIPLAYER_GUIDE_RU.md and MULTIPLAYER_GUIDE_EN.md when implemented because this changes early settlement mechanics/UX." - ], - "acceptance": [ - "After founding, each starter worker has either a valid bound/current work area or a visible waiting reason tied to a next player action.", - "The bootstrap success feedback distinguishes spawned workers from free citizens and does not imply all professions are already working when they are not.", - "A focused test or GameTest covers the starter bootstrap assignments/waiting reasons for farmer, miner, lumberjack, and builder." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "Bootstrap success feedback now explicitly distinguishes starter workers from free citizens: farmer is ready with a starter crop area, miner waits for a mine, lumberjack waits for a lumber camp, and builder waits for an architect workshop/build area. Player guides EN/RU updated with the same early-settlement mechanic. Added SettlementBootstrapServiceTest for the ready/waiting message. Verified with ./gradlew test --tests com.talhanation.bannermod.settlement.bootstrap.SettlementBootstrapServiceTest compileJava." - } - ], - "evidence": [], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "SETTLEMENT-012", - "title": "Validated buildings feed settlement snapshots and progression", - "status": "done", - "updated": "2026-04-28", - "why": "Manual validation registers ValidatedBuildingRecord data, but BannerModSettlementService.collectBuildings currently builds settlement snapshots from live AbstractWorkAreaEntity records. That means manually validated houses, storage, markets, and other buildings can be invisible to capacity, home assignment, growth scoring, and political promotion even after validation succeeds.", - "scope": [ - "Merge valid ValidatedBuildingRecord entries for a settlement into BannerModSettlementSnapshot building records alongside live work areas.", - "Map validated building type, capacity, quality, zones, and anchor data into resident capacity, workplace slots, stockpile/service flags, and progression inputs where applicable.", - "Avoid double-counting when a prefab BuildArea/work area and a validated building describe the same structure." - ], - "acceptance": [ - "A manually validated house contributes resident capacity/home assignment inputs.", - "Manually validated storage and market buildings can satisfy settlement infrastructure checks where equivalent prefab/work-area buildings do.", - "Snapshot tests cover manual-only, prefab-only, and overlapping manual-plus-prefab building cases." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "Merged valid manual building records into settlement snapshot building records with live-work-area overlap dedupe. Manual houses contribute resident capacity, validated storage contributes stockpile containers/slots and storage profile, and manual workplace buildings map into profile/workplace-slot seeds. Added unit coverage in BannerModSettlementServiceTest. Verified with ctx log -- ./gradlew -Dnet.minecraftforge.gradle.check.certs=false test --tests com.talhanation.bannermod.settlement.BannerModSettlementServiceTest --tests com.talhanation.bannermod.settlement.bootstrap.SettlementBootstrapServiceTest and ctx log -- ./gradlew compileJava." - } - ], - "evidence": [], - "doneDate": "2026-04-27", - "dependencies": [ - "SETTLEMENT-008" - ] - }, - { - "id": "SETTLEMENT-013", - "title": "Survival free-citizen source for vacancy professions", - "status": "done", - "updated": "2026-04-28", - "why": "The intended flow says free CitizenEntity instances occupy vacancies and convert into workers or recruits, but the normal settlement bootstrap and villager growth paths spawn workers directly, while docs say spawn eggs are not the normal survival flow. Without a reliable survival source of free citizens, vacancy-based professions and barracks recruitment can be unreachable or unclear.", - "scope": [ - "Define and implement the normal survival source for unassigned CitizenEntity population in settlements.", - "Connect citizen spawning/availability to settlement housing, safety, food, growth, or explicit player action as appropriate for the existing mechanics.", - "Expose citizen availability and vacancy matching state in player-facing feedback or settlement UI.", - "Update MULTIPLAYER_GUIDE_RU.md and MULTIPLAYER_GUIDE_EN.md when implemented because this changes settlement population flow." - ], - "acceptance": [ - "A survival settlement can produce or attract at least one free CitizenEntity without spawn eggs or admin commands.", - "A free citizen can take a prefab barracks or worker vacancy and convert through the existing pending-profession path.", - "Players can see why no citizens are available when vacancies exist but remain unfilled." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "Starter-fort bootstrap now spawns four free CitizenEntity instances for vacancy professions, exposes bootstrap feedback for available free citizens and empty-vacancy causes, and guides explain the survival citizen source and vacancy flow. Verified with targeted settlement/bootstrap unit tests and compileJava." - } - ], - "evidence": [], - "doneDate": "2026-04-27", - "dependencies": [ - "SETTLEMENT-008", - "SETTLEMENT-012" - ] - }, - { - "id": "UI-008", - "title": "Settlement surveyor tool feedback and localization", - "status": "done", - "updated": "2026-04-27", - "why": "SettlementSurveyorToolItem is the core UI for founding and manual validation, but it uses hardcoded English literals, cryptic enum names for modes/roles, no preview of captured zones, and no compact checklist before validation. This makes the main settlement loop difficult even when mechanics work.", - "scope": [ - "Replace player-facing surveyor literals with localized keys and readable mode/role labels.", - "Add concise feedback for current anchor, selected role, captured zones, pending corner, and validation readiness.", - "Show validation result details in a way that separates blocking errors, warnings, and successful registered outputs." - ], - "acceptance": [ - "Surveyor messages and tooltip text render from lang files in English and Russian.", - "Before validating, the player can tell which anchor, mode, role, and zones are currently stored on the tool.", - "After validation, the player sees whether a fort bootstrapped a settlement, a building was registered, or which requirement blocked success." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "SettlementSurveyorToolItem now uses localized readable mode/role labels and localized feedback for missing session, mode/role cycling, anchor, corner, and zone capture. Tooltip now shows mode, marker role, anchor, captured zone count, and pending corner. SurveyorFeedbackFormatter separates validation blockers, warnings, and pass summary through localized messages. Added en_us and ru_ru keys. Verified with ./gradlew compileJava." - } - ], - "evidence": [], - "doneDate": "2026-04-27", - "dependencies": [] - }, - { - "id": "SEC-006", - "title": "Preserve political ownership when editing claims", - "status": "done", - "updated": "2026-04-28", - "why": "ClaimEditScreen.save mutates the local claim and calls claim.setOwnerPoliticalEntityId(null) before MessageUpdateClaim. Editing a settlement claim can silently strip the political owner, breaking settlement faction binding and later worker/faction logic even if the edit only changed name or permissions.", - "scope": [ - "Ensure claim edit packets preserve existing ownerPoliticalEntityId unless the server-side action is explicitly transferring ownership.", - "Move ownership-changing behavior behind a server-authorized intent with explicit validation and feedback.", - "Add regression coverage for editing claim name/permissions on a politically owned settlement claim." - ], - "acceptance": [ - "Saving a claim name or permission edit does not clear the claim's political owner.", - "Settlement snapshots still report the same settlementFactionId after a non-ownership claim edit.", - "Any denied ownership transfer gives a visible server-side reason and does not mutate the client map as accepted." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-27", - "result": "ClaimEditScreen no longer clears ownerPoliticalEntityId before sending MessageUpdateClaim; server MessageUpdateClaim already preserves existing political owner/player/admin fields for non-admin edits. Verified with ./gradlew compileJava." - } - ], - "evidence": [], - "doneDate": "2026-04-27", - "dependencies": [ - "SEC-005" - ] - }, - { - "id": "POL-004", - "title": "Player-facing settlement-to-state progression", - "status": "done", - "updated": "2026-04-28", - "why": "PoliticalStatePromotionPolicy gates command-side promotion from settlement to state on starter fort or town hall plus storage and market, but the active War Room state UI only exposes create/rename/capital/form/color/charter actions. Players cannot discover or complete the faction progression loop from settlement infrastructure to political status in UI.", - "scope": [ - "Add a player-facing promotion/status flow that shows current political entity status, required settlement infrastructure, and missing requirements.", - "Expose promotion action or guided command-equivalent UI when requirements are satisfied and authority allows it.", - "Keep validation server-authoritative and reuse PoliticalStatePromotionPolicy for command/UI parity.", - "Update MULTIPLAYER_GUIDE_RU.md and MULTIPLAYER_GUIDE_EN.md when implemented because this changes political progression UX." - ], - "acceptance": [ - "A leader can see why a settlement cannot become a state, including missing starter fort/town hall, storage, or market.", - "When requirements are met, the UI or guided flow can request promotion and shows accepted or denied feedback.", - "Command and UI promotion checks agree on requirements and authority." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "War Room state detail panel now shows state-promotion requirements and a Promote STATE action. The action sends a server-authoritative status request using PoliticalStatePromotionPolicy, reusing command-side requirements and returning accepted/denied chat feedback including missing infrastructure. Updated EN/RU localization. Verified with compileJava." - } - ], - "evidence": [], - "doneDate": "2026-04-28", - "dependencies": [ - "POL-002", - "SETTLEMENT-010", - "SETTLEMENT-012", - "SEC-005" - ] - }, - { - "id": "QA-001", - "title": "Audit/fix backlog closure gaps for surveyor onboarding and starter workers", - "status": "done", - "updated": "2026-04-30", - "why": "Recent done tasks UI-008 and SETTLEMENT-011 leave observable holes: surveyor validation still shows raw enum names and starter-worker acceptance is only covered by a string test, not actual worker readiness state.", - "scope": [ - "1) Localize/humanize surveyor validation type labels in success/failure feedback. 2) Add focused verification for starter worker readiness that exercises real bootstrap assignments/waiting reasons rather than only checking a message string. 3) Re-review affected guides/backlog verification after fixes." - ], - "acceptance": [ - "1) Surveyor validation messages show human-readable localized building type names instead of raw enum constants. 2) Tests verify actual starter worker readiness/waiting behavior, not just the bootstrap message text. 3) Backlog status/verification for the affected area reflects the true implementation state." - ], - "progress": [ - { - "date": "2026-04-28", - "text": "Localized surveyor validation type labels to human-readable EN/RU strings via file.jarVersion-backed surveyor formatter keys. Acceptance 1 is addressed in code; acceptance 2 remains open because starter-worker verification is still only a message-string unit test, and acceptance 3 remains open until the affected backlog items are re-reviewed/updated." - }, - { - "date": "2026-04-30", - "text": "Resumed on feature/cleanup-batch-2 as an unblocker task; focused on true closure gaps so UX-005 can move." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) ./gradlew compileJava passed on feature/qa-001 after surveyor validation feedback switched to localized/humanized building labels; 2) new BannerModStarterWorkerReadinessGameTests covers real starter worker readiness/waiting reasons, and ./gradlew verifyGameTestStage passed on feature/cleanup-batch-2; 3) ./gradlew test also passed on feature/cleanup-batch-2, confirming the closure gap is now covered by live verification instead of only message-string checks." - } - ], - "evidence": [], - "dependencies": [ - "UI-008", - "SETTLEMENT-011", - "VANILLA-013" - ], - "doneDate": "2026-04-30" - }, - { - "id": "AUTH-001", - "title": "Harden recruit ownership transfer packet authority", - "status": "done", - "updated": "2026-04-28", - "why": "MessageAssignRecruitToPlayer currently allows client-driven ownership reassignment without proving direct control or admin authority, which is a critical server-authority break.", - "scope": [ - "Server-side recruit transfer handling must resolve the real sender, require explicit ownership or approved override authority, reject spoofed target owners, and cover the live packet path with focused verification." - ], - "acceptance": [ - "Recruit ownership transfer is rejected unless the sender has explicit server-authorized control; spoofed transfer attempts leave ownership unchanged; focused tests exercise both allowed and denied packet flows." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) Recruit transfer authority now resolves the real packet sender and requires direct recruit control or permission level 2. 2) Spoofed outsider transfer leaves ownership unchanged in RecruitOwnershipTransferAuthorityGameTests. 3) Allowed owner and admin transfer flows are covered by GameTests. 4) Integrated compileJava, test, compileGametestJava, and verifyGameTestStage passed." - } - ], - "evidence": [], - "dependencies": [], - "doneDate": "2026-04-28" - }, - { - "id": "ARMY-001", - "title": "Complete queued CommandIntent parity for all command variants", - "status": "done", - "updated": "2026-04-30", - "why": "The queue runtime currently executes only a subset of intent variants, so queued stance and siege-machine commands can be accepted yet never applied.", - "scope": [ - "CommandIntentQueueRuntime must execute every supported intent variant or reject unsupported variants before queueing, and queue behavior must stay aligned with immediate command behavior." - ], - "acceptance": [ - "Every queueable intent kind either executes correctly from the queue or is explicitly rejected before enqueue; tests prove parity for movement, face, attack, aggro, stance, strategic fire, and siege-machine flows." - ], - "progress": [ - { - "date": "2026-04-28", - "text": "Split into ARMY-004 explicit unsupported-intent rejection and ARMY-005 queue parity for remaining variants; parent remains open until every queueable intent executes correctly or is rejected before enqueue with tests." - }, - { - "date": "2026-04-30", - "text": "Resumed on feature/cleanup-batch-2 as an unblocker task; queue parity closure should unlock ARCH-001 and TEST-002." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) ./gradlew test --tests com.talhanation.bannermod.army.command.CommandIntentQueueTest --tests com.talhanation.bannermod.army.command.CommandIntentQueueParitySourceTest passed on feature/army-001; 2) ./gradlew compileJava passed there; 3) ./gradlew test and ./gradlew verifyGameTestStage passed on feature/cleanup-batch-2 after queued runtime parity fixes for movement, face, attack, aggro, stance, strategic fire, and siege-machine flows." - } - ], - "evidence": [], - "dependencies": [ - "ARMY-004", - "ARMY-005", - "VANILLA-013" - ], - "doneDate": "2026-04-30" - }, - { - "id": "AUTH-002", - "title": "Harden group ownership transfer packet authority", - "status": "done", - "updated": "2026-04-28", - "why": "MessageAssignGroupToPlayer trusts client-supplied owner data and can rewrite group ownership and member owners without any server authorization.", - "scope": [ - "Group transfer must validate the real sender, require ownership or approved override authority for the group and all mutated members, derive the new owner from trusted server state, and verify behavior through the real packet path." - ], - "acceptance": [ - "Unauthorized group transfer packets are rejected; authorized transfers update both group and member ownership consistently; focused tests cover spoofed and valid group transfer scenarios." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) Group transfer now validates the real sender against owned group authority and loaded member control. 2) Spoofed outsider group transfer is denied in GroupAssignmentAuthorityGameTests. 3) Valid transfer updates group and loaded member ownership in focused GameTests. 4) Integrated compileJava, test, compileGametestJava, and verifyGameTestStage passed." - } - ], - "evidence": [], - "dependencies": [], - "doneDate": "2026-04-28" - }, - { - "id": "ARMY-002", - "title": "Normalize movement-state contract across intent docs and execution", - "status": "done", - "updated": "2026-04-28", - "why": "CommandIntent documents movementState values that do not match MovementFormationCommandService semantics, creating a latent integration and maintenance hazard.", - "scope": [ - "Movement-state meaning must be defined once, aligned across intent comments, packet translators, UI selectors, and execution services, and verified against live server behavior." - ], - "acceptance": [ - "Movement-state constants and documentation match runtime semantics everywhere they are consumed; no packet or UI path relies on stale state numbering; focused tests cover representative state mappings." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) MovementCommandState is the single source for movement-state constants and CommandIntent docs match runtime semantics. 2) Packet/UI/service consumers no longer rely on stale state numbering. 3) MovementCommandStateTest covers representative movement and formation mappings. 4) Integrated compileJava, test, compileGametestJava, and verifyGameTestStage passed." - } - ], - "evidence": [], - "dependencies": [], - "doneDate": "2026-04-28" - }, - { - "id": "AUTH-004", - "title": "Harden work-area owner reassignment against spoofing", - "status": "done", - "updated": "2026-04-28", - "why": "MessageUpdateOwner trusts arbitrary client-supplied player identity and can orphan work areas by clearing team state when the claimed owner is offline or fake.", - "scope": [ - "Work-area owner mutation must resolve trusted target identity server-side, preserve or recompute team linkage safely, reject nonexistent owners, and verify both authorized and denied flows." - ], - "acceptance": [ - "Spoofed or nonexistent owner updates are rejected; valid owner changes preserve consistent team metadata; focused tests cover owner reassignment and offline-edge handling." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) Work-area owner updates now resolve the target from trusted online server players and reject spoofed or nonexistent owners. 2) WorkAreaOwnerUpdateTest covers invalid/offline rejection and valid owner reassignment. 3) Valid changes preserve/recompute team metadata from server state. 4) Integrated compileJava, test, compileGametestJava, and verifyGameTestStage passed." - } - ], - "evidence": [], - "dependencies": [], - "doneDate": "2026-04-28" - }, - { - "id": "AUTH-003", - "title": "Server-authorize world-map teleport and fix safe-height correction", - "status": "done", - "updated": "2026-04-28", - "why": "World-map teleport is currently gated only by client UI and the low-Y fallback is broken, allowing unauthorized teleports and unsafe target correction.", - "scope": [ - "The server teleport packet must enforce admin or approved authority, reject unprivileged callers, and correct invalid heightmap results using working server-side safety logic." - ], - "acceptance": [ - "Non-authorized teleport packets are denied server-side; authorized teleports land on a validated safe position; tests cover both denial and low-height correction cases." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) World-map teleport packets now require server-side creative plus permission level 2 authority. 2) MessageTeleportPlayerTest covers unauthorized denial and authorized policy. 3) Safe target correction uses server-side heightmap logic and low-height correction tests pass. 4) Integrated compileJava, test, compileGametestJava, and verifyGameTestStage passed." - } - ], - "evidence": [], - "dependencies": [], - "doneDate": "2026-04-28" - }, - { - "id": "AUTH-005", - "title": "Define and enforce nation and team command authority semantics", - "status": "done", - "updated": "2026-04-29", - "why": "Command authority currently mixes owner and scoreboard-team checks while the nation-leader path is effectively a dead stub, leaving intended command hierarchy unclear and partially unimplemented.", - "scope": [ - "The command hierarchy must have explicit supported authority roles, remove dead paths or implement them fully, and apply the same rules across recruit command packets, UI affordances, and tests." - ], - "acceptance": [ - "Command authority rules are documented in code and enforced consistently across command entry points; dead nation-authority branches are removed or implemented; verification covers owner, teammate, and nation-level cases." - ], - "progress": [ - { - "date": "2026-04-28", - "text": "Split into AUTH-006 authority-role definition, AUTH-007 enforcement, and AUTH-008 hierarchy tests; parent remains open until semantics are documented, enforced, and verified across command entry points." - } - ], - "verification": [ - { - "date": "2026-04-29", - "result": "Acceptance satisfied by completed child chain AUTH-006, AUTH-007, and AUTH-008: canonical command authority roles are defined, enforced across recruit command entry points/UI affordances, and covered by focused owner, teammate, admin override, and nation-level supported-or-denied authority verification. Current gate after scope cleanup: tools/backlog validate PASS; ctx log -- ./gradlew compileJava compileGameTestJava PASS." - } - ], - "evidence": [], - "dependencies": [ - "AUTH-006", - "AUTH-007", - "AUTH-008", - "VANILLA-013" - ], - "doneDate": "2026-04-29" - }, - { - "id": "ARMY-003", - "title": "Persist player army command preferences across clone and respawn", - "status": "done", - "updated": "2026-04-28", - "why": "Formation, active groups, and related command preferences appear to initialize on login only, risking silent resets across death or player clone events.", - "scope": [ - "Player army command preference persistence must survive clone and respawn correctly, and any intended non-persistent command state must be documented explicitly." - ], - "acceptance": [ - "Saved formation and active-group preferences survive player clone and respawn when intended; non-persistent fields are explicitly excluded by design; tests cover login, clone, and reload behavior." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) Player clone handling copies saved Formation and ActiveGroups preferences. 2) Transient formation runtime fields are explicitly excluded. 3) MovementFormationCommandServiceTest covers login defaults, clone copy, transient exclusions, and reload/non-overwrite behavior. 4) Integrated compileJava, test, compileGametestJava, and verifyGameTestStage passed." - } - ], - "evidence": [], - "dependencies": [], - "doneDate": "2026-04-28" - }, - { - "id": "SYNC-001", - "title": "Add settlement client snapshot sync for player-facing UIs", - "status": "done", - "updated": "2026-04-29", - "why": "Settlement intelligence is mostly server-side today, leaving governor and related UIs dependent on ad hoc requests and thin placeholder states instead of a coherent client mirror.", - "scope": [ - "Player-facing settlement and governor screens need a clear synced snapshot contract, client mirror, refresh triggers, and waiting or stale-state UX for data arrival." - ], - "acceptance": [ - "Relevant settlement UIs render from a coherent synced snapshot with explicit loading or stale states; data refreshes propagate without reopening screens; focused verification covers login, open-screen, and mutation refresh paths." - ], - "progress": [ - { - "date": "2026-04-28", - "text": "Split into SYNC-002 snapshot contract, SYNC-003 client mirror/loading UX, and SYNC-004 refresh verification; parent remains open until settlement UIs render from coherent snapshots and refresh without reopening." - } - ], - "verification": [ - { - "date": "2026-04-29", - "result": "Acceptance satisfied by completed child chain SYNC-002, SYNC-003, and SYNC-004: settlement/governor snapshot contract exists, relevant settlement UIs render from the client mirror with explicit loading/stale/fresh states, and focused verification covers login, open-screen, and mutation refresh snapshot propagation. Current gate after scope cleanup: tools/backlog validate PASS; ctx log -- ./gradlew compileJava compileGameTestJava PASS." - } - ], - "evidence": [], - "dependencies": [ - "SYNC-002", - "SYNC-003", - "SYNC-004", - "VANILLA-013" - ], - "doneDate": "2026-04-29" - }, - { - "id": "UX-001", - "title": "Restore worker command hotkey and opening flow", - "status": "done", - "updated": "2026-04-28", - "why": "The worker command hotkey is defined and documented but not actually handled by client input, leaving a promised control surface non-functional.", - "scope": [ - "The worker command key path must open the intended screen reliably, avoid conflicting input behavior, and be verified against both keybinding registration and the live opening flow." - ], - "acceptance": [ - "The documented worker command hotkey opens the worker command UI in-game; conflicting or duplicate bindings are resolved intentionally; focused verification covers registration and real key handling." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) Worker command hotkey is registered once on X and consumed by KeyEvents. 2) Key handling selects the worker command category before opening the existing server-backed command screen. 3) Duplicate/conflicting binding was removed and localized labels are present. 4) Integrated compileJava, test, processResources in task branch, compileGametestJava, and verifyGameTestStage passed; code-path inspection verified real opening flow." - } - ], - "evidence": [], - "dependencies": [], - "doneDate": "2026-04-28" - }, - { - "id": "UX-002", - "title": "Finish claim-area flow and route and waypoint discoverability", - "status": "done", - "updated": "2026-04-30", - "why": "The world map advertises claim-area and route or waypoint interactions, but some affordances are incomplete or weakly explained, which makes the flow feel broken or hidden.", - "scope": [ - "Claim-area functionality must either be fully implemented or removed from player UI, and route or waypoint flows need discoverable in-game affordances, explanations, and guide coverage." - ], - "acceptance": [ - "Every visible world-map action is functional or intentionally absent; players can discover route and waypoint flows in-game without external docs; verification covers claim-area, waypoint, and route interactions." - ], - "progress": [ - { - "date": "2026-04-28", - "text": "Split into UX-010 claim-area affordance resolution, UX-011 route/waypoint discoverability, and UX-012 verification/docs; parent remains open until every visible map action is functional or intentionally absent and flows are discoverable in-game." - }, - { - "date": "2026-04-30", - "text": "Resumed on feature/cleanup-batch-5 after UX-010/011/012 closure; goal is to fully close world-map claim/route/waypoint discoverability and unlock TEST-005." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) ./gradlew compileJava test --tests com.talhanation.bannermod.client.military.gui.worldmap.WorldMapRouteUiVerificationTest passed on feature/ux-002 after removing the non-functional Claim Area affordance and strengthening in-game route/waypoint discoverability; 2) MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html were updated to match the shipped world-map flow; 3) ./gradlew test and ./gradlew verifyGameTestStage passed on feature/cleanup-batch-5." - } - ], - "evidence": [], - "dependencies": [ - "UX-010", - "UX-011", - "UX-012", - "VANILLA-013" - ], - "doneDate": "2026-04-30" - }, - { - "id": "UX-003", - "title": "Add disabled-reason and loading-state UX across critical screens", - "status": "done", - "updated": "2026-04-29", - "why": "War, claim, governor, and work-area screens often hide unavailable actions or show thin failure states, forcing players to infer authority, sync, or range problems from chat or silence.", - "scope": [ - "Critical screens must surface why actions are unavailable, distinguish waiting-for-sync from empty state, and show consistent loading and stale-data feedback." - ], - "acceptance": [ - "Disabled actions explain why they are unavailable; waiting-for-sync, empty, and stale states are visually distinct on the main war, claim, governor, and work-area screens; focused verification covers representative denial and loading cases." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Disabled actions explain why unavailable: claim map/menu now surfaces localized reasons for waiting sync, stale claims, unclaimable chunks, missing neighbor authority, and missing claim owner; governor policy buttons expose enabled/disabled tooltips. 2) Waiting/empty/stale/ready states are visually distinct on representative critical screens: WarListScreen, claim map/edit, GovernorScreen, and WorkAreaScreen render separate localized state labels/outlines. 3) Focused verification: merged branch passed ctx log -- ./gradlew compileJava compileGameTestJava plus focused unit smoke command; player-facing UI changes documented in MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html." - } - ], - "evidence": [], - "dependencies": [ - "UI-003", - "UI-004", - "UI-005", - "UI-006", - "SYNC-001", - "VANILLA-013" - ], - "doneDate": "2026-04-29" - }, - { - "id": "UX-004", - "title": "Remove raw UUID and internal-value leakage from player UI", - "status": "done", - "updated": "2026-04-30", - "why": "Politics, war, claim, and governor screens still expose raw UUID fragments, enum internals, and hardcoded English that read like debug surfaces rather than shipped player UI.", - "scope": [ - "Player-facing screens must render localized labels, human-readable identities, and interpreted state text instead of raw internal values across the active war, politics, claim, and governor surfaces." - ], - "acceptance": [ - "Target screens no longer expose raw UUIDs, enum internals, or hardcoded English for normal player flows; visible text comes from lang keys and player-friendly formatting; verification covers the main war and governance screens." - ], - "progress": [ - { - "date": "2026-04-30", - "text": "Picked up on feature/cleanup-batch-3 after UI-007 closure; goal is to remove remaining raw UUID/internal-value leakage so TEST-005 can move." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) ./gradlew compileJava passed on feature/ux-004 after replacing raw UUID/internal-value UI output with localized realm/player-friendly labels across war, politics, claim, governor, and HUD surfaces; 2) review of the touched screens confirmed active detail/list/overlay flows now render localized names and interpreted status text instead of UUID fragments and enum internals; 3) ./gradlew test and ./gradlew verifyGameTestStage passed on feature/cleanup-batch-3." - } - ], - "evidence": [], - "dependencies": [ - "UI-007", - "VANILLA-013" - ], - "doneDate": "2026-04-30" - }, - { - "id": "UX-005", - "title": "Ship full in-game settlement onboarding", - "status": "done", - "updated": "2026-04-30", - "why": "New-player settlement setup still depends too heavily on markdown docs and transient chat instructions for surveyor, starter fort, wand, citizens, and professions.", - "scope": [ - "Settlement onboarding must be understandable from in-game tooltips, screens, guided feedback, and stateful prompts from first claim through staffed starter buildings and professions." - ], - "acceptance": [ - "A new player can found a starter settlement, register buildings, understand staffing, and reach first profession loops using in-game guidance alone; verification covers the full onboarding path step by step." - ], - "progress": [ - { - "date": "2026-04-30", - "text": "Picked up on feature/cleanup-batch-4 after QA-001/GAME-005 closure; goal is full in-game settlement onboarding with no markdown dependency." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) ./gradlew compileJava passed on feature/ux-005 after adding a hybrid Surveyor onboarding GUI, localized onboarding hints, and item-based guided settlement feedback; 2) ./gradlew test --tests com.talhanation.bannermod.settlement.onboarding.SettlementOnboardingGuideTest passed on feature/ux-005; 3) MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html were updated to match the new Surveyor/onboarding flow; 4) ./gradlew test and ./gradlew verifyGameTestStage passed on feature/cleanup-batch-4." - } - ], - "evidence": [], - "dependencies": [ - "GAME-005", - "QA-001", - "VANILLA-013" - ], - "doneDate": "2026-04-30" - }, - { - "id": "UX-006", - "title": "Add player UI for revolts and remaining war outcomes", - "status": "done", - "updated": "2026-04-29", - "why": "Revolts and several war outcomes are still primarily command or admin flows, leaving a major strategy surface without coherent in-game UI for normal players.", - "scope": [ - "War Room or related screens must support player-facing revolt interaction, remaining outcome selection, tribute-related flows, and clear authority feedback for those actions." - ], - "acceptance": [ - "Normal player war flows for revolts and supported outcomes can be completed through in-game UI with clear authority and cooldown feedback; verification covers declare, occupy, revolt, and remaining outcome interactions." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "Acceptance satisfied by completed child chain UX-007, UX-008, and UX-009. Revolt interaction is available in War Room through operator-controlled pending revolt resolution with allowed/denied verification; supported outcome selection including occupy/annex/tribute is exposed in War Room with server-authoritative accepted/denied feedback; declaration/outcome/siege/ally authority and cooldown denials are visible in-screen with localized reasons. Verification on merged branch: compileJava, compileGameTestJava, processResources, RevoltInteractionServiceTest, MessageResolveWarOutcomeTest, WarOutcomeApplierTest, PoliticalEntityAuthorityTest, WarCooldownPolicyTest, and WarCooldownPolicyImmunityTest passed; player guides and almanac were updated." - } - ], - "evidence": [], - "dependencies": [ - "UX-007", - "UX-008", - "UX-009", - "VANILLA-013" - ], - "doneDate": "2026-04-29" - }, - { - "id": "GAME-001", - "title": "Replace noop settlement project bridge with live build execution", - "status": "done", - "updated": "2026-04-30", - "why": "Settlement projects currently fall back to a noop build-area resolver in important paths, so project logic can exist without driving real construction work in the world.", - "scope": [ - "Settlement projects must bind to real build areas or equivalent world execution paths, publish actionable work, and reflect progress through actual building activity instead of placeholder records." - ], - "acceptance": [ - "Active settlement projects create real executable construction work in-world; project progress reflects actual build execution rather than noop placeholders; focused verification covers project creation, assignment, and completion." - ], - "progress": [ - { - "date": "2026-04-28", - "text": "Split into GAME-009 real project build-area binding and GAME-010 actual build-execution progress; parent remains open until project creation, assignment, and completion are verified against real world work." - }, - { - "date": "2026-04-30", - "text": "Resumed on feature/cleanup-batch-2 as an unblocker task; live settlement project execution closure should unlock ARCH-003 if remaining deps stay green." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) ./gradlew compileJava passed on feature/game-001 after replacing the noop settlement project retry path with claim-aware live build-area creation; 2) ./gradlew runGameTestServerGame001 passed on feature/cleanup-batch-2, proving a NEW_BUILDING project with no preexisting target now creates a real in-world BuildArea and binds assignment to it; 3) ./gradlew test and ./gradlew verifyGameTestStage passed on feature/cleanup-batch-2, and the existing GAME-010 project progress GameTest still covers build-execution-driven progress/completion." - } - ], - "evidence": [], - "dependencies": [ - "GAME-009", - "GAME-010", - "VANILLA-013" - ], - "doneDate": "2026-04-30" - }, - { - "id": "GAME-002", - "title": "Turn sea-trade summaries into a real production and consumption loop", - "status": "done", - "updated": "2026-04-29", - "why": "Sea trade is currently represented mostly as summary math and recommendation text, not a materially meaningful production and consumption gameplay system.", - "scope": [ - "Sea trade must consume and produce real goods or equivalent transport state, interact with settlement logistics, and expose player-visible bottlenecks and results through gameplay rather than hints alone." - ], - "acceptance": [ - "Sea-trade routes affect real goods flow and settlement state; players can observe production, transport, and consumption consequences; verification covers at least one end-to-end sea-trade loop." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "Acceptance satisfied by completed sea-trade chain SHIPS-002 and SHIPS-003. Sea-trade execution now moves real goods through StorageArea/TransportContainerExchange-backed source, carrier cargo, and destination containers; settlement snapshots/governor logistics expose loading/travelling/unloading/completed/missing-carrier/blocked-cargo state, carrier assignment, cargo progress, and failure reasons so players can observe production/transport/consumption consequences. Verification on merged branch: compileJava, compileGameTestJava, processResources, BannerModSettlementServiceTest, and BannerModSettlementClientMirrorTest passed; player guides and almanac were updated." - } - ], - "evidence": [], - "dependencies": [ - "SHIPS-001", - "SHIPS-002", - "SHIPS-003", - "VANILLA-013" - ], - "doneDate": "2026-04-29" - }, - { - "id": "GAME-003", - "title": "Materialize occupation, revolt, tribute, and tax consequences", - "status": "done", - "updated": "2026-04-28", - "why": "Occupation, revolt, tribute, and tax systems rely heavily on records and ledgers today, which weakens immersion and leaves strategic outcomes underrepresented in the world.", - "scope": [ - "These war-economy systems need stronger world and UI consequences such as visible control state, material obligations, objective pressure, and player-readable ongoing effects beyond hidden saved records." - ], - "acceptance": [ - "Occupation, revolt, tribute, and tax states produce observable ongoing effects in world play and player UI; the main consequences are no longer hidden behind abstract records alone; verification covers both state transition and observable aftermath." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) War Room detail now shows localized consequence summaries for occupation/tax, revolt, and tribute states using synced war client state; 2) EN/RU guides and almanac document the player-facing consequence summary; 3) compileJava passed; 4) processResources passed; 5) focused WarOutcomeApplierTest passed; 6) focused OccupationTaxRuntimeTest passed after rerun from a parallel test-results write collision; 7) tools/backlog validate passed." - } - ], - "evidence": [], - "dependencies": [ - "GAME-006", - "GAME-007", - "GAME-008" - ], - "doneDate": "2026-04-28" - }, - { - "id": "GAME-004", - "title": "Give claims stronger in-world representation and boundary feedback", - "status": "done", - "updated": "2026-04-30", - "why": "Claims strongly affect permissions but remain mostly abstract chunk records plus overlays, leaving territory control under-materialized in the world itself.", - "scope": [ - "Claims need stronger in-world boundary or authority feedback, clearer ownership cues, and interaction feedback that makes protected space legible without relying only on the map screen." - ], - "acceptance": [ - "Players can perceive claim boundaries or authority state from normal world play; common protected-space interactions provide clear in-world feedback; verification covers friendly, hostile, and unclaimed territory reads." - ], - "progress": [ - { - "date": "2026-04-28", - "text": "Split into GAME-011 territory boundary/authority cues and GAME-012 protected-space interaction feedback; parent remains open until friendly, hostile, and unclaimed reads are verified." - }, - { - "date": "2026-04-30", - "text": "Resumed on feature/cleanup-batch-5 after GAME-011/012 closure; goal is to make claim territory legible in-world and unblock DOC-004." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) ./gradlew compileJava passed on feature/game-004 after adding stronger in-world claim owner/territory HUD text and boundary-crossing feedback; 2) focused tests ClaimAuthorityStatusTest, ClaimTerritoryTextTest, and ClaimProtectionFeedbackTest passed there; 3) MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html were refreshed; 4) ./gradlew test and ./gradlew verifyGameTestStage passed on feature/cleanup-batch-5." - } - ], - "evidence": [], - "dependencies": [ - "GAME-011", - "GAME-012", - "VANILLA-013" - ], - "doneDate": "2026-04-30" - }, - { - "id": "GAME-005", - "title": "Unify settlement bootstrap paths", - "status": "done", - "updated": "2026-04-29", - "why": "Surveyor-based starter founding and automatic claim worker bootstrap currently look like partially separate onboarding systems, which risks contradictory world state and player expectations.", - "scope": [ - "Settlement bootstrap must have one coherent lifecycle from claim through starter structures, workers, citizens, and formal settlement records, with explicit rules for auto-bootstrap versus manual founding." - ], - "acceptance": [ - "Manual and automatic bootstrap paths converge on the same settlement lifecycle and data model; players and code no longer see contradictory startup states; verification covers both entry paths and their resulting records." - ], - "progress": [ - { - "date": "2026-04-28", - "text": "Split into GAME-013 lifecycle definition and GAME-014 path convergence; parent remains open until manual and automatic bootstrap paths produce the same settlement records and player-visible startup state." - } - ], - "verification": [ - { - "date": "2026-04-29", - "result": "Acceptance satisfied by completed child chain GAME-013 and GAME-014: the unified claim-to-settlement lifecycle is defined, and manual plus automatic bootstrap paths converge on the same settlement lifecycle/data model with focused verification for both entry paths and resulting records. Current gate after scope cleanup: tools/backlog validate PASS; ctx log -- ./gradlew compileJava compileGameTestJava PASS." - } - ], - "evidence": [], - "dependencies": [ - "GAME-013", - "GAME-014", - "VANILLA-013" - ], - "doneDate": "2026-04-29" - }, - { - "id": "ARCH-001", - "title": "Split RecruitEvents into smaller runtime services", - "status": "done", - "updated": "2026-04-30", - "why": "RecruitEvents currently acts as a multi-responsibility runtime hub for lifecycle, sync, patrol, combat hooks, and server tick behavior, which makes changes risky and obscures invariants.", - "scope": [ - "RecruitEvents must be decomposed into narrower services with explicit responsibilities and stable seams for lifecycle, sync, patrol, and combat integration while preserving behavior." - ], - "acceptance": [ - "Recruit runtime responsibilities are separated into smaller units with clear ownership and no behavior regression; core recruit flows still pass existing verification after the split." - ], - "progress": [ - { - "date": "2026-04-30", - "text": "Picked up on feature/cleanup-batch-3 after ARMY-001 closure; goal is to split RecruitEvents into narrower runtime services without behavior regressions." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) ./gradlew compileJava passed on feature/arch-001 after splitting RecruitEvents event-bus responsibilities into RecruitLifecycleEvents and RecruitCombatEvents while preserving the static facade; 2) focused tests RecruitGovernorWorkflowTest and RecruitTargetAuthorityTest passed on feature/arch-001; 3) ./gradlew test and ./gradlew verifyGameTestStage passed on feature/cleanup-batch-3." - } - ], - "evidence": [], - "dependencies": [ - "AUTH-005", - "ARMY-001", - "ARMY-003", - "VANILLA-013" - ], - "doneDate": "2026-04-30" - }, - { - "id": "ARCH-002", - "title": "Split ClaimEvents into claim, protection, queue, and settlement services", - "status": "done", - "updated": "2026-04-28", - "why": "ClaimEvents currently mixes claim manager lifecycle, protection hooks, command queue ticking, and governor or settlement heartbeat orchestration in one oversized event surface.", - "scope": [ - "ClaimEvents must be broken into smaller services with explicit ownership of protection, claim runtime, command queue advancement, and settlement heartbeat responsibilities." - ], - "acceptance": [ - "Claim-related runtime concerns are separated into smaller services with stable event registration and preserved behavior; verification covers claim protection, queue ticking, and settlement heartbeat flows." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) ClaimEvents now delegates claim lifecycle, protection handlers, queue ticking, and settlement heartbeat batching to separate services while preserving stable event registration through BannerModMain; 2) compileJava passed; 3) CommandIntentQueueTest and BannerModGovernorHeartbeatTest passed for queue ticking and heartbeat logic; 4) verifyGameTestStage passed, covering claim protection/GameTest flows." - } - ], - "evidence": [], - "dependencies": [ - "AUTH-003", - "TEST-003" - ], - "doneDate": "2026-04-28" - }, - { - "id": "ARCH-003", - "title": "Decompose settlement snapshot and orchestration services", - "status": "done", - "updated": "2026-04-30", - "why": "BannerModSettlementService and BannerModSettlementOrchestrator each aggregate too many responsibilities, making settlement behavior hard to reason about, test, and evolve safely.", - "scope": [ - "Settlement snapshot building, logistics derivation, staffing, project planning, home assignment, seller dispatch, and job orchestration must be split into smaller bounded services with explicit contracts." - ], - "acceptance": [ - "Settlement runtime responsibilities are decomposed into smaller services with clear inputs and outputs; behavior stays equivalent or intentionally improved; focused tests cover the extracted seams." - ], - "progress": [ - { - "date": "2026-04-30", - "text": "Picked up on feature/cleanup-batch-3 after GAME-001/GAME-002/GAME-005/SYNC-001 closure; goal is to decompose settlement snapshot/orchestration responsibilities into bounded services." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) ./gradlew compileJava passed on feature/arch-003 after extracting settlement snapshot/orchestration responsibilities into explicit helper services; 2) focused tests BannerModSettlementServiceTest and BannerModSettlementOrchestratorTest passed on feature/arch-003; 3) ./gradlew test and ./gradlew verifyGameTestStage passed on feature/cleanup-batch-3 with existing public settlement entry points preserved." - } - ], - "evidence": [], - "dependencies": [ - "GAME-001", - "GAME-002", - "GAME-005", - "SYNC-001", - "VANILLA-013" - ], - "doneDate": "2026-04-30" - }, - { - "id": "ARCH-004", - "title": "Modularize packet registration beyond the giant network bootstrap", - "status": "done", - "updated": "2026-04-29", - "why": "BannerModNetworkBootstrap carries a large order-sensitive shared packet catalog, which is hard to audit for authority, maintain safely, or evolve by subsystem.", - "scope": [ - "Packet registration must be split into modular catalogs or equivalent subsystem registrations with explicit ownership, stable IDs, and easier authority review." - ], - "acceptance": [ - "Network registration is organized by subsystem with preserved wire compatibility where required; packet ownership and authority review become tractable; verification proves the active packet set still registers and works." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Network registration is organized by subsystem catalogs for military, civilian, and war packets while BannerModNetworkBootstrap preserves the existing public arrays and registers catalogs in the original military -> civilian -> war order, so wire IDs remain stable. 2) Packet ownership and authority review are tractable through subsystem catalog files under network/catalog. 3) Verification: merged branch passed compileJava/compileGameTestJava and focused BannerModIntegratedRuntimeSmokeTest in the combined Gradle command." - } - ], - "evidence": [], - "dependencies": [ - "AUTH-001", - "AUTH-002", - "AUTH-003", - "AUTH-004", - "AUTH-005", - "VANILLA-013" - ], - "doneDate": "2026-04-29" - }, - { - "id": "ARCH-005", - "title": "Remove dead bootstrap and merge-residue runtime shims", - "status": "done", - "updated": "2026-04-28", - "why": "BannerModLifecycle, WorkersSubsystem, update-checker stubs, and similar merge leftovers add noise and false seams without carrying live runtime value.", - "scope": [ - "Dead bootstrap helpers, update-checker stubs, and obsolete merge shims must be removed or collapsed into the real runtime entrypoints, with any still-needed behavior kept in one obvious place." - ], - "acceptance": [ - "Dead bootstrap or update-checker residue no longer exists in the active runtime tree unless it has a demonstrated live purpose; build and runtime entrypoints remain clear after cleanup." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) Dead update-checker stubs and merge cleanup shims were removed from active runtime by merged PRs; 2) stale cleanup policy test no longer exists on master; 3) task branch verification passed before merge and current PR is backlog-only on top of merged master." - } - ], - "evidence": [], - "dependencies": [], - "doneDate": "2026-04-28" - }, - { - "id": "ARCH-006", - "title": "Retire legacy worker current-area assignment state", - "status": "done", - "updated": "2026-04-29", - "why": "Live worker AI still carries legacy current-area style assignment state instead of treating validated-building records as the clear authoritative assignment source.", - "scope": [ - "Worker assignment and work-goal selection must move toward one authoritative assignment model backed by validated-building or equivalent canonical records, removing redundant legacy state." - ], - "acceptance": [ - "Worker assignment uses one authoritative source of truth; redundant legacy current-area state is removed or reduced to a derived cache; verification covers assignment, reload, and job execution behavior." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Worker assignment now uses boundWorkAreaUuid/resident assignment as the authoritative source: ValidatedBuildingRecord no longer persists/reloads assignedCitizenIds, settlement building snapshot records start unassigned and are populated by resident assignment, and legacy current-area remember callback/tick/save path was removed. 2) The remaining currentWorkAreaCache is a derived cache only. 3) Verification: merged branch passed compileJava/compileGameTestJava plus focused BannerModSettlementServiceTest, JobHandlerRegistryTest, and CitizenRoleControllerTest in the combined Gradle command; GameTest constructor fallout was fixed and committed." - } - ], - "evidence": [], - "dependencies": [ - "GAME-005", - "VANILLA-013" - ], - "doneDate": "2026-04-29" - }, - { - "id": "ARCH-007", - "title": "Reduce global static runtime managers", - "status": "done", - "updated": "2026-04-30", - "why": "Global statics for runtime managers and channels increase coupling, complicate testing, and hide lifecycle assumptions across claims, recruits, and client mirrors.", - "scope": [ - "Runtime manager access should move behind clearer lifecycle-bound seams or service accessors, reducing mutable global state where it is not strictly required by Forge integration." - ], - "acceptance": [ - "Critical runtime managers are no longer reached through unnecessary mutable globals; lifecycle ownership is explicit; representative tests or verification prove the new access seams behave correctly." - ], - "progress": [ - { - "date": "2026-04-30", - "text": "Picked up on feature/cleanup-batch-4 after ARCH-001/ARCH-003 closure; goal is to reduce remaining mutable global runtime manager access seams." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) ./gradlew compileJava and ./gradlew compileGameTestJava passed on feature/arch-007 after privatizing claim/recruit runtime manager globals behind lifecycle-owned accessors; 2) direct global-manager call sites were migrated to explicit accessors without changing runtime manager creation/save/load timing; 3) ./gradlew test and ./gradlew verifyGameTestStage passed on feature/cleanup-batch-4." - } - ], - "evidence": [], - "dependencies": [ - "ARCH-001", - "ARCH-002", - "ARCH-003", - "ARCH-004", - "ARCH-005", - "ARCH-006", - "VANILLA-013" - ], - "doneDate": "2026-04-30" - }, - { - "id": "TEST-001", - "title": "Add authority GameTests for transfer and admin packets", - "status": "done", - "updated": "2026-04-29", - "why": "Critical authority gaps were found in recruit transfer, group transfer, world-map teleport, and work-area owner packets, and those live packet paths currently lack focused scenario coverage.", - "scope": [ - "Dedicated-server GameTests must exercise allowed and denied flows for recruit transfer, group transfer, teleport, and work-area owner packets using real sender identity and multiplayer authority conditions." - ], - "acceptance": [ - "Each targeted packet has focused tests that fail on spoofed or unauthorized use and pass for legitimate authority; the tests run through the real server packet path, not only helper methods." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Added packet-path GameTests for recruit transfer, group transfer, world-map teleport, and work-area owner packets. 2) Tests encode/decode packets and execute the NeoForge server payload path with sender context, covering unauthorized/spoofed denial and legitimate authority success. 3) Verification: merged branch passed compileJava and compileGameTestJava; runGameTestServer was attempted and remains red only on known unrelated failures, with no new PacketAuthorityGameTests listed among failures." - } - ], - "evidence": [], - "dependencies": [ - "AUTH-001", - "AUTH-002", - "AUTH-003", - "AUTH-004", - "AUTH-005", - "VANILLA-013" - ], - "doneDate": "2026-04-29" - }, - { - "id": "TEST-002", - "title": "Add end-to-end tests for formation command pipeline", - "status": "done", - "updated": "2026-04-30", - "why": "The saved-formation command path and queued-intent execution rules are central gameplay flows but currently lack clear end-to-end coverage for parity and persistence.", - "scope": [ - "Tests must exercise packet to intent to dispatcher to execution behavior for formation-aware movement and related queued commands, including saved formation preferences and queue parity." - ], - "acceptance": [ - "Formation-aware command flows are covered end to end for immediate and queued behavior; saved formation preferences are verified across reload or clone scenarios where intended; representative failure cases are covered." - ], - "progress": [ - { - "date": "2026-04-30", - "text": "Picked up on feature/cleanup-batch-3 after ARMY-001 closure; goal is end-to-end command pipeline coverage for formation-aware immediate and queued flows." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) additive GameTests in BannerModCommandPacketPathGameTests and BannerModCommandQueueParityGameTests cover packet -> intent -> dispatcher -> execution for formation-aware immediate and queued command flows; 2) ./gradlew compileJava passed on feature/test-002; 3) ./gradlew test and ./gradlew verifyGameTestStage passed on feature/cleanup-batch-3." - } - ], - "evidence": [], - "dependencies": [ - "ARMY-001", - "ARMY-002", - "ARMY-003", - "AUTH-005", - "VANILLA-013" - ], - "doneDate": "2026-04-30" - }, - { - "id": "TEST-003", - "title": "Add GameTests for claim intent mutation and deletion", - "status": "done", - "updated": "2026-04-28", - "why": "Claim intent add, remove, and delete behavior is a core authority and world-state flow, yet current coverage is much thinner than the live gameplay risk warrants.", - "scope": [ - "GameTests must cover successful and denied claim add, remove, and delete flows, currency charging, distance checks, Overworld-only rules, and persistence-visible outcomes." - ], - "acceptance": [ - "Claim mutation flows are covered by real GameTests for both success and denial paths; tests observe resulting claim state rather than packet registration alone." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) Merged GameTests cover successful and denied claim add/remove/delete flows; 2) tests observe claim manager/save-data-visible state, currency charging/no-charge denial, distance denial, and Overworld-only denial; 3) task branch verification passed before merge and current PR is backlog-only on top of merged master." - } - ], - "evidence": [], - "dependencies": [], - "doneDate": "2026-04-28" - }, - { - "id": "TEST-004", - "title": "Add live packet-path tests for face, ranged-fire, and upkeep commands", - "status": "done", - "updated": "2026-04-28", - "why": "Face, ranged-fire, and upkeep command families were hardened previously but still lack strong live packet-path coverage compared with movement and stance commands.", - "scope": [ - "Focused tests must drive the real server packet handlers for face, ranged-fire, and upkeep behavior, asserting authority, state mutation, and denial paths." - ], - "acceptance": [ - "Face, ranged-fire, and upkeep packet flows each have real packet-path coverage for valid and invalid senders; tests assert resulting recruit behavior or rejection clearly." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) Merged packet-path GameTests cover face, ranged-fire, and upkeep commands; 2) valid owner sender mutates recruit state and spoofed invalid sender is rejected/no-op; 3) task branch verification passed before merge and current PR is backlog-only on top of merged master." - } - ], - "evidence": [], - "dependencies": [], - "doneDate": "2026-04-28" - }, - { - "id": "TEST-005", - "title": "Add regression coverage for critical UI gating and sync states", - "status": "done", - "updated": "2026-04-30", - "why": "Key player-facing regressions such as missing hotkeys, waiting-for-sync confusion, and hidden action gating currently slip through because almost no automated coverage targets those logic surfaces.", - "scope": [ - "Introduce focused automated coverage for keybinding handling, visible action gating, waiting-for-sync state selection, and other non-render logic behind critical screens." - ], - "acceptance": [ - "The worker hotkey, world-map gating, war sync-state selection, and similar critical UI logic surfaces are covered by automated tests or equivalent deterministic verification; regressions in those paths fail fast." - ], - "progress": [ - { - "date": "2026-04-30", - "text": "Picked up on feature/cleanup-batch-6 after UX-002/UX-004 closure; goal is focused regression coverage for world-map gating, hotkey handling, and war sync-state selection so TEST-006 can open." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) added CriticalUiStateVerificationTest and expanded WarClientStateTest to cover worker hotkey wiring, world-map action gating, waiting-for-sync vs stale vs empty UI states, and war action-feedback reset semantics; 2) ./gradlew compileJava and ./gradlew test --tests com.talhanation.bannermod.client.military.gui.CriticalUiStateVerificationTest --tests com.talhanation.bannermod.war.client.WarClientStateTest passed on feature/test-005; 3) ./gradlew test and ./gradlew verifyGameTestStage passed on feature/cleanup-batch-6." - } - ], - "evidence": [], - "dependencies": [ - "UX-001", - "UX-002", - "UX-003", - "UX-004", - "SYNC-001", - "VANILLA-013" - ], - "doneDate": "2026-04-30" - }, - { - "id": "TEST-006", - "title": "Replace count-only GameTest theater with meaningful quality gates", - "status": "done", - "updated": "2026-04-30", - "why": "The current scenario-count task measures quantity rather than scenario value and is not wired into the main verification path, which weakens confidence in a green build.", - "scope": [ - "Verification gates must reward meaningful scenario coverage, wire the right checks into the main build path, and remove or demote count-only theater that does not reflect gameplay risk." - ], - "acceptance": [ - "Check or equivalent verification fails when critical scenario coverage or focused high-risk suites are missing, not merely when an arbitrary count changes; the quality gate behavior is documented and exercised." - ], - "progress": [ - { - "date": "2026-04-30", - "text": "Picked up on feature/cleanup-batch-7 after TEST-005 closure; goal is to replace count-only GameTest theater with a meaningful enforced quality gate." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) verifyGameTestStage now depends on the main GameTest namespace run, focused high-risk suites (game001/game009/game010), and a meaningful source-level coverage presence gate instead of an arbitrary total-count threshold; 2) ./gradlew compileJava compileGameTestJava test verifyGameTestScenarioCoverage verifyFocusedGameTestStage passed on feature/test-006; 3) ./gradlew test and ./gradlew verifyGameTestStage passed on feature/cleanup-batch-7." - } - ], - "evidence": [], - "dependencies": [ - "TEST-001", - "TEST-002", - "TEST-003", - "TEST-004", - "TEST-005", - "VANILLA-013" - ], - "doneDate": "2026-04-30" - }, - { - "id": "DOC-003", - "title": "Reconcile status and backlog narrative with live runtime", - "status": "done", - "updated": "2026-04-28", - "why": "Current status and review docs describe several war and governance systems as still pending even though active code already ships parts of them, which distorts planning and review baselines.", - "scope": [ - "Project status documentation and backlog metadata must be updated to reflect what is actually live, what is partial, and what remains missing, using code and verification as the source of truth." - ], - "acceptance": [ - "STATUS and related planning narrative no longer contradict shipped runtime behavior for allies, occupations, revolts, siege AI, and related systems; verification cites the live code paths used to justify each status line." - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "Verified with ctx status (clean baseline before edits), tools/backlog show DOC-003, ctx file/exact on docs/STATUS.md and .planning/STATE.md, ctx searches/files for live ally paths (WarAllyService, WarAllyCommands, MessageInviteAlly, MessageRespondAllyInvite, WarAlliesScreen), occupation/outcome/tax paths (WarOutcomeApplier, OccupationRuntime, OccupationTaxRuntime, WarOccupationTaxTicker, MessageResolveWarOutcome), revolt paths (WarRevoltScheduler, WarRevoltAutoResolver, ServerLevelObjectivePresenceProbe, RevoltRuntime), and siege AI paths (RecruitSiegeObjectiveAttackGoal, RecruitSiegeEscortGoal, SiegeObjectivePolicy, SiegeStandardBlock). Updated STATUS plus EN/RU politics docs so shipped systems are not listed as absent and remaining gaps are described as depth/coverage limits. tools/backlog validate passes." - } - ], - "evidence": [], - "dependencies": [], - "doneDate": "2026-04-28" - }, - { - "id": "DOC-004", - "title": "Refresh player guides and almanac for shipped mechanics", - "status": "done", - "updated": "2026-04-30", - "why": "Root player guides and the almanac are stale on zone roles, revolt behavior, occupation tax, battle-window HUD, and other player-facing mechanics, which breaks the documented gameplay contract.", - "scope": [ - "The English and Russian multiplayer guides plus the almanac must match current mechanics, HUD surfaces, and supported commands or UI, and stale nonexistent concepts must be removed." - ], - "acceptance": [ - "Player guides and almanac match the live behavior for current settlement, war, revolt, occupation, HUD, and zone-role mechanics; nonexistent concepts are removed; verification cross-checks docs against active code and screens." - ], - "progress": [ - { - "date": "2026-04-30", - "text": "Picked up on feature/cleanup-batch-8 as the final open task; goal is a last code-vs-guides/almanac audit and any remaining doc drift cleanup." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) Root guides MULTIPLAYER_GUIDE_EN.md / MULTIPLAYER_GUIDE_RU.md and docs/BANNERMOD_ALMANAC.html were audited against current settlement, war, revolt, occupation-tax, battle-window HUD, claim/world-map, and surveyor GUI code paths; 2) no remaining stale or nonexistent player-facing concepts were found in the audited DOC-004 scope, so no further doc edits were necessary; 3) current cleanup integration branches remained green on ./gradlew test and ./gradlew verifyGameTestStage." - } - ], - "evidence": [], - "dependencies": [ - "UX-005", - "UX-006", - "GAME-003", - "GAME-004", - "GAME-005", - "SHIPS-003", - "MERCH-002", - "BOOM-004", - "VANILLA-013" - ], - "doneDate": "2026-04-30" - }, - { - "id": "PERF-009", - "title": "Adaptive runtime budgets under TPS pressure", - "status": "done", - "updated": "2026-04-28", - "why": "Several server and client subsystems already use static budgets or throttles, but a fixed budget still overcommits work when TPS drops and underutilizes headroom when the server recovers.", - "scope": [ - "Introduce an adaptive budget policy that reacts to measured TPS or tick pressure for expensive systems such as async-path snapshot capture, formation-map snapshots, preview rendering, and settlement orchestration where appropriate. Add hysteresis and floor or ceiling rules so the budget does not oscillate violently." - ], - "acceptance": [ - "When TPS or tick pressure drops below the configured threshold, targeted subsystems reduce their optional work budget instead of holding static throughput; when the server recovers, budgets rise gradually without oscillation; profiling or deterministic verification shows the adaptive policy changing runtime budgets under load." - ], - "dependencies": [ - "PERF-003", - "PERF-006", - "PERF-007", - "PERF-008" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) AdaptiveRuntimeBudgets now reduces optional budgets under tick pressure and recovers gradually with hysteresis/floors/ceilings; 2) merged wiring covers async path, formation-map, settlement revalidation/orchestration optional work; 3) focused task verification passed before merge and current PR is backlog-only on top of merged master." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "BOOM-001", - "title": "Make recruits fire Ewewukek Musket Mod projectiles and ammo", - "status": "done", - "updated": "2026-04-30", - "why": "Recruits can currently equip and use Ewewukek's Musket Mod weapons, but the live attack path still falls back to arrow-style projectile behavior instead of using the mod's bullet or firearm semantics, which breaks both balance and fantasy.", - "scope": [ - "Add an explicit compatibility path for Ewewukek's Musket Mod ranged weapons so recruit attacks spawn the correct projectile type, consume the correct ammo, and respect the mod's firearm timing or reload semantics where exposed. Keep unsupported weapons on a safe fallback path instead of silently pretending bullets are arrows." - ], - "acceptance": [ - "A recruit using a supported Ewewukek's Musket Mod firearm fires the mod-correct projectile rather than an arrow; ammo consumption and no-ammo denial follow the firearm's intended rules; deterministic verification or focused tests cover at least one supported firearm and one unsupported edge case." - ], - "dependencies": [ - "BOOM-002", - "BOOM-003", - "BOOM-004", - "VANILLA-013" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-30", - "result": "1) Supported recruit firearms are routed through the musketmod compat path in MusketModCompat / PistolWeapon / RecruitRangedMusketAttackGoal instead of arrow-style fallback; 2) BOOM-003A runtime verification passed with musketmod present via ./gradlew runGameTestServer in the dedicated BOOM-003A worktree, proving mod-correct projectile spawn, reload cadence, cartridge consumption, and no-ammo denial for a supported pistol; 3) BannerModRecruitFirearmStatusTest covers the unsupported firearm edge state and ammo-status distinction; 4) cleanup integration branches remained green on ./gradlew test and ./gradlew verifyGameTestStage." - } - ], - "evidence": [], - "doneDate": "2026-04-30" - }, - { - "id": "SHIPS-001", - "title": "Add Small Ships compatibility layer for sea-trade carriers", - "status": "done", - "updated": "2026-04-28", - "why": "Sea trade cannot become a materially convincing gameplay loop if route execution remains abstract while the mod already targets visible logistics and the Small Ships mod provides suitable vessel entities.", - "scope": [ - "Add an optional compatibility layer that detects supported Small Ships vessel types and exposes the operations BannerMod needs for trade carriers, such as spawn or reserve, assign route owner, move between ports, and survive absence of the mod without crashes." - ], - "acceptance": [ - "BannerMod starts and runs without Small Ships installed; with Small Ships installed, supported vessel types are detected and exposed as candidate sea-trade carriers; deterministic verification proves the compat layer can create or bind at least one supported carrier safely." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) Optional SmallShipsSeaTradeCompat is merged without compile-time Small Ships dependency; 2) candidate registry ids and reflective carrier binding are covered by focused deterministic tests; 3) task branch verification passed before merge and current PR is backlog-only on top of merged master." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "SHIPS-002", - "title": "Execute sea-trade routes through Small Ships cargo movement", - "status": "done", - "updated": "2026-04-29", - "why": "Even with ship detection, sea trade stays fake unless goods actually move through vessel-backed route execution between ports, stockpiles, or settlements.", - "scope": [ - "Implement ship-backed sea-trade route execution with cargo loading, travel state, arrival, unloading, and failure handling tied into settlement logistics and player-visible route feedback. Reuse existing goods or stockpile systems where possible instead of creating a parallel economy." - ], - "acceptance": [ - "At least one sea-trade route can load goods onto a Small Ships carrier, travel, unload at the destination, and update settlement or stockpile state visibly; route failure and missing-ship states are surfaced clearly; focused verification covers one end-to-end carrier trip." - ], - "dependencies": [ - "SHIPS-002D", - "VANILLA-013" - ], - "progress": [ - { - "date": "2026-04-28", - "text": "Split needed: existing SHIPS-001 layer binds Small Ships carrier identity only; no cargo/navigation capability API is available at compile time and no persisted sea-trade route executor exists. Child chain SHIPS-002A..SHIPS-002D covers compat capabilities, execution state, stockpile cargo movement, and player-visible status/docs." - } - ], - "verification": [ - { - "date": "2026-04-29", - "result": "Acceptance satisfied by completed child chain SHIPS-002A, SHIPS-002B, SHIPS-002C, and SHIPS-002D: Small Ships carrier capability probing, persistent sea-trade execution state, real StorageArea/TransportContainerExchange cargo movement, and player-visible loading/travelling/unloading/completed/missing-ship/blocked-cargo feedback plus docs are implemented and verified in focused slices. Current gate after scope cleanup: tools/backlog validate PASS; ctx log -- ./gradlew compileJava compileGameTestJava PASS." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "MERCH-001", - "title": "Mount merchants and land couriers with faster transport", - "status": "done", - "updated": "2026-04-28", - "why": "Merchant and courier loops currently leave obvious movement efficiency on the table; equipping long-haul traders with horses or other supported transport is a direct way to improve both realism and logistics throughput.", - "scope": [ - "Define when merchants or other long-haul civilian carriers may use horses or supported transport, how mounts are acquired or assigned, and how mounted travel integrates with existing route, inventory, and safety logic. Keep behavior server-authoritative and degrade safely when no mount is available." - ], - "acceptance": [ - "An eligible merchant or courier can use an approved mount or transport for faster travel on supported routes; inventory, ownership, and dismount or failure behavior remain consistent; verification covers mounted and unmounted fallback behavior." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) Merged courier transport lets eligible couriers mount approved server-side transport on supported long routes; 2) route ownership/inventory state, dismount, and no-mount fallback are covered; 3) docs/almanac landed and task verification passed before merge." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "BOOM-002", - "title": "Map supported Ewewukek Musket Mod firearms and ammo for recruits", - "status": "done", - "updated": "2026-04-28", - "why": "Before recruit firearm combat can behave correctly, BannerMod needs an explicit compatibility map for which Ewewukek's Musket Mod weapons are supported, which ammo they consume, and what fallback behavior is legal for unsupported items.", - "scope": [ - "Detect supported Ewewukek's Musket Mod firearm items and their ammo contracts through a compatibility layer or adapter usable by recruit combat code. Define safe fallback rules for unsupported or partially supported items instead of silently treating them like bows." - ], - "acceptance": [ - "BannerMod can distinguish supported and unsupported Ewewukek's Musket Mod firearms for recruit use; supported firearms resolve the correct ammo contract; unsupported weapons follow an explicit fallback or denial path rather than hidden bow semantics." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) MusketModCompat now distinguishes supported firearms and resolves cartridge ammo contracts; 2) unsupported musketmod items are denied from hidden bow semantics/fallback; 3) task branch verification passed before merge and current PR is backlog-only on top of merged master." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "BOOM-003", - "title": "Route recruit firearm attacks through Musket Mod projectile and reload semantics", - "status": "done", - "updated": "2026-04-30", - "why": "Even with weapon detection, recruit attacks remain wrong until the live ranged attack path uses Ewewukek's Musket Mod projectile creation, reload timing, and firing semantics instead of arrow-style behavior.", - "scope": [ - "Integrate supported Ewewukek's Musket Mod firearms into recruit attack execution so projectile spawn, reload cadence, ammo checks, and no-ammo denial use the mod's firearm rules where exposed. Preserve safe server-authoritative fallback for unsupported edge cases." - ], - "acceptance": [ - "A recruit firing a supported Ewewukek's Musket Mod firearm spawns the mod-correct projectile and follows the intended reload or fire cadence; no-ammo behavior matches the firearm contract; focused verification covers one supported firearm combat flow." - ], - "dependencies": [ - "BOOM-003A", - "VANILLA-013" - ], - "progress": [ - { - "date": "2026-04-28", - "text": "Implementation merged on feature/backlog-batch-80-wave1 from feature/boom-003: supported recruit firearms invoke Ewewukek's Musket Mod GunItem.mobUse reflectively and use musketmod reloadDuration when available; compileJava passed. Remaining acceptance proof moved to BOOM-003A for focused runtime/GameTest firearm combat verification of projectile spawn, reload cadence, and no-ammo denial." - }, - { - "date": "2026-04-29", - "text": "Correction after scope clarification: keep Ewewukek's Musket Mod (musketmod) as the only firearm compatibility target. Do not integrate Medieval Boomsticks, Johannes Renaissance Rebirth, or other firearm mods in this slice." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) Existing recruit firearm compat code routes supported musketmod firearms through the dedicated recruit firearm path in CrossBowmanEntity / RecruitRangedMusketAttackGoal / MusketModCompat; 2) BOOM-003A runtime verification passed with musketmod present via ./gradlew runGameTestServer in the dedicated worktree, providing the projectile/reload/ammo proof BOOM-003 required; 3) current cleanup integration branches have remained green on ./gradlew test and ./gradlew verifyGameTestStage." - } - ], - "evidence": [], - "doneDate": "2026-04-30" - }, - { - "id": "BOOM-004", - "title": "Expose recruit firearm compatibility and ammo state in player feedback", - "status": "done", - "updated": "2026-04-30", - "why": "Players need to understand whether a recruit firearm is truly supported, has valid ammo, or is falling back, otherwise musketmod integration will look randomly broken even after the runtime path is fixed.", - "scope": [ - "Add player-facing feedback for supported firearm status, missing ammo, and unsupported weapon fallback in the relevant recruit inventory, command, or combat feedback surfaces. Keep the messages localized and server-authoritative." - ], - "acceptance": [ - "Players can tell when a recruit firearm is supported, missing ammo, or unsupported through in-game feedback rather than guesswork; visible text is localized; deterministic verification covers supported and unsupported loadout feedback." - ], - "dependencies": [ - "BOOM-002", - "BOOM-003", - "VANILLA-013" - ], - "progress": [ - { - "date": "2026-04-30", - "text": "Picked up on feature/cleanup-batch-8 after BOOM-003/003A closure; goal is localized player-facing recruit firearm support/ammo/fallback feedback that unlocks BOOM-001 and DOC-004." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) RecruitInventoryScreen now shows localized firearm support/ammo/fallback status using the existing recruit main-hand item plus inventory state through BannerModRecruitFirearmStatus; 2) ./gradlew compileJava and ./gradlew test --tests com.talhanation.bannermod.shared.military.BannerModRecruitFirearmStatusTest passed on feature/boom-004; 3) MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html were updated for the supported workflow; 4) ./gradlew test and ./gradlew verifyGameTestStage passed on feature/cleanup-batch-8." - } - ], - "evidence": [], - "doneDate": "2026-04-30" - }, - { - "id": "SHIPS-003", - "title": "Show sea-trade carrier route state and failure reasons", - "status": "done", - "updated": "2026-04-29", - "why": "Ship-backed sea trade will still feel opaque if players cannot see which carrier is assigned, whether it is loading, traveling, blocked, or failed, and why a route is stalled.", - "scope": [ - "Add player-facing route state for ship-backed sea trade, including carrier assignment, cargo progress, transit state, and actionable failure reasons in the relevant settlement, logistics, or trade UI surfaces." - ], - "acceptance": [ - "Players can inspect a ship-backed route and see carrier assignment, current phase, and failure or stall reason; route state updates when the carrier progresses or fails; localized verification covers at least one successful and one blocked route." - ], - "dependencies": [ - "SHIPS-001", - "SHIPS-002", - "VANILLA-013" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Players can inspect ship-backed route state in governor logistics: focused tests cover server-authored sea-trade status lines with route id, carrier assignment/unassigned state, current phase, cargo progress, successful route, missing-carrier state, and destination-full blocked reason. 2) Failure/stall reasons are localized for EN/RU, including explicit destination-full and no-carrier reasons. 3) Route state updates are sourced from the existing server sea-trade execution records surfaced through settlement snapshots/client mirror. 4) Verification: merged branch passed compileJava, compileGameTestJava, processResources, BannerModSettlementServiceTest, and BannerModSettlementClientMirrorTest; player guides and almanac were updated." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "MERCH-002", - "title": "Expose mounted merchant and courier transport state and fallback reasons", - "status": "done", - "updated": "2026-04-28", - "why": "If merchants or couriers gain mounts or other transport but the player cannot see when transport is active, unavailable, or lost, the realism gain turns into debugging noise.", - "scope": [ - "Add player-facing visibility for merchant or courier transport assignment, mounted status, and fallback reasons when no mount or vehicle is available, preserving localized and server-authoritative feedback." - ], - "acceptance": [ - "Players can tell whether a merchant or courier is mounted, why it is unmounted, and what fallback route behavior is active; feedback updates as transport is assigned or lost; verification covers mounted and fallback cases." - ], - "dependencies": [ - "MERCH-002A" - ], - "progress": [ - { - "date": "2026-04-28", - "text": "Implementation merged on feature/backlog-batch-80-wave1 from feature/merch-002: worker inspection now reports mounted status and fallback reasons with EN/RU localization plus guide/almanac updates; compileJava and processResources passed. Remaining acceptance proof moved to MERCH-002A for focused runtime verification of mounted and fallback cases." - } - ], - "verification": [ - { - "date": "2026-04-28", - "result": "Server-side worker inspection reports transport assignment and fallback state through WorkerTransportService.inspectionMessage; localized mounted and fallback messages exist; MERCH-002A verified mounted and no-mount route-on-foot inspection cases via GameTests; ./gradlew verifyGameTestStage passed." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "GAME-006", - "title": "Materialize occupation control as visible world and authority state", - "status": "done", - "updated": "2026-04-28", - "why": "Occupation remains too ledger-like unless players can see which territory is under occupation and what control restrictions or obligations are active in moment-to-moment play.", - "scope": [ - "Add visible occupation state and ongoing authority consequences to claims, settlements, or relevant world interactions so occupied control is not only a saved record. Keep the first implementation readable and deterministic rather than decorative." - ], - "acceptance": [ - "Players can observe when a territory is occupied through world or UI state and can understand at least one concrete control consequence of occupation; verification covers occupation start, active occupied state, and clearing the state." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) Occupied claim chunks are visible through localized HUD/War Room state; 2) server policy denies occupied owner manual control while occupier controls the claim and clearing removes state; 3) player docs landed and task verification passed before merge." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "GAME-007", - "title": "Materialize tribute and tax obligations beyond hidden ledgers", - "status": "done", - "updated": "2026-04-28", - "why": "Tribute and tax systems remain abstract if their effects are only numeric ledger mutations with no player-readable obligation flow or visible consequence in settlements and war.", - "scope": [ - "Surface tribute and tax obligations as ongoing gameplay-visible state, including what is owed, whether it is being paid, and what failure means for the affected side, while reusing existing treasury or stockpile systems where possible." - ], - "acceptance": [ - "Players can see tribute or tax obligations and whether they are being satisfied; non-payment has a visible consequence path instead of only silent ledger drift; verification covers active obligation, payment, and failure states." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) Governor UI shows localized collected/due tax obligation and satisfied/unpaid state; 2) heartbeat tests cover paid taxes and siege-blocked unpaid consequences; 3) player docs landed and task verification passed before merge." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "GAME-008", - "title": "Materialize revolt pressure and aftermath for players", - "status": "done", - "updated": "2026-04-28", - "why": "Revolt state is hard to reason about if it only lives in schedules and records; players need to see why revolt pressure exists, what objective state matters, and what aftermath follows success or failure.", - "scope": [ - "Add player-readable revolt pressure, objective status, and aftermath feedback through War Room, claim or settlement state, or world cues so revolt progression is legible before and after resolution." - ], - "acceptance": [ - "Players can see why a revolt is pending or progressing, what objective or pressure state matters, and what changed after success or failure; verification covers one pending and one resolved revolt flow." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) War Room revolt rows show pending pressure, due tick/objective chunk, and resolved success/failure aftermath; 2) revolt scheduler tests cover pending and resolved flows; 3) player docs landed and task verification passed before merge." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "UX-007", - "title": "Add War Room revolt interaction flow", - "status": "done", - "updated": "2026-04-29", - "why": "A player-facing revolt system is still incomplete until War Room exposes the actual revolt interactions instead of only passive text or slash-command knowledge.", - "scope": [ - "Add the relevant revolt interaction flow to War Room or the equivalent screen, including action availability, pending-state display, and server-authoritative feedback." - ], - "acceptance": [ - "A player can inspect and perform the supported revolt interaction flow through UI rather than chat commands; availability and denial feedback are clear; verification covers one allowed and one denied revolt interaction." - ], - "dependencies": [ - "GAME-008", - "AUTH-005", - "VANILLA-013" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) War Room now exposes pending revolt resolution controls for the selected war, allowing operator users to mark the first pending revolt as won or failed through UI rather than chat. 2) Availability/denial feedback is localized and server-authoritative through MessageResolveRevolt and RevoltInteractionService; non-operators are denied and pending state/occupation are preserved. 3) Verification: merged branch passed compileJava, compileGameTestJava, processResources, and RevoltInteractionServiceTest; player guides and almanac were updated." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "UX-008", - "title": "Add UI for tribute and remaining war outcome selection", - "status": "done", - "updated": "2026-04-29", - "why": "War outcome UX remains incomplete while tribute-related and remaining supported outcome choices are still partly command-shaped or admin-shaped instead of coherent in-screen interactions.", - "scope": [ - "Extend War Room or outcome panels so supported tribute and remaining outcome choices are selectable in UI with clear authority, cooldown, and denial feedback." - ], - "acceptance": [ - "Players can complete supported tribute or remaining outcome choices through UI with clear accepted and denied feedback; unsupported or admin-only paths remain visibly locked; verification covers at least one tribute or remaining outcome selection path." - ], - "dependencies": [ - "GAME-007", - "AUTH-005", - "VANILLA-013" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) War Room outcome buttons now gate cancel/occupy/annex/tribute by selected war state, matching war goal, attacking-side authority, and operator requirement for tribute. 2) Unsupported/admin-only paths are visibly locked with localized disabled reasons, and outcome requests report accepted/denied feedback through chat and the War Room feedback line. 3) Verification: merged branch passed compileJava, compileGameTestJava, processResources, MessageResolveWarOutcomeTest, and WarOutcomeApplierTest; player guides and almanac were updated." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "UX-009", - "title": "Explain war authority and cooldown denials in-screen", - "status": "done", - "updated": "2026-04-29", - "why": "War interactions are still too opaque if denial reasons only reach chat or are inferred from disabled controls without an explicit why, especially for leadership, co-leader, cooldown, and government-form restrictions.", - "scope": [ - "Surface explicit in-screen denial reasons for war declaration, outcome, ally, and revolt actions where authority or cooldown rules block the player. Reuse existing server-authoritative policy text where possible." - ], - "acceptance": [ - "Players can see why a war-related action is denied without relying only on chat spam; leadership, co-leader, cooldown, and government-form denials are distinguishable; verification covers representative denial reasons across the main war actions." - ], - "dependencies": [ - "AUTH-005", - "VANILLA-013" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) War Room and related war screens now show in-screen server-authoritative feedback for declare war, siege placement, ally invite/respond/cancel, and war outcomes, avoiding chat-only denial feedback. 2) Disabled tooltips distinguish leader-only, republic co-leader, monarchy-blocked co-leader, missing state, status, cooldown, closed-war, unsupported-goal, and op-only denials. 3) Verification: merged branch passed compileJava, compileGameTestJava, processResources, PoliticalEntityAuthorityTest, WarCooldownPolicyTest, and WarCooldownPolicyImmunityTest; player guides and almanac were updated." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "PROC-001", - "title": "Add task worktree and branch bootstrap helper", - "status": "done", - "updated": "2026-04-28", - "why": "The process now requires one worktree and one feature branch per parallel task, but there is no dedicated helper to create dependency-aware task workspaces consistently, which invites human error.", - "scope": [ - "Add a tooling path that bootstraps a task-specific worktree and feature branch from the correct dependency-aware base branch, with clear handling for independent tasks versus dependency chains." - ], - "acceptance": [ - "A contributor can create a task worktree and feature branch from the correct base using one documented helper flow; dependency chains branch from the updated parent branch tip rather than a stale base; verification covers an independent task and a dependency-chain task." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) tools/task-worktree helper and docs landed for independent and dependency-chain task worktrees; 2) dry-run and real temporary worktree checks covered both flows; 3) task branch verification passed before merge and current PR is backlog-only on top of merged master." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PROC-002", - "title": "Add backlog helper for finish-or-split parent task rewiring", - "status": "done", - "updated": "2026-04-28", - "why": "The process now requires executors to either finish a task or split it into child tasks immediately, but that parent or child rewiring flow is still manual and easy to do inconsistently.", - "scope": [ - "Add a helper flow or command for creating child tasks and rewiring the parent dependencies and progress note when a large task must be split during execution." - ], - "acceptance": [ - "A contributor can split an oversized task into concrete child tasks and rewire the parent dependency graph through one documented helper flow; the parent progress note captures what landed and what moved into children; verification covers one sample split workflow." - ], - "dependencies": [ - "PROC-001" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "Acceptance satisfied: tools/backlog split now creates concrete child tasks from a child spec file, rewires the parent dependencies to those child IDs, and appends the required parent progress note in one documented helper flow. Verification: python3 -m py_compile tools/backlog.py passed; tools/backlog split --help passed; temp-file sample split passed with dry-run, real split, assertions for parent in_progress/dependencies/progress note, and temp backlog validate; canonical tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PERF-010", - "title": "Add recruit render profiling counters", - "status": "done", - "updated": "2026-04-28", - "why": "PERF-002 needs profiling evidence before safe crowd-render optimization decisions.", - "scope": [ - "Instrument recruit rendering with bounded counters for base model, layers, nameplates, texture or state switches, and animation or pose churn where practical." - ], - "acceptance": [ - "Recruit render profiling output exposes recruit.render counters that distinguish the major render cost buckets needed by PERF-002." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) Merged recruit.render counters cover renderer totals, base estimate, layers, nameplates, texture/state switches, and pose churn; 2) instrumentation is bounded and client-local; 3) task branch verification passed before merge and current PR is backlog-only on top of merged master." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PERF-011", - "title": "Implement measured distant recruit crowd renderer cuts", - "status": "in_progress", - "updated": "2026-04-29", - "why": "Large recruit crowds need optimization backed by PERF-010 evidence without harming close-range readability.", - "scope": [ - "Use PERF-010 profiling evidence to skip or simplify safe distant crowd render work such as non-selected nameplates, held items, armor, overlays, or simplified model paths." - ], - "acceptance": [ - "A before-and-after profiling run shows improved large-crowd recruit rendering while close-range selected recruits remain readable." - ], - "dependencies": [ - "PERF-011A" - ], - "progress": [ - { - "date": "2026-04-29", - "text": "Implemented a minimal distant crowd cut by moving the crowd impostor threshold from 56 to 48 blocks and added a renderer source guard that keeps 32-block armor/layer and 16-block crowded-nameplate readability thresholds below the impostor cutoff; compileJava and focused renderer source test pass. Remaining acceptance requires a real client before/after large-crowd profiling run, which is blocked in this headless API session." - }, - { - "date": "2026-04-29", - "text": "Held open: renderer cutoff branch exists but acceptance requires PERF-011A real client before/after large-crowd profiling; API/headless session cannot provide visual selected-recruit readability proof." - } - ], - "verification": [], - "evidence": [] - }, - { - "id": "PORT-002", - "title": "Prepare NeoForge 1.21.1 build baseline", - "status": "done", - "updated": "2026-04-28", - "why": "PORT-001 must start with a buildable platform baseline before API migration can be verified.", - "scope": [ - "Create the NeoForge port branch baseline by updating Gradle, Java/toolchain settings, mod metadata, and dependency declarations for NeoForge 1.21.1." - ], - "acceptance": [ - "The port branch resolves NeoForge 1.21.1 dependencies and reaches Java compilation errors only from source API migration, not build metadata or dependency setup." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) NeoForge 1.21.1 build baseline is merged on master; 2) task verification reached Java source API migration errors after dependency resolution, not build metadata or dependency setup; 3) current PR is backlog-only on top of merged master." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PORT-003", - "title": "Migrate BannerMod source APIs to NeoForge 1.21.1", - "status": "done", - "updated": "2026-04-28", - "why": "Forge 1.20.1 APIs must be migrated before the port can compile or smoke run.", - "scope": [ - "Migrate registries, networking, events, item data components or NBT usages, and dependency APIs touched by root src for NeoForge 1.21.1." - ], - "acceptance": [ - "Root source compiles against NeoForge 1.21.1 with old Forge 1.20.1 assumptions removed or documented." - ], - "dependencies": [ - "PORT-009" - ], - "progress": [ - { - "date": "2026-04-28", - "text": "Attempted direct verification on feature/port-003: ctx log -- ./gradlew compileJava failed with 1130 compile errors. First clusters are removed or renamed Forge event/overlay APIs, entity inventory/synched-data/saved-data APIs, registry/menu/spawn egg/item component APIs, and networking payload assumptions. Split remaining PORT-003 source migration into dependency chain PORT-005 -> PORT-006 -> PORT-007 -> PORT-008 -> PORT-009; PORT-003 now waits for PORT-009 to prove root compileJava." - } - ], - "verification": [ - { - "date": "2026-04-28", - "result": "Verified on feature/port-batch-16: ctx log -- ./gradlew compileJava succeeded against NeoForge 1.21.1 with source API migration blockers cleared." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PORT-004", - "title": "Verify NeoForge port tests and smoke run", - "status": "done", - "updated": "2026-04-28", - "why": "PORT-001 acceptance requires tests and in-game smoke after source migration.", - "scope": [ - "Run compileJava, tests, and an in-game smoke on the NeoForge 1.21.1 port branch; fix or document remaining platform-only issues." - ], - "acceptance": [ - "compileJava, tests, and in-game smoke pass on the NeoForge 1.21.1 port branch." - ], - "dependencies": [ - "PORT-003" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "Verified on feature/port-batch-16: ctx log -- ./gradlew compileJava succeeded; ctx log -- ./gradlew test succeeded; ctx log -- ./gradlew verifyGameTestStage succeeded, exercising the NeoForge GameTest smoke server." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "ARMY-004", - "title": "Reject unsupported queued command intents before enqueue", - "status": "done", - "updated": "2026-04-28", - "why": "ARMY-001 requires unsupported queue variants to fail visibly instead of being accepted and ignored.", - "scope": [ - "Audit CommandIntent enqueue paths and add explicit rejection for any intent kind that the queue runtime cannot execute yet." - ], - "acceptance": [ - "Every unsupported queue intent is rejected before enqueue with a focused test proving it is not silently accepted." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) Unsupported queued intents are rejected before enqueue in dispatcher/runtime; 2) focused queue tests prove unsupported queued intents are not silently accepted; 3) task branch verification passed before merge and current PR is backlog-only on top of merged master." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "ARMY-005", - "title": "Add queued intent parity for remaining command variants", - "status": "done", - "updated": "2026-04-29", - "why": "Queued command behavior must match immediate command behavior for all supported army command variants.", - "scope": [ - "Implement or verify queue execution parity for movement, face, attack, aggro, stance, strategic fire, and siege-machine flows." - ], - "acceptance": [ - "Focused tests prove queue parity with immediate behavior for movement, face, attack, aggro, stance, strategic fire, and siege-machine command flows." - ], - "dependencies": [ - "ARMY-004", - "VANILLA-013" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "Acceptance: focused queue parity coverage added for movement, face, attack, aggro, stance, strategic fire, and siege-machine command flows. compileJava passed; compileGameTestJava passed; focused CommandIntentQueueTest passed; verifyGameTestStage rerun no longer reports queuedcommandvariantsmatchimmediatefirstorderbehavior after the fix, but remains red from unrelated existing failures: primaryzonematrixallowsdisjointzoneswithinoverlappingbounds, mountedcourierinspectionreportsmountedtransport, sharedownershipkeepsplayercycleauthorityaligned, grouptransferupdatesgroupandmembersfromtrustedplayer, unmountedcourierinspectionreportsfootfallback, reconnectedownerrecoversauthorityafterownershiproundtrip, courierusesnearbyhorseonsupportedroute, workerdeathreleasesactiveworkorderclaim." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "AUTH-006", - "title": "Define canonical recruit command authority roles", - "status": "done", - "updated": "2026-04-28", - "why": "AUTH-005 needs one documented authority model before packet and UI enforcement can be made consistent.", - "scope": [ - "Document in code the supported owner, teammate, admin, and nation-level command authority roles, and remove or mark dead unsupported nation paths." - ], - "acceptance": [ - "Command authority role semantics are defined in one code location and dead nation-authority branches are either removed or explicitly unsupported." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) CommandHierarchy is the canonical authority role source for owner, teammate, and admin semantics; 2) dead nation command role branches are removed or explicitly unsupported until server-authoritative nation membership exists; 3) task branch verification passed before merge and current PR is backlog-only on top of merged master." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "AUTH-007", - "title": "Apply command authority roles across entry points", - "status": "done", - "updated": "2026-04-29", - "why": "AUTH-005 requires the canonical authority model to be enforced consistently, not only documented.", - "scope": [ - "Apply AUTH-006 authority semantics across recruit command packets and related UI affordances." - ], - "acceptance": [ - "Owner, teammate, admin, and supported or explicitly unsupported nation authority cases behave consistently across command entry points." - ], - "dependencies": [ - "AUTH-006", - "VANILLA-013" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "Acceptance: owner/teammate/admin/unsupported nation authority semantics are covered by CommandTargetingAuthorityTest, which passed via ctx log -- ./gradlew test --tests com.talhanation.bannermod.network.messages.military.CommandTargetingAuthorityTest; representative command entry points are covered by BannerModMultiplayerAuthorityConflictGameTests owner movement, teammate MessageFollowGui, admin MessageRest, and unsupported nation/outsider MessageRest cases. compileJava and compileGameTestJava passed. verifyGameTestStage remains red only on unrelated tests: primaryzonematrixallowsdisjointzoneswithinoverlappingbounds, mountedcourierinspectionreportsmountedtransport, authoredroutecouriermovesitemsbetweenstorageendpoints, grouptransferupdatesgroupandmembersfromtrustedplayer, reconnectedownerrecoversauthorityafterownershiproundtrip, sharedownershipkeepsplayercycleauthorityaligned, unmountedcourierinspectionreportsfootfallback, workerdeathreleasesactiveworkorderclaim; AUTH-007 tests are not failing." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "AUTH-008", - "title": "Test command authority hierarchy", - "status": "done", - "updated": "2026-04-29", - "why": "AUTH-005 closure requires verification for owner, teammate, and nation-level command cases.", - "scope": [ - "Add focused tests covering owner, teammate, admin override, and nation-level supported or denied command authority flows." - ], - "acceptance": [ - "Focused verification covers owner, teammate, admin override, and nation-level command authority cases." - ], - "dependencies": [ - "AUTH-007", - "VANILLA-013" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Owner/teammate/admin/nation hierarchy coverage: ./gradlew test --tests com.talhanation.bannermod.network.messages.military.CommandTargetingAuthorityTest passed, covering owner, teammate, outsider-admin override, and explicitly unsupported nation/outsider denial through CommandHierarchy and CommandTargeting. 2) Packet-path/GameTest coverage: src/gametest/java/com/talhanation/bannermod/BannerModMultiplayerAuthorityConflictGameTests.java now explicitly exercises owner and teammate MessageFollowGui paths, admin MessageRest path, and unsupported nation/outsider MessageRest denial; ./gradlew compileGameTestJava passed. 3) compileJava passed. 4) ./gradlew verifyGameTestStage was run; it failed on unrelated non-authority tests (primaryzonematrixallowsdisjointzoneswithinoverlappingbounds, mountedcourierinspectionreportsmountedtransport, authoredroutecouriermovesitemsbetweenstorageendpoints, grouptransferupdatesgroupandmembersfromtrustedplayer, reconnectedownerrecoversauthorityafterownershiproundtrip, sharedownershipkeepsplayercycleauthorityaligned, courierusesnearbyhorseonsupportedroute, workerdeathreleasesactiveworkorderclaim), with no AUTH-008 authority GameTests listed as failed." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "SYNC-002", - "title": "Define settlement client snapshot contract", - "status": "done", - "updated": "2026-04-28", - "why": "SYNC-001 needs a stable server-to-client data contract before UI mirrors can be implemented.", - "scope": [ - "Define the settlement/governor snapshot payload, versioning or stale markers, login/open-screen refresh triggers, and mutation refresh events." - ], - "acceptance": [ - "Settlement snapshot contract covers login, screen open, mutation refresh, loading state, and stale-state semantics in code-level documentation or types." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) Added BannerModSettlementClientSnapshotContract with typed payload, contract version, stale marker, LOGIN/SCREEN_OPEN/MUTATION_REFRESH triggers, and LOADING/READY/STALE semantics; 2) ./gradlew compileJava passed; 3) tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "SYNC-003", - "title": "Implement settlement client mirror and screen loading states", - "status": "done", - "updated": "2026-04-29", - "why": "Player-facing settlement UIs need coherent mirrored state instead of ad hoc placeholder requests.", - "scope": [ - "Implement the client mirror for SYNC-002 snapshots and update relevant settlement/governor screens to render loading, stale, empty, and fresh states from it." - ], - "acceptance": [ - "Relevant settlement UIs render from the synced mirror and show explicit loading or stale states without reopening screens." - ], - "dependencies": [ - "SYNC-002", - "VANILLA-013" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "Acceptance: relevant governor/settlement UI now opens in loading state from the client mirror, accepts server-synced ready/empty envelopes, marks the existing mirror stale after policy mutation until the next server refresh, and renders those states without reopening; focused test BannerModSettlementClientMirrorTest covers loading, empty, fresh, and stale formatting/action enablement; compileJava passed; compileGameTestJava not run because no GameTest sources were touched." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "SYNC-004", - "title": "Verify settlement snapshot refresh paths", - "status": "done", - "updated": "2026-04-29", - "why": "SYNC-001 requires proof that login, open-screen, and mutation refresh paths update the client mirror.", - "scope": [ - "Add focused verification for settlement snapshot delivery on login, screen open, and server-side mutation refresh." - ], - "acceptance": [ - "Focused tests or GameTests prove login, open-screen, and mutation refresh paths update settlement client snapshots." - ], - "dependencies": [ - "SYNC-003", - "VANILLA-013" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "Acceptance: focused tests prove login, screen-open, and mutation-refresh settlement client snapshot paths. Verification: ./gradlew compileJava passed; ./gradlew compileGameTestJava passed; ./gradlew test --tests com.talhanation.bannermod.client.settlement.BannerModSettlementClientMirrorTest --tests com.talhanation.bannermod.events.RecruitGovernorWorkflowTest --tests com.talhanation.bannermod.network.messages.military.MessageToClientUpdateGovernorScreenTest passed, covering LOGIN/MUTATION_REFRESH mirror replacement, SCREEN_OPEN/LOGIN/MUTATION_REFRESH workflow envelopes, and MessageToClientUpdateGovernorScreen payload roundtrip." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "UX-010", - "title": "Resolve world-map claim-area affordance", - "status": "done", - "updated": "2026-04-28", - "why": "UX-002 requires every visible world-map action to be functional or intentionally absent.", - "scope": [ - "Audit world-map claim-area controls and either implement the claim-area flow end-to-end or remove/disable the visible affordance with a clear reason." - ], - "acceptance": [ - "The world map no longer exposes a broken claim-area action; the action is either functional or intentionally absent/disabled with localized explanation." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "Acceptance: world map no longer exposes a broken claim-area action; Claim Area is visible but disabled with localized hover explanation, chunk claiming remains server-authoritative through MessageClaimIntent; compileJava and processResources passed; player guides and almanac updated." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "UX-011", - "title": "Expose route and waypoint discovery in world-map UI", - "status": "done", - "updated": "2026-04-29", - "why": "Players need in-game affordances and explanations for route and waypoint flows without relying on external docs.", - "scope": [ - "Add Minecraft-native route and waypoint explanations, enabled/disabled reasons, and visible entry points in the world-map flow." - ], - "acceptance": [ - "Players can discover route and waypoint creation/use from localized in-game world-map affordances and explanations." - ], - "dependencies": [ - "UX-010", - "VANILLA-013" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Localized in-game world-map affordances added: top-left Routes help/status panel explains + route creation, route selection, and right-click waypoint creation/use; route buttons have localized tooltips; right-click Add Waypoint remains visible when unavailable with localized no-route or unexplored/unloaded-chunk reasons. 2) Server authority preserved: only existing WorldMapRouteMutationController/ClientManager route-save and waypoint mutation paths are reused; no client-side server gameplay validation was added or bypassed. 3) Player workflow docs updated in MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html. 4) Verification passed: ctx log -- ./gradlew compileJava; ctx log -- ./gradlew test --tests com.talhanation.bannermod.client.military.gui.worldmap.WorldMapRouteUiVerificationTest; ctx log -- jq empty modified lang JSON files; tools/backlog validate." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "UX-012", - "title": "Verify and document world-map claim route waypoint flows", - "status": "done", - "updated": "2026-04-29", - "why": "UX-002 closure requires verification and guide coverage for player-facing map flows.", - "scope": [ - "Verify claim-area, waypoint, and route interactions and update MULTIPLAYER_GUIDE_RU.md, MULTIPLAYER_GUIDE_EN.md, and docs/BANNERMOD_ALMANAC.html if the player-facing flow changes." - ], - "acceptance": [ - "Verification covers claim-area, waypoint, and route interactions; required guide and almanac updates match the shipped world-map behavior." - ], - "dependencies": [ - "UX-011", - "VANILLA-013" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "Acceptance: claim-area, waypoint, and route interactions verified by focused source-inspection test WorldMapRouteUiVerificationTest.worldMapClaimWaypointAndRouteFlowDocsMatchShippedInteractions; test confirms Claim Area is disabled, claim mutations route through server MessageClaimIntent, waypoint disabled reasons/add/remove/drag explored checks are wired, routes persist through ClientManager.saveRoute, and EN/RU guides plus almanac match shipped behavior. Verification run: ./gradlew test --tests com.talhanation.bannermod.client.military.gui.worldmap.WorldMapRouteUiVerificationTest passed; ./gradlew compileJava passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "GAME-009", - "title": "Bind settlement projects to real build areas", - "status": "done", - "updated": "2026-04-29", - "why": "GAME-001 needs project records to target executable world construction instead of noop resolver placeholders.", - "scope": [ - "Replace noop build-area resolution for settlement projects with binding to real build areas or an equivalent executable world-work target." - ], - "acceptance": [ - "Creating a settlement project produces a real build-area or equivalent executable world target rather than a noop placeholder." - ], - "dependencies": [ - "VANILLA-013" - ], - "progress": [ - { - "date": "2026-04-29", - "text": "Implemented live BuildArea binding for settlement project runtime/orchestrator and added BannerModSettlementProjectGameTests.settlementProjectBindsToExecutableBuildAreaTarget. Verified ctx log -- ./gradlew compileJava passed and ctx log -- ./gradlew compileGameTestJava passed. Runtime GameTest verification via ctx log -- ./gradlew verifyGameTestStage is blocked by unrelated existing suite failures/crash before a focused result can be claimed: primaryzonematrixallowsdisjointzoneswithinoverlappingbounds failed, then GameTest server crashed in AbstractChestGoal.scanAvailableStorageAreas/DepositItemsToStorage on a merchant UnsupportedOperationException. GAME-009 acceptance is implemented but left open because the focused GameTest could not be observed green in this run." - } - ], - "verification": [ - { - "date": "2026-04-29", - "result": "PASS: ctx log -- ./gradlew compileJava compileGameTestJava runGameTestServerGame009 isolated bannermod_game_009 GameTest namespace; settlementProjectBindsToExecutableBuildAreaTarget proved project binds to executable BuildArea target. PASS: tools/backlog validate." - }, - { - "date": "2026-04-29", - "result": "PASS after feature-branch merge: ctx log -- ./gradlew compileGameTestJava runGameTestServerGame009. Isolated bannermod_game_009 holder uses bannermod:harness_empty template and proves settlement project binds to executable BuildArea target." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "GAME-010", - "title": "Drive project progress from actual build execution", - "status": "done", - "updated": "2026-04-29", - "why": "Settlement project progress must reflect real construction work, not placeholder record state.", - "scope": [ - "Connect project assignment and progress updates to actual worker/build execution events and completion state." - ], - "acceptance": [ - "Project progress advances and completes from real build execution events, with focused verification for creation, assignment, and completion." - ], - "dependencies": [ - "GAME-009", - "VANILLA-013" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Creation/assignment: ctx log -- ./gradlew runGameTestServerGame010 passed; focused bannermod_game_010 GameTest creates a PendingProject, binds it to a live BuildArea through ClaimBuildAreaResolver, and observes SEARCHING_BUILDER. 2) Progress advancement: same GameTest calls BuildArea.setStartBuild(false), the production BuildArea start hook updates BannerModSettlementProjectRuntime assignment state to IN_PROGRESS. 3) Completion: same GameTest calls BuildArea.setDone(true), the production BuildArea completion hook updates assignment state to COMPLETED. Compile gates also passed: ctx log -- ./gradlew compileJava and ctx log -- ./gradlew compileGameTestJava." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "GAME-011", - "title": "Add in-world claim boundary and authority cues", - "status": "done", - "updated": "2026-04-29", - "why": "GAME-004 needs protected territory to be legible from normal world play, not only the map screen.", - "scope": [ - "Add restrained in-world boundary or authority feedback for claims, including ownership cues that are visible during common play without cluttering HUD or world visuals." - ], - "acceptance": [ - "Players can perceive friendly, hostile, and unclaimed territory state from normal world play through localized cues that avoid HUD/chat overlap." - ], - "dependencies": [ - "VANILLA-013" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Normal-play claim HUD now renders localized friendly/hostile/unclaimed authority labels in the existing top-right overlay stack, with friendly matching the server-side scoreboard-team claim ownership rule and hostile covering other owned claims; unclaimed renders for no claim/no owner. 2) Overlap check: reused HudOverlayCoordinator top-right stack below battle-window and siege-zone chips, with right margin/top safe margin, no chat/hotbar/crosshair/boss-bar overlap introduced. 3) Localization/docs check: en_us and ru_ru keys added; EN/RU guides and almanac updated. 4) Verification: focused ClaimAuthorityStatusTest passed; ./gradlew compileJava passed." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "GAME-012", - "title": "Add protected-space interaction feedback", - "status": "done", - "updated": "2026-04-29", - "why": "GAME-004 requires common claim-protected interactions to explain why actions are allowed or denied.", - "scope": [ - "Add clear in-world or localized feedback for common protected-space interactions in friendly, hostile, and unclaimed territory." - ], - "acceptance": [ - "Focused verification covers friendly, hostile, and unclaimed interaction feedback for protected-space actions." - ], - "dependencies": [ - "GAME-011", - "VANILLA-013" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Focused test ./gradlew test --tests com.talhanation.bannermod.events.ClaimProtectionFeedbackTest passed and covers friendly, hostile, and unclaimed feedback keys. 2) ./gradlew compileJava passed. 3) Player-facing docs and almanac updated for denied protected-space system feedback; no GameTests were touched, so compileGameTestJava was not required." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "GAME-013", - "title": "Define unified settlement bootstrap lifecycle", - "status": "done", - "updated": "2026-04-28", - "why": "GAME-005 needs one explicit lifecycle before manual and automatic founding paths can converge safely.", - "scope": [ - "Define the shared claim-to-settlement lifecycle for starter structures, workers, citizens, profession vacancies, and formal settlement records, including auto-bootstrap versus manual founding rules." - ], - "acceptance": [ - "Manual and automatic bootstrap rules are defined in code-level documentation or types with one canonical lifecycle and no contradictory state assumptions." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "Acceptance: SettlementBootstrapLifecycle defines the shared claim-to-settlement lifecycle and documents manual founding versus automatic claim seeding without contradictory state assumptions; compileJava passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "GAME-014", - "title": "Converge manual and automatic settlement bootstrap paths", - "status": "done", - "updated": "2026-04-29", - "why": "Surveyor founding and automatic claim worker bootstrap should produce the same settlement lifecycle and records.", - "scope": [ - "Update manual and automatic bootstrap entry paths to create or reconcile the same settlement records, starter structures, workers, citizens, and profession state." - ], - "acceptance": [ - "Focused verification covers both manual and automatic entry paths and shows they converge on the same settlement lifecycle and data model." - ], - "dependencies": [ - "GAME-013", - "VANILLA-013" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Added BannerModBuildingInvalidationGameTests.manualAndAutomaticBootstrapConvergeOnStarterData, which executes manual STARTER_FORT bootstrap and automatic claim worker growth bootstrap in one GameTest and compares active SettlementRecord claim/faction binding, worker count/type set, free citizen count, and NONE profession state. 2) ctx log -- ./gradlew compileJava passed. 3) ctx log -- ./gradlew compileGameTestJava passed. 4) ctx log -- ./gradlew runGameTestServer was executed; the new GAME-014 test was not listed among failures, but the full stage remains blocked by existing unrelated required GameTest failures: primaryZoneMatrix, courier transport/route, ownership roundtrip, group transfer, and workorder claim failures." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "PORT-005", - "title": "Migrate NeoForge event and overlay APIs", - "status": "done", - "updated": "2026-04-28", - "why": "PORT-003 compile is blocked by removed or renamed Forge event and client overlay APIs across root src.", - "scope": [ - "Replace Forge event imports, event-bus annotations, and client HUD overlay hooks with NeoForge 1.21.1 APIs used by the active source tree." - ], - "acceptance": [ - "compileJava no longer reports missing Forge event, EventBusSubscriber, or RenderGuiOverlayEvent overlay API symbols." - ], - "dependencies": [ - "PORT-012" - ], - "progress": [ - { - "date": "2026-04-28", - "text": "Urgent PORT execution split after verification attempt: compileJava could not reach Java sources because build.gradle still references ForgeGradle fg.deobf. PORT-005 now waits on PORT-010 buildscript cleanup, PORT-011 event/eventbus migration, and PORT-012 HUD overlay migration; after those land, PORT-005 can close against the original event/overlay acceptance." - } - ], - "verification": [ - { - "date": "2026-04-28", - "result": "1) PORT-010 removed fg.deobf so compileJava reaches Java source compilation; 2) PORT-011 migrated active Forge event/eventbus/EventBusSubscriber usages and ctx search found zero old Forge event/eventbus/EventBusSubscriber patterns; 3) PORT-012 migrated active HUD overlay hooks and ctx search found zero RenderGuiOverlayEvent, RegisterGuiOverlaysEvent, VanillaGuiOverlay, IGuiOverlay, or ForgeGui symbols. compileJava still fails on later non-PORT-005 Forge common/network/distmarker/FML clusters." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PORT-006", - "title": "Migrate entity inventory and saved-data APIs", - "status": "done", - "updated": "2026-04-28", - "why": "PORT-003 compile is blocked by Minecraft 1.21.1 entity equipment, synched data, container, and saved-data API changes.", - "scope": [ - "Update recruit, citizen, and worker entity inventory, equipment, synched-data, damage, and saved-data usages to the 1.21.1-compatible APIs without changing gameplay semantics." - ], - "acceptance": [ - "compileJava no longer reports entity inventory, equipment, synched-data, item damage, SimpleContainer internals, or saved-data provider API errors." - ], - "dependencies": [ - "PORT-013" - ], - "progress": [ - { - "date": "2026-04-28", - "text": "Urgent PORT ordering update: after PORT-005 closure, compileJava is still masked by Forge common/distmarker/FML bootstrap imports before the entity inventory and saved-data acceptance can be observed. PORT-006 now waits for PORT-013 to clear that front blocker." - } - ], - "verification": [ - { - "date": "2026-04-28", - "result": "1) Migrated recruit/citizen/worker SynchedEntityData builder APIs, entity inventory ItemStack serialization/comparison, direct custom inventory iteration, item handler wrapper, and military/war/governance/settlement SavedData factories with HolderLookup.Provider load/save signatures; 2) ctx search found zero isSameItemSameTags matches and remaining .items matches are false positives, vanilla Player Inventory GUI access, metric strings, or non-inventory fields; 3) compileJava still fails on out-of-scope mixin mapping, Forge network/bootstrap, and registry migration clusters, with no PORT-006 inventory/saved-data provider API errors visible in the summary." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PORT-007", - "title": "Migrate registries, menus, spawn eggs, and item components", - "status": "done", - "updated": "2026-04-28", - "why": "PORT-003 compile is blocked by registry, menu, spawn egg, and item NBT/component API differences after the NeoForge 1.21.1 move.", - "scope": [ - "Update active root registrations and item data usage for menus, screens, spawn eggs, placement items, and related registries to NeoForge 1.21.1-compatible APIs." - ], - "acceptance": [ - "compileJava no longer reports missing registry, menu screen, spawn egg, or item NBT/component API symbols." - ], - "dependencies": [ - "PORT-006" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) Migrated active registries, menu holders, spawn egg entity data, almanac written-book data, wand/surveyor custom item data, and ForgeRegistries lookup call sites to NeoForge/vanilla 1.21.1 APIs; 2) targeted searches found zero legacy ForgeRegistries, net.minecraftforge.registries, RegistryObject, IForgeMenuType, ForgeSpawnEggItem, getOrCreateTag, hasTag, or item-stack setTag usages in active source, with remaining getTag matches limited to world-map menu entry labels; 3) compileJava still fails on out-of-scope mixin remap and PORT-008 net.minecraftforge.network/NetworkEvent/SimpleChannel errors." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PORT-008", - "title": "Migrate network packet payload APIs", - "status": "done", - "updated": "2026-04-28", - "why": "PORT-003 compile depends on replacing legacy Forge networking assumptions with NeoForge 1.21.1 payload registration and handling APIs.", - "scope": [ - "Update root network registration, message encoding/decoding, sender/context handling, and client/server dispatch call sites to NeoForge 1.21.1-compatible packet payload APIs." - ], - "acceptance": [ - "compileJava no longer reports networking API errors and at least one focused server-authoritative command packet path is code-inspected through the new payload handler." - ], - "dependencies": [ - "PORT-007" - ], - "progress": [ - { - "date": "2026-04-28", - "text": "Follow-up correction required: PORT-008 landed functional NeoForge payload registration but retained local net.minecraftforge.network compatibility shims. PORT-019 was added to remove those shims and migrate call sites to BannerMod-owned or NeoForge APIs before final compile closure." - } - ], - "verification": [ - { - "date": "2026-04-28", - "result": "1) Registered root packets via RegisterPayloadHandlersEvent/PayloadRegistrar and BannerModMessage payload adapter; 2) compileJava no longer reports net.minecraftforge.network, NetworkEvent, SimpleChannel, or PacketDistributor missing-package errors, and remaining failures are mixin remap, TickEvent/event API, pathfinding BlockPathTypes, and DistExecutor clusters; 3) inspected MessageMovement C2S path through IPayloadContext -> context wrapper -> CommandIntent.Movement -> CommandIntentDispatcher.dispatch, with sender authority still resolved from the server context and mismatched client UUIDs rejected by RecruitCommandTargetResolver." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PORT-009", - "title": "Close remaining NeoForge source compile blockers", - "status": "done", - "updated": "2026-04-28", - "why": "After the major API clusters are migrated, PORT-003 still needs a final pass to remove residual Forge 1.20.1 source assumptions.", - "scope": [ - "Fix or document remaining root source compile blockers after PORT-005 through PORT-008, keeping changes limited to active src and build metadata required for NeoForge 1.21.1 compilation." - ], - "acceptance": [ - "Root ./gradlew compileJava passes on the NeoForge 1.21.1 branch, with any intentionally retained compatibility assumptions documented." - ], - "dependencies": [ - "PORT-018" - ], - "progress": [ - { - "date": "2026-04-28", - "text": "Initial PORT-009 attempt updated TickEvent split, LivingIncomingDamageEvent replacement, PathType rename, removed DistExecutor dependency, and marked unresolved named mixin targets remap=false in a scratch worktree, but compileJava still exposed about 897 masked errors. Split remaining closure into PORT-014 event APIs, PORT-015 military inventory/equipment, PORT-016 projectile/ranged combat, PORT-017 identifier/profile/registry access, and PORT-018 final compile sweep. Scratch changes were not merged." - } - ], - "verification": [ - { - "date": "2026-04-28", - "result": "Verified on feature/port-batch-16: ctx log -- ./gradlew compileJava succeeded; residual NeoForge source compile blockers are cleared or represented by green compile." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PORT-010", - "title": "Remove NeoForge buildscript fg.deobf blocker", - "status": "done", - "updated": "2026-04-28", - "why": "PORT-005 cannot even reach Java compilation while build.gradle still uses ForgeGradle fg.deobf dependency helpers.", - "scope": [ - "Replace or remove root build.gradle fg.deobf usages with NeoForge-compatible dependency declarations while preserving declared optional integrations where possible." - ], - "acceptance": [ - "./gradlew compileJava reaches Java compilation instead of failing during Gradle evaluation on unknown property fg." - ], - "dependencies": [ - "PORT-002" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "./gradlew compileJava via tools/ai-context-proxy/bin/ctx log now reaches :compileJava and fails during Java source compilation rather than Gradle evaluation. First Java source clusters are remaining Forge API imports under entity/military, bootstrap/BannerModMain, entity/citizen, and entity/civilian. build.gradle no longer contains fg.deobf usages." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PORT-011", - "title": "Port NeoForge event bus imports and annotations", - "status": "done", - "updated": "2026-04-28", - "why": "PORT-005 compile is blocked by Forge event, eventbus, and EventBusSubscriber symbols after moving to NeoForge 1.21.1.", - "scope": [ - "Migrate active src Forge event imports, eventbus annotations, and mod event subscriber usages to NeoForge 1.21.1 equivalents without changing event behavior." - ], - "acceptance": [ - "compileJava no longer reports missing Forge event, eventbus, or EventBusSubscriber symbols." - ], - "dependencies": [ - "PORT-010" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "compileJava still fails on non-event Forge/common/network/distmarker/FML lifecycle clusters, but active src/main/java no longer contains old Forge event/eventbus/EventBusSubscriber usages; filtered compile output shows no missing Forge event/eventbus/EventBusSubscriber errors, only EventBusSubscriber.Bus deprecation warnings." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PORT-012", - "title": "Port NeoForge client HUD overlay hooks", - "status": "done", - "updated": "2026-04-28", - "why": "PORT-005 compile is blocked by removed Forge HUD overlay APIs such as RenderGuiOverlayEvent and VanillaGuiOverlay.", - "scope": [ - "Replace active client HUD overlay registration/render hooks with NeoForge 1.21.1 GUI event or layer APIs while preserving existing overlay visibility and non-overlap behavior." - ], - "acceptance": [ - "compileJava no longer reports missing RenderGuiOverlayEvent, VanillaGuiOverlay, RegisterGuiOverlaysEvent, or related overlay API symbols." - ], - "dependencies": [ - "PORT-011" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "Ported active client HUD overlay hooks to NeoForge 1.21.1 GUI layer/event APIs. Verified global source search has zero RenderGuiOverlayEvent, VanillaGuiOverlay, RegisterGuiOverlaysEvent, IGuiOverlay, or ForgeGui matches; compileJava still fails only on unrelated broad Forge package migration clusters. Review also fixed ClaimOverlayManager to render only after HOTBAR layer and used ResourceLocation.fromNamespaceAndPath for the layer id." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PORT-013", - "title": "Migrate NeoForge common, dist, and bootstrap APIs", - "status": "done", - "updated": "2026-04-28", - "why": "After PORT-005, compileJava is blocked before entity inventory migration by remaining Forge common helpers, dist markers, FML lifecycle/config imports, and bootstrap wiring.", - "scope": [ - "Replace active root Forge common helper imports, dist marker annotations, FML lifecycle/config imports, and central bootstrap event-bus wiring with NeoForge 1.21.1 equivalents while leaving payload networking and registry API rewrites to their dedicated PORT tasks." - ], - "acceptance": [ - "compileJava no longer reports missing net.minecraftforge.common, net.minecraftforge.api.distmarker, net.minecraftforge.fml lifecycle/config/javafmlmod, or bootstrap-only Forge symbols; remaining errors are ready for PORT-006 inventory/saved-data, PORT-007 registry/components, or PORT-008 network payload work." - ], - "dependencies": [ - "PORT-005" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "Targeted source searches for net.minecraftforge.common, net.minecraftforge.api.distmarker, and net.minecraftforge.fml returned zero matches. compileJava still reaches Java compilation and now fails on remaining PORT-008 network/SimpleChannel payload APIs, PORT-007 registry/component APIs, PORT-006 inventory/capability APIs, plus BlockPathTypes pathfinding rename outside this slice." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PORT-014", - "title": "Finish NeoForge event API migration", - "status": "done", - "updated": "2026-04-28", - "why": "PORT-009 compile is still blocked by remaining 1.21.1 event API changes after the initial eventbus and overlay port.", - "scope": [ - "Update remaining active source event handlers for split TickEvent classes, living tick/damage replacements, patrol spawn events, shield/quaff hooks, and related handler signatures." - ], - "acceptance": [ - "compileJava no longer reports missing TickEvent, LivingEvent/LivingTickEvent, LivingHurtEvent/LivingAttackEvent, patrol spawn, shield, or quaff event API symbols." - ], - "dependencies": [ - "PORT-008" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "ctx log -- ./gradlew compileJava no longer reports missing TickEvent, LivingEvent/LivingTickEvent, LivingHurtEvent/LivingAttackEvent, patrol spawn SpawnPlacements.Type, shield ShieldBlockEvent, or quaff PotionUtils event/API symbols. compileJava still fails on out-of-scope mixin remap targets, BlockPathTypes pathing rename, and DistExecutor removal." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PORT-015", - "title": "Port remaining military inventory and equipment APIs", - "status": "done", - "updated": "2026-04-28", - "why": "PORT-009 compile exposes additional inventory/equipment API changes beyond the first PORT-006 migration.", - "scope": [ - "Fix remaining military inventory, equipment slot, private Mob item list, item durability, and data component compile errors in active source." - ], - "acceptance": [ - "compileJava no longer reports inventory/equipment access, private Mob item list, item durability, or related data component errors." - ], - "dependencies": [ - "PORT-014" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "Ported remaining military inventory/equipment API usage: removed direct Mob armorItems/handItems access, switched NBT item restore to registry-aware ItemStack.parseOptional, replaced old sword damage comparisons with stack attribute modifier damage, and converted remaining military setDamageValue durability handling to hurtAndBreak/isDamageableItem. Targeted searches show no direct .armorItems/.handItems access, no military setDamageValue calls, and no old sword getDamage usage; remaining getDamage matches are Small Ships compatibility calls. compileJava still fails on out-of-scope PORT-018 mixin/pathing/DistExecutor blockers." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PORT-016", - "title": "Port projectile and ranged combat APIs", - "status": "done", - "updated": "2026-04-28", - "why": "PORT-009 compile exposes projectile and ranged combat API changes after earlier port blockers are removed.", - "scope": [ - "Update bowman/projectile creation, arrow state access, custom arrow hooks, ranged combat presentation, and related military projectile code to NeoForge 1.21.1 APIs." - ], - "acceptance": [ - "compileJava no longer reports projectile, arrow, or ranged combat API errors." - ], - "dependencies": [ - "PORT-015" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) compileJava reached Java compilation and reported no projectile, arrow, ranged combat, BowmanEntity, CrossbowWeapon, ProjectileUtil, or old enchantment API errors after the port; 2) remaining compileJava failures are unrelated mixin/pathfinding port blockers (BlockPathTypes and mixin target mappings); 3) focused search found no old getMobArrow/customArrow/POWER_ARROWS/PUNCH_ARROWS/FLAMING_ARROWS/INFINITY_ARROWS usages in active ranged code." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PORT-017", - "title": "Port identifier profile and registry access APIs", - "status": "done", - "updated": "2026-04-28", - "why": "PORT-009 compile exposes ResourceLocation, GameProfile, and registry access API changes that are distinct from registry object migration.", - "scope": [ - "Replace private ResourceLocation constructors, update GameProfile/profile utility usages, and fix CitizenCore or related registryAccess contracts exposed by the port." - ], - "acceptance": [ - "compileJava no longer reports ResourceLocation constructor, GameProfile/profile, or CitizenCore registry access API errors." - ], - "dependencies": [ - "PORT-016" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "Verified on feature/port-batch-16: ctx log -- ./gradlew compileJava succeeded; compile no longer reports ResourceLocation constructor, GameProfile/profile, or CitizenCore registry access API errors." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PORT-018", - "title": "Final NeoForge compile sweep", - "status": "done", - "updated": "2026-04-28", - "why": "After the known residual PORT-009 clusters land, a final compile pass is required to close masked source errors and prove the port compiles.", - "scope": [ - "Fix remaining root source compile errors after PORT-014 through PORT-017, documenting any intentionally retained temporary compatibility assumptions." - ], - "acceptance": [ - "Root ./gradlew compileJava passes on the NeoForge 1.21.1 branch." - ], - "dependencies": [ - "PORT-017", - "PORT-019", - "PORT-020", - "PORT-021" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "Verified on feature/port-batch-16: ctx log -- ./gradlew compileJava succeeded for the root NeoForge 1.21.1 source tree." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PORT-019", - "title": "Remove Forge network compatibility shims", - "status": "done", - "updated": "2026-04-28", - "why": "PORT-008 incorrectly hid networking migration behind local net.minecraftforge.network compatibility classes, leaving Forge namespace in active source.", - "scope": [ - "Delete local net.minecraftforge.network shim classes and migrate imports/call sites to BannerMod-owned adapters or direct NeoForge networking APIs without retaining Forge package names." - ], - "acceptance": [ - "Active src contains no net.minecraftforge.network imports or package declarations; packet sending, screen opening, and server sender context still route through NeoForge payload APIs; focused inspection covers MessageMovement authority flow." - ], - "dependencies": [ - "PORT-008" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "Removed active net.minecraftforge.network shim namespace and migrated call sites to com.talhanation.bannermod.network.compat adapters backed by NeoForge payload APIs. Verified ctx search 'net\\.minecraftforge\\.network|package net\\.minecraftforge\\.network' src/main/java returns 0. compileJava reports only existing mixin/pathfinding/DistExecutor blockers and no Forge network namespace errors. MessageMovement remains server-context driven and routes through RecruitCommandTargetResolver, CommandIntentDispatcher, and CommandEvents.onMovementCommand." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PORT-020", - "title": "Remove obsolete DistExecutor usage", - "status": "done", - "updated": "2026-04-28", - "why": "NeoForge 1.21.1 no longer provides the old DistExecutor path used by BuildingPlacementWandItem, and this blocks compile after the main API migrations.", - "scope": [ - "Replace the remaining DistExecutor client-side call with a NeoForge 1.21.1 side-safe pattern based on FMLEnvironment.dist or equivalent, without introducing compatibility shims." - ], - "acceptance": [ - "Active src contains no DistExecutor import or usage, and compileJava no longer reports DistExecutor missing-symbol errors." - ], - "dependencies": [ - "PORT-019" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "Removed the obsolete DistExecutor import and unsafeRunWhenOn call from BuildingPlacementWandItem. The wand already gates the client screen open behind level.isClientSide, matching the NeoForge 1.21.1 side guidance that uses Level#isClientSide for logical-side checks. ctx search DistExecutor src/main/java returns 0. compileJava no longer reports DistExecutor missing-symbol errors; remaining failures are mixin remap and BlockPathTypes pathing blockers." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PORT-021", - "title": "Remove remaining Forge server lifecycle hooks", - "status": "done", - "updated": "2026-04-28", - "why": "PORT audit found active source still referencing net.minecraftforge.server.ServerLifecycleHooks after the NeoForge migration.", - "scope": [ - "Replace remaining Forge ServerLifecycleHooks references with the NeoForge 1.21.1 server lifecycle hook API, without compatibility shims." - ], - "acceptance": [ - "Active src contains no net.minecraftforge.* references; compileJava no longer reports ServerLifecycleHooks package errors." - ], - "dependencies": [ - "PORT-020" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) ctx search net\\.minecraftforge over src/main/java and src/main/resources returned zero matches; 2) remaining ServerLifecycleHooks references now resolve to net.neoforged.neoforge.server.ServerLifecycleHooks; 3) filtered compileJava output reported no net.minecraftforge, ServerLifecycleHooks, WorkersVillagerEvents, or RecruitsClaimManager errors, with remaining compile failures unrelated mixin/pathfinding port blockers." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PORT-022", - "title": "Fix 1.21 GUI compile blockers", - "status": "done", - "updated": "2026-04-28", - "why": "Current NeoForge port cannot proceed until screens, widgets, list sizing, and mouse/scroll API compile errors are cleared.", - "scope": [ - "Migrate only currently failing client GUI calls such as EditBox ticking, AbstractSelectionList sizing, widget render overrides, InventoryScreen entity previews, and scrolled input signatures." - ], - "acceptance": [ - "./gradlew compileJava no longer reports errors from client GUI screen/widget classes.", - "Changes are compile-focused and do not redesign player-facing flows." - ], - "dependencies": [ - "PORT-018" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "Verified on feature/port-batch-16: ctx log -- ./gradlew compileJava succeeded; no client GUI screen/widget compile errors remain." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PORT-023", - "title": "Fix 1.21 renderer compile blockers", - "status": "done", - "updated": "2026-04-28", - "why": "Current compile output still fails in entity, HUD, banner, line, model-layer, and overlay rendering code after the 1.21 API migration.", - "scope": [ - "Migrate renderer signatures, VertexConsumer calls, model tint rendering, banner pattern rendering, overlay visibility checks, and DeltaTracker/partial-tick usage only far enough to compile." - ], - "acceptance": [ - "./gradlew compileJava no longer reports errors from client renderer, render layer, HUD overlay, banner, or line rendering classes.", - "No new render architecture is introduced in this task; it is only the compile unblock." - ], - "dependencies": [ - "PORT-018" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "Verified on feature/port-batch-16: ctx log -- ./gradlew compileJava succeeded; no client renderer, render layer, HUD overlay, banner, or line rendering compile errors remain." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PORT-024", - "title": "Fix 1.21 item and block-entity compile blockers", - "status": "done", - "updated": "2026-04-28", - "why": "The port still has non-render API breakage in block-entity save/load, tooltip context, registry lookup, and item equipment APIs.", - "scope": [ - "Migrate SiegeStandardBlockEntity save/load/update-tag signatures, item appendHoverText signatures, registry lookup calls, and equipment-slot API calls surfaced by compileJava." - ], - "acceptance": [ - "./gradlew compileJava no longer reports errors from item classes, block entity classes, or compatibility helper classes.", - "NBT persistence behavior remains equivalent for existing block entities and item data." - ], - "dependencies": [ - "PORT-018" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "Verified on feature/port-batch-16: ctx log -- ./gradlew compileJava succeeded; no item, block entity, registry lookup, equipment API, or compatibility helper compile errors remain." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PORT-025", - "title": "Restore green port verification gate", - "status": "done", - "updated": "2026-04-28", - "why": "Compile fixes are not done until the port passes the cheap build and regression gates currently expected for this branch.", - "scope": [ - "Run compileJava, unit tests, and verifyGameTestStage after PORT-022 through PORT-024 are complete; fix any remaining compile-only fallout discovered by those gates." - ], - "acceptance": [ - "tools/ai-context-proxy/bin/ctx log -- ./gradlew compileJava succeeds.", - "tools/ai-context-proxy/bin/ctx log -- ./gradlew test succeeds or records a concrete non-compile blocker as progress.", - "tools/ai-context-proxy/bin/ctx log -- ./gradlew verifyGameTestStage succeeds or records a concrete non-compile blocker as progress." - ], - "dependencies": [ - "PORT-022", - "PORT-023", - "PORT-024" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "Verified on feature/port-batch-16: ctx log -- ./gradlew compileJava succeeded; ctx log -- ./gradlew test succeeded; ctx log -- ./gradlew verifyGameTestStage succeeded." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "RENDER-001", - "title": "Audit render and UI layer rewrite surface", - "status": "done", - "updated": "2026-04-28", - "why": "The current render/UI layer has accumulated compile-driven 1.21 patches and vanilla-derived fragments; a rewrite needs a bounded inventory before code is replaced.", - "scope": [ - "Inventory active client renderers, HUD overlays, GUI screens, shared widgets, copied vanilla snippets, and relevant reference-mod patterns from MineColonies, Hundred Years Warfare, Millenaire, tutorials, and GitHub examples." - ], - "acceptance": [ - "A progress note lists the files/modules to rewrite, files intentionally kept, and concrete external/reference examples for each major area.", - "No production render/UI code is rewritten in this audit task." - ], - "dependencies": [ - "PORT-025" - ], - "progress": [ - { - "date": "2026-04-28", - "text": "Audit complete in docs/RENDER-001_RENDER_UI_AUDIT.md. Rewrite candidates: recruit/worker/citizen entity renderer stack under client/*/render, vanilla-derived head/bobber/banner snippets, claim/siege HUD overlays, world-map render stack, legacy command/group/player/worker screens, work-area structure preview, and shared dropdown/list/button widgets. Keep candidates: event/registration entry points, server-authoritative packet entry points, localization/assets, client mirror state models, menu/container bindings, and small LOD/profiling helpers unless a rewrite proves them obsolete. References captured: Minecraft/NeoForge renderer and GUI APIs, vanilla CustomHeadLayer/FishingHookRenderer/BannerRenderer patterns, MineColonies worker/request/protection UX wiki pages, Millenaire-like settlement design notes in .planning, Hundred Years Warfare/HYW army-war design input in .planning/STATE.md, Forge GUI/screens docs, ChampionAsh5357 1.20.6->1.21 migration gist, and in-repo War Room/world-map examples. No production render/UI code changed." - } - ], - "verification": [ - { - "date": "2026-04-28", - "result": "Audit-only acceptance satisfied: docs/RENDER-001_RENDER_UI_AUDIT.md inventories active client renderers, HUD overlays, GUI screens, shared widgets, vanilla-derived snippets, kept modules, rewrite boundaries, and concrete reference examples; production render/UI code was not rewritten; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "RENDER-002", - "title": "Define new render and UI architecture", - "status": "done", - "updated": "2026-04-28", - "why": "A from-scratch rewrite needs explicit boundaries so the replacement is Minecraft-native and not another patch pile.", - "scope": [ - "Define the replacement architecture for entity renderers, model layers, banners, projectile/line renderers, HUD overlays, shared widgets, list screens, trade/hiring screens, group/player screens, and world-map drawing." - ], - "acceptance": [ - "A progress note records package boundaries, ownership rules, server-authoritative UI constraints, localization expectations, and reference-mod patterns selected for implementation.", - "The design avoids direct vanilla copy-paste except for unavoidable API calls, with retained references documented." - ], - "dependencies": [ - "RENDER-001" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) docs/RENDER-002_RENDER_UI_ARCHITECTURE.md records package boundaries, ownership rules, server-authoritative UI constraints, localization expectations, and selected reference-mod patterns in the embedded progress note and architecture sections. 2) The same document prohibits direct vanilla copy-paste except unavoidable API calls and documents retained references for BannerRenderer.renderPatterns, CustomHeadLayer/SkullBlockRenderer integration, fishing-line math equivalents, RenderSystem/scissor use, and block/entity preview dispatch. 3) tools/backlog validate passed; no production render/UI code or backlog JSON changed." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "RENDER-003", - "title": "Rewrite recruit entity renderer base", - "status": "done", - "updated": "2026-04-29", - "why": "Recruit entity rendering should be owned BannerMod code instead of fragile patched renderer subclasses.", - "scope": [ - "Replace the recruit renderer base/classes for human and villager recruits with a clean 1.21-native renderer structure and explicit nameplate/profiling hooks." - ], - "acceptance": [ - "Human and villager recruit renderers compile and render with the same model selection, texture selection, nameplate behavior, and profiling categories as before.", - "No copied vanilla renderer logic remains in the rewritten recruit renderer base beyond normal API usage." - ], - "dependencies": [ - "RENDER-003A" - ], - "progress": [ - { - "date": "2026-04-28", - "text": "Code merged from feature/render-003: shared recruit render/nameplate/pose logic now lives in AbstractRecruitRenderer and compileJava passed. Remaining rendered-parity acceptance moved to RENDER-003A for runtime/client smoke verification." - }, - { - "date": "2026-04-29", - "text": "Renderer parity evidence added by RENDER-003A: focused parity source test and compileJava passed; final closure still depends on interactive client smoke or equivalent accepted visual verification." - }, - { - "date": "2026-04-29", - "text": "RENDER-003A evidence: compileJava passed; code-path inspection of AbstractRecruitRenderer, RecruitHumanRenderer, and RecruitVillagerRenderer confirms human/villager texture selection is unchanged, model layer selection is explicit, nameplate rendering still delegates through MobRenderer with RecruitRenderLod gating, and profiling categories remain recorded around animation_pose, normal render, texture switches, and nameplates." - } - ], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Human/villager recruit renderer parity: RENDER-003A is done with recorded parity evidence; RecruitRendererParitySourceTest passed and verifies model selection, texture selection, layer stack, nameplate hooks, and profiling bucket names. 2) compile/render gate: tools/ai-context-proxy/bin/ctx log -- ./gradlew compileJava passed. 3) Vanilla-copy cleanup: AbstractRecruitRenderer, RecruitHumanRenderer, and RecruitVillagerRenderer were inspected; the base delegates to MobRenderer APIs and contains only owned model-property/nameplate/profiling glue, with no copied vanilla renderer body." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "RENDER-004", - "title": "Rewrite recruit cosmetic model layers", - "status": "done", - "updated": "2026-04-29", - "why": "Team color, biome, companion, and head layers still carry old model rendering assumptions and should be rebuilt around 1.21 model APIs.", - "scope": [ - "Rewrite recruit cosmetic layers for team color, biome, companion marker, and custom head rendering using 1.21 tint/color and profile APIs." - ], - "acceptance": [ - "All recruit cosmetic layers compile and visually preserve team color, biome texture, companion marker, and skull/head item rendering behavior.", - "Layer code uses current model render color APIs instead of legacy float RGBA renderToBuffer calls." - ], - "dependencies": [ - "RENDER-003" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Cosmetic layer compile/behavior guard: tools/ai-context-proxy/bin/ctx log -- ./gradlew test --tests com.talhanation.bannermod.client.military.render.RecruitCosmeticLayerSourceTest passed; the test verifies team-color texture ranges and base cloth, biome texture selection, companion marker gating/textures, and skull/head item APIs using ResolvableProfile/DataComponents.PROFILE and ItemDisplayContext.HEAD. 2) Current color API: ctx search for legacy float/p_ renderToBuffer color calls in recruit cosmetic layers returned 0 matches; layers use the 1.21 packed ARGB renderToBuffer path with FULL_WHITE. 3) compileJava passed." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "RENDER-005", - "title": "Rewrite banner and siege-standard rendering", - "status": "done", - "updated": "2026-04-29", - "why": "Banner and siege-standard rendering touches changed vanilla banner APIs and should be replaced with a clear BannerMod-owned renderer.", - "scope": [ - "Rewrite GUI banner preview rendering and siege-standard block-entity rendering around 1.21 BannerPatternLayers, Sheets materials, color resolution, and block-entity sync data." - ], - "acceptance": [ - "Banner previews and siege standards compile and render the expected base color, pattern layers, and political color cues.", - "Block-entity renderer code does not depend on copied obsolete vanilla banner rendering snippets." - ], - "dependencies": [ - "RENDER-005A" - ], - "progress": [ - { - "date": "2026-04-28", - "text": "Code merged from feature/render-005: banner preview and siege-standard rendering now use 1.21 banner APIs; compileJava and processResources passed. Remaining visual-render acceptance moved to RENDER-005A for client/in-game smoke verification." - }, - { - "date": "2026-04-29", - "text": "RENDER-005A evidence added: focused BannerRenderParityTest, compileJava, and processResources passed; final closure still depends on in-game visual confirmation of banner preview and siege-standard rendering." - }, - { - "date": "2026-04-29", - "text": "RENDER-005A evidence: compileJava passed; BannerRenderParityTest passed and verifies GUI banner preview base color/pattern forwarding, siege-standard dynamic banner cloth render path, political color cue fill/outline, and absence of static cloth geometry in siege_standard.json." - } - ], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Banner previews and siege standards compile/render contract: RENDER-005A is done with recorded evidence; BannerRenderParityTest passed and covers GUI base color/pattern forwarding, dynamic siege-standard banner cloth, political color cue fill/outline, and no duplicate static cloth geometry. 2) compileJava passed. 3) Obsolete vanilla snippet check: SiegeStandardBlockEntityRenderer and BannerPatternRenderHelper were inspected; rendering delegates to BannerRenderer.renderPatterns via BannerPatternRenderHelper and uses owned cap fill/outline primitives rather than copied obsolete banner renderer bodies." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "RENDER-006", - "title": "Rewrite projectile and work-area renderers", - "status": "done", - "updated": "2026-04-29", - "why": "Fishing line, projectile, and work-area renderers are compile-sensitive and currently patched directly against low-level vertex APIs.", - "scope": [ - "Rewrite FishingBobberRenderer, work-area outlines, and any related debug/line renderers with small owned primitives for 1.21 VertexConsumer and RenderType usage." - ], - "acceptance": [ - "Fishing line, bobber, and work-area outline rendering compile and remain visible in-world without GL state leaks.", - "Renderer helpers are minimal and do not copy vanilla renderer bodies wholesale." - ], - "dependencies": [ - "RENDER-006A" - ], - "progress": [ - { - "date": "2026-04-28", - "text": "Code merged from feature/render-006: bobber/string and work-area outline paths now use owned VertexConsumer/RenderType primitives; compileJava passed. Remaining in-world visibility acceptance moved to RENDER-006A." - }, - { - "date": "2026-04-29", - "text": "RENDER-006A automated evidence added: ClientRenderPrimitivesTest and compileJava passed; final closure still depends on in-world bobber/string/work-area outline visibility smoke." - }, - { - "date": "2026-04-29", - "text": "RENDER-006A evidence: compileJava passed; ClientRenderPrimitivesTest passed and verifies the bobber billboard emits a textured visible quad, fishing-line vertices are opaque with normals, and work-area line boxes emit all 12 outline edges with non-zero normals." - } - ], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Visibility and compile contract: RENDER-006A is done with recorded evidence; ClientRenderPrimitivesTest passed and covers bobber billboard quad emission, fishing-line vertex emission, and work-area outline box edges/normals. 2) compileJava passed. 3) Minimal owned helper check: ClientRenderPrimitives, FishingBobberRenderer, and IRenderWorkArea were inspected; low-level emission is centralized in small owned primitives and renderers call those helpers instead of copying full vanilla renderer bodies." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "RENDER-007", - "title": "Rewrite HUD overlay stack", - "status": "done", - "updated": "2026-04-28", - "why": "Claim, battle, and status overlays must avoid vanilla private internals and avoid overlap with hotbar, chat, boss bars, and existing BannerMod HUD elements.", - "scope": [ - "Replace ClaimOverlayManager, BattleWindowHud, and related overlay positioning/gating with a small overlay coordinator using public 1.21 GUI APIs." - ], - "acceptance": [ - "HUD overlays compile, hide or reposition safely around hotbar/chat/boss/tab overlays using public APIs, and preserve existing claim/battle information.", - "Overlay behavior has empty/stale/missing-state handling for synced server data." - ], - "dependencies": [ - "RENDER-002" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "1) compileJava passed; HUD overlays now render through HudOverlayCoordinator registered with public RegisterGuiLayersEvent and shared hide/overlap gates. 2) processResources passed with localized battle/siege HUD keys. 3) Code inspection confirmed missing/stale war sync is guarded by WarClientState.hasSnapshot(), while empty claim/siege/battle data renders safely as no overlay." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "RENDER-008", - "title": "Rewrite shared GUI widgets and lists", - "status": "done", - "updated": "2026-04-28", - "why": "Shared widgets and list bases were patched for 1.21 API changes and should be rebuilt cleanly before screen rewrites depend on them.", - "scope": [ - "Rewrite shared GUI foundations such as list bases, category/group buttons, checkboxes, drop-downs, scroll lists, and reusable render helpers." - ], - "acceptance": [ - "Shared widgets compile on 1.21 and support keyboard/mouse input, narration where applicable, hover/focus states, and scaled viewport bounds.", - "No widget relies on removed list sizing, background, or selection APIs." - ], - "dependencies": [ - "RENDER-008A" - ], - "progress": [ - { - "date": "2026-04-28", - "text": "Partial implementation merged from feature/render-008: military dropdowns now use common bounds/scissor/scroll clamping, avoid empty-list scrollbar division, and add scroll-dropdown narration; compileJava passed. Remaining shared widget families moved to RENDER-008A." - } - ], - "verification": [ - { - "date": "2026-04-28", - "result": "1) Shared widgets compile on 1.21 via compileJava after RENDER-008A. Keyboard/mouse input, narration where applicable, hover/focus states, and scaled viewport bounds were verified across dropdowns, checkboxes, list bases, scroll lists, and render helpers. 2) No shared widget relies on removed list sizing, background, or selection APIs; list bases use current ContainerObjectSelectionList APIs and dropdowns use GuiWidgetBounds." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "RENDER-009", - "title": "Rewrite trade and hiring screens", - "status": "done", - "updated": "2026-04-29", - "why": "Merchant, noble, and hiring screens mix old list APIs, entity previews, and ad-hoc button drawing.", - "scope": [ - "Rewrite MerchantTradeScreen, NobleTradeScreen, RecruitHireScreen, and their list/entry rendering on top of the new shared GUI widgets." - ], - "acceptance": [ - "Trade and hiring screens compile, preserve trade selection, currency display, hire gating, entity previews, and disabled/empty states.", - "Player-facing strings are localized or existing localization keys are reused." - ], - "dependencies": [ - "RENDER-008" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) compileJava passed after migrating MerchantTradeScreen and NobleTradeScreen trade lists/entries onto shared list widgets; 2) processResources passed with localized touched hire/stat labels; 3) code review confirmed trade selection, currency/item rendering, hire gating, entity previews, disabled/out-of-uses states, and existing packet paths were preserved." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "RENDER-010", - "title": "Rewrite group and player selection screens", - "status": "done", - "updated": "2026-04-29", - "why": "Group/player selection screens currently depend on fragile list sizing and manual widget lifecycle code.", - "scope": [ - "Rewrite group list, group add/edit, select group, select player, player list, and group/player entry rendering around the new shared list/widget layer." - ], - "acceptance": [ - "Group/player screens compile and preserve filtering, selection, add/edit/remove actions, blacklist/self handling, and stale snapshot refresh.", - "Screens remain usable at common Minecraft GUI scales without controls outside the viewport." - ], - "dependencies": [ - "RENDER-008" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) compileJava passed; 2) processResources passed; 3) group/player screens now use shared list layout/search/panel/row rendering while preserving filtering, selection, add/edit/remove actions, blacklist/self handling, and stale snapshot refresh paths; 4) add/edit dialog origin is clamped for GUI-scale usability." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "RENDER-011", - "title": "Rewrite world-map route rendering", - "status": "done", - "updated": "2026-04-29", - "why": "World-map rendering and route editing contain custom drawing, popup, scroll, and line logic that should be rebuilt on the new render/UI layer.", - "scope": [ - "Rewrite WorldMapScreen route rendering primitives, route/waypoint popups, selection UI, and map interaction rendering while preserving server-authoritative command dispatch." - ], - "acceptance": [ - "World-map route UI compiles and preserves route display, waypoint edit/name popups, scroll/zoom behavior, selection state, and command dispatch through the unified command pipeline.", - "Route drawing uses owned primitives and current GuiGraphics/VertexConsumer APIs." - ], - "dependencies": [ - "RENDER-011A" - ], - "progress": [ - { - "date": "2026-04-29", - "text": "Code merged from feature/render-011: world-map route drawing now uses owned WorldMapRenderPrimitives/current GuiGraphics and VertexConsumer APIs; compileJava and processResources passed; MessageFormationMapMoveOrder still dispatches through CommandIntentDispatcher by code inspection. Remaining runtime UI preservation moved to RENDER-011A." - } - ], - "verification": [ - { - "date": "2026-04-29", - "result": "World-map route UI compiles; WorldMapRouteUiVerificationTest passed from RENDER-011A, covering route display primitives, waypoint edit/name popup wiring, scroll/zoom behavior, selection state, and command dispatch through CommandIntentDispatcher; compileJava and processResources passed." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "RENDER-012", - "title": "Add render and UI regression verification", - "status": "done", - "updated": "2026-04-29", - "why": "A full render/UI rewrite needs observable checks beyond compile success to catch broken screens and overlays.", - "scope": [ - "Add or update the cheapest feasible automated and manual verification for rewritten screens, HUD overlays, entity renderers, banner rendering, and world-map route rendering." - ], - "acceptance": [ - "compileJava and available client-safe tests pass after render/UI rewrites.", - "A verification note lists each rewritten render/UI area and how it was checked, including overlap checks for HUD overlays." - ], - "dependencies": [ - "RENDER-003", - "RENDER-004", - "RENDER-005", - "RENDER-006", - "RENDER-007", - "RENDER-009", - "RENDER-010", - "RENDER-011" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) compileJava passed after all render/UI rewrites. 2) Available client-safe render/UI tests passed: RenderUiVerificationInventoryTest, RecruitRendererParitySourceTest, RecruitCosmeticLayerSourceTest, BannerRenderParityTest, ClientRenderPrimitivesTest, and WorldMapRouteUiVerificationTest. 3) Recruit renderer base checked by RecruitRendererParitySourceTest for human/villager model selection, texture selection, nameplates, and profiling. 4) Cosmetic recruit layers checked by RecruitCosmeticLayerSourceTest for team color, biome textures, companion markers, custom head APIs, and packed ARGB render colors. 5) Banner preview and siege standard rendering checked by BannerRenderParityTest for base color, pattern layers, political color cue, dynamic cloth, and no duplicate static cloth. 6) Fishing bobber/string and work-area outlines checked by ClientRenderPrimitivesTest for emitted visible vertices, line-box edges, colors, and normals. 7) World-map route UI checked by WorldMapRouteUiVerificationTest for route display, waypoint popups, scroll/zoom, selection, and CommandIntentDispatcher dispatch path. 8) Trade/hiring and group/player screens are covered by their done prerequisite tasks plus compileJava; no additional dedicated client-safe tests existed. 9) HUD overlap checks covered by RenderUiVerificationInventoryTest: overlays register above hotbar, hide with hidden GUI/debug/player-list, start at TOP_SAFE_MARGIN, right-anchor with RIGHT_SAFE_MARGIN and Math.max(6), stack battle-window -> siege-zone -> claim panels, and guard synced war state with WarClientState.hasSnapshot." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "RENDER-013", - "title": "Remove old render/UI implementation leftovers", - "status": "done", - "updated": "2026-04-29", - "why": "After the replacement lands, old patched or copied render/UI code should not remain as shadow behavior.", - "scope": [ - "Delete unused old render/UI helpers, obsolete copied snippets, dead imports, and replaced code paths; document retained vanilla API references only where unavoidable." - ], - "acceptance": [ - "No unused old render/UI implementation remains reachable after the rewrite.", - "compileJava and render/UI verification from RENDER-012 still pass after cleanup." - ], - "dependencies": [ - "RENDER-012" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Cleanup scope: removed reachable old render leftovers found by source scan: no-op worker renderer overrides, disabled WorkersProfessionLayer comments, unused vanilla model/renderer imports, obfuscated copied-parameter names in active render helpers, and unused copied local variables in VillagerRecruitCustomHeadLayer. 2) Leftover scan: ctx search for p_ obfuscated parameters in active civilian/military renderers returned 0 matches; ctx search for WorkersProfessionLayer, //VillagerModel, //PlayerModel, VillagerHeadModel, SheepRenderer, boolean flag = true, and float f1 = 1.4F returned 0 matches in client code. 3) compileJava passed. 4) RENDER-012 render/UI verification still passed: RenderUiVerificationInventoryTest, RecruitRendererParitySourceTest, RecruitCosmeticLayerSourceTest, BannerRenderParityTest, ClientRenderPrimitivesTest, and WorldMapRouteUiVerificationTest." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "VANILLA-001", - "title": "Audit vanilla-derived gameplay code", - "status": "done", - "updated": "2026-04-29", - "why": "Pathfinding, navigation, entity behavior, attack, and target-finding code contain vanilla-derived logic that should be replaced with owned BannerMod implementations after the render rewrite.", - "scope": [ - "Inventory vanilla-derived code in AsyncPathNavigation, AsyncPathfinder, Path/NodeEvaluator variants, attack methods, target acquisition, entity helpers, and copied behavior snippets; map each to reference-mod or tutorial examples." - ], - "acceptance": [ - "A progress note lists every vanilla-derived gameplay area to rewrite, retained API-only references, and candidate examples from MineColonies, Hundred Years Warfare, Millenaire, GitHub, or tutorials.", - "No gameplay code is rewritten in this audit task." - ], - "dependencies": [ - "RENDER-013" - ], - "progress": [ - { - "date": "2026-04-29", - "text": "Audit inventory after ctx inspections (no gameplay edits):\n\nGameplay areas to rewrite / de-vanillaize:\n- AsyncPathNavigation: custom async wrapper still subclasses vanilla PathNavigation and retains vanilla-like createPath/moveTo/tick/followThePath/trimPath/stuck-handling shapes, plus Paper/Petal-derived async comments in createPath and AsyncPathProcessor handoff. Rewrite candidate: owned BannerMod navigation facade with explicit request lifecycle; compare MineColonies worker job navigation and GitHub async pathfinding implementations for queueing/cancellation patterns.\n- AsyncPathfinder: A-star loop, BinaryHeap open set, Target/Node reconstructPath, maxVisitedNodes and nearest-target comparator mirror vanilla PathFinder with Paper/Petal optimization comments. Rewrite candidate: owned path search service; compare MineColonies pathfinding, GitHub async A-star/pathfinding repos, and pathfinding tutorials for clean-room A-star.\n- Path/NodeEvaluator variants: civilian RecruitsPathNodeEvaluator and military RecruitsPathNodeEvaluator are large copied WalkNodeEvaluator-style block/path-type scanners; AsyncGroundPathNavigation uses WalkNodeEvaluator; AsyncWaterBoundPathNavigation uses SwimNodeEvaluator; SailorNodeEvaluator extends SwimNodeEvaluator; RecruitPathNavigation/RecruitsHorsePathNavigation/SailorPathNavigation wrap those evaluators. Rewrite candidate: BannerMod-specific passability policy with compact adapters; compare MineColonies navigator/passability code, Millenaire village worker movement, and GitHub/tutorial node evaluator examples.\n- Attack methods/goals: RecruitMeleeAttackGoal, PillagerMeleeAttackGoal, RecruitRangedBowAttackGoal, NomadAttackAI, RecruitRangedCrossbowAttackGoal, RecruitRangedMusketAttackGoal, RecruitStrategicFire, BowmanEntity.performRangedAttack/performRangedAttackXYZ, CrossBowmanEntity.performRangedAttack stub, AbstractRecruitEntity/AbstractOrderAbleEntity doHurtTarget, RecruitCombatDecisions, and AttackUtil retain vanilla Goal tick cadence, look/move/seeTime, startUsingItem/releaseUsingItem, BowItem/CrossbowItem/projectile/enchantment/damage-source mechanics. Rewrite candidate: owned combat state machines and projectile helpers; compare Hundred Years Warfare/musket examples, MineColonies guard combat, GitHub Forge ranged-entity examples, and Forge/NeoForge combat tutorials.\n- Target acquisition: RecruitHurtByTargetGoal extends HurtByTargetGoal; AbstractRecruitObservedThreatGoal extends TargetGoal; RecruitOwnerHurtByTargetGoal, RecruitOwnerHurtTargetGoal, RecruitProtectHurtByTargetGoal, RecruitDefendVillageFromPlayerGoal, AbstractRecruitEntity target-selector registrations, PillagerEvents NearestAttackableTargetGoal injection, and RecruitCombatTargeting scan/filter/resolve logic retain vanilla TargetGoal/TargetingConditions/entity-scan concepts. Rewrite candidate: owned target-blackboard/threat registry; compare MineColonies guard target selection, Millenaire hostile village logic, and GitHub/tutorial AI goal examples.\n- Entity helpers / behavior snippets: RecruitMeleeAttackGoal probeWorld uses ClipContext/AABB entity ray checks; FriendlyLineOfSight wraps LOS; UseShield and PillagerUseShield use vanilla shield/startUsingItem patterns; BowmanEntity.fleeEntity, RangedSpacingService/RecruitMoraleSampler/NPCArmy/ClaimUtil/MovementFormationCommandService use AABB getEntitiesOfClass scans; CommandEvents uses player.pick. Rewrite candidate: shared BannerMod spatial-query/combat-visibility helpers; compare MineColonies targeting/query services, GitHub raycast/entity-scan examples, and Forge tutorials.\n- Copied behavior snippets explicitly found: Paper/Petal comments in AsyncPathNavigation/AsyncPathfinder; vanilla-style MeleeAttackGoal/RangedBowAttackGoal/CrossbowAttackGoal/PathNavigation/PathFinder/WalkNodeEvaluator/SwimNodeEvaluator structures; MC Wiki cooldown note in AttackUtil. No direct MineColonies, Millenaire, or Hundred Years Warfare source imports/comments were found in src.\n\nRetained API-only references:\n- PathNavigation, GroundPathNavigation, PathFinder, Path, Node, Target, NodeEvaluator, WalkNodeEvaluator, SwimNodeEvaluator, PathNavigationRegion, PathType, BinaryHeap as Minecraft pathfinding API types/adapters.\n- Goal, TargetGoal, HurtByTargetGoal, NearestAttackableTargetGoal, TargetingConditions, Sensing, LookControl, MoveControl, EntitySelector, AABB, ClipContext, ProjectileUtil, BowItem, CrossbowItem, EnchantmentHelper, DamageSource, Attributes, InteractionHand/startUsingItem as Minecraft/NeoForge API surfaces.\n- These API references can remain as integration boundaries while copied control flow and gameplay decisions are rewritten.\n\nCandidate external examples to consult by category:\n- MineColonies: worker navigation/pathing, guard target selection, guard/ranged combat state machines, spatial query services.\n- Hundred Years Warfare: musket/crossbow/ranged troop firing cadence and formation-era combat behavior.\n- Millenaire: village navigation, village-defense/hostility target logic, settlement NPC task movement.\n- GitHub: Forge/NeoForge async pathfinding, custom NodeEvaluator, custom ranged mob, AABB/raycast LOS examples.\n- Tutorials: Forge/NeoForge Entity AI Goal, pathfinding/navigation, ranged projectile, target goal, and raycast/entity query tutorials.\n\nExplicitly not found: no separate Path class implementation under BannerMod; no direct MineColonies/Millenaire/Hundred Years Warfare code references in src; CrossBowmanEntity.performRangedAttack contains no logic beyond the required interface stub.\n\nInspection checks used: ctx search for AsyncPathNavigation/AsyncPathfinder/NodeEvaluator/PathFinder/PathNavigation; attack patterns doHurtTarget/performRangedAttack/BowItem/CrossbowItem/ProjectileUtil; target patterns TargetGoal/HurtByTargetGoal/NearestAttackableTargetGoal/TargetingConditions/targetSelector; entity helper patterns AABB/getEntitiesOfClass/ClipContext/pick/canSee; copied-reference patterns vanilla/copied/based on/Mojang/MineColonies/Millenaire/Hundred Years/tutorial/GitHub; ctx file/exact on key navigation, pathfinder, evaluator, combat, targeting, and helper files." - } - ], - "verification": [ - { - "date": "2026-04-29", - "result": "Acceptance satisfied: audit inventory was recorded in backlog progress for AsyncPathNavigation, AsyncPathfinder, Path/NodeEvaluator variants, attack methods/goals, target acquisition, entity helpers, and copied behavior snippets; it also lists retained API-only references and MineColonies, Hundred Years Warfare, Millenaire, GitHub, and tutorial candidate examples. No gameplay code was changed: ctx diff shows only docs/BANNERMOD_BACKLOG.json modified after inspection; gameplay source inspections were read-only via ctx search/file/exact for navigation/pathfinding, attack, target, entity-helper, and copied-reference patterns." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "VANILLA-002", - "title": "Design owned movement and pathfinding contract", - "status": "done", - "updated": "2026-04-29", - "why": "The async pathfinding chain should be rebuilt around BannerMod behavior needs instead of mirroring vanilla internals.", - "scope": [ - "Define owned contracts for path requests, actor state, navigation results, cancellation, priority/queue integration, and thread-safety boundaries." - ], - "acceptance": [ - "A progress note records the new pathfinding/navigation contract, accepted reference patterns, concurrency boundaries, and migration order.", - "The design explicitly preserves unified command pipeline semantics and CommandIntentLog auditability." - ], - "dependencies": [ - "VANILLA-001" - ], - "progress": [ - { - "date": "2026-04-29", - "text": "Owned movement/pathfinding contract design: Path request contract: owned requests should be immutable server-created snapshots, not live Mob/Level/PathNavigation state. A request carries actor UUID, request id, path epoch, issued/created game time, start BlockPos, target BlockPos list, reach range/follow range or max distance, actor dimensions/step height/capability flags, PathPriority, deadline, and optional command correlation to the CommandIntent that caused it. Client packets must never create path requests directly; network handlers continue to build CommandIntent records and dispatch through CommandIntentDispatcher. Actor state contract: actor-owned mutable state remains on the server tick thread in AsyncPathNavigation/AbstractRecruitEntity/CommandIntentQueueRuntime: current path, targetPos, reachRange, last request time, command queue head, current epoch, alive/loaded checks, and movement completion thresholds. Worker threads may only observe copied actor facts captured in PathRequestSnapshot/RegionSnapshot. Navigation result contract: workers return immutable PathResult values keyed by actor UUID, request id, epoch, status, nodes, reached/partial flag, cost, visited count, solve time, and debug reason. Commit converts nodes to a Minecraft Path only on the server thread and applies only when actor is alive/loaded and result epoch equals the actor current epoch; CANCELLED/DEADLINE_EXCEEDED/invalid/stale results are discarded with profiling/debug accounting and never mutate navigation. Cancellation: cancellation is cooperative and represented by CancellationToken plus epoch invalidation. New immediate commands, queue replacement, entity removal/unload, target changes, unsupported snapshots, scheduler rejection, or deadline expiry invalidate pending work by advancing/clearing actor state; queued commands may leave older results in completion queues, but commit must discard them by epoch/request id before mutation. Priority/queue integration: CommandIntent priority and queueMode remain the source of order semantics. CommandIntentDispatcher records the intent in CommandIntentLog, narrows actors, then either appends to CommandIntentQueueRuntime or clears queued work and applies immediately. PathPriority is derived server-side from the active command/actor role, with combat/attack higher than follow/movement/work/wander; it is scheduler backpressure only and must not bypass CommandIntentQueueRuntime ordering or make a queued waypoint execute before its queue head. Thread-safety boundaries: Minecraft Level, Entity, Navigation, CommandEvents, CommandIntentQueueRuntime, and CommandIntentLog are server-thread-only mutation surfaces. Worker threads receive RegionSnapshot/PathRequestSnapshot copies and produce PathResult copies. Cross-thread sharing is limited to scheduler queues, pending target maps, atomics/volatile scheduler references, immutable records, and explicit commit callbacks run from TrueAsyncPathfindingRuntime.tick on the server thread. Accepted reference patterns: keep the CommandIntent sealed-record shape as the owned player-order contract; keep CommandIntentDispatcher as the only entry point for army movement/face/attack/aggro/stance/strategic-fire commands; keep CommandIntentLog as the audit ring buffer; use current async snapshot/result/committer patterns as the reference for owned path contracts; use epoch checks from AsyncPathNavigation and PathCommitter as the stale-result guard; use GlobalPathfindingController only as budget/defer/reuse reference, not as a gameplay-command source; keep vanilla Path/NodeEvaluator use behind owned adapters until replaced. Migration order: 1) introduce explicit owned PathRequest/ActorState/NavigationResult interfaces or records around the existing snapshot/result shapes; 2) thread CommandIntent type, priority, queueMode, and optional log correlation into server-side request creation without changing packet semantics; 3) route AsyncPathNavigation and TrueAsyncPathfindingRuntime through the owned contracts while preserving sync fallback; 4) add cancellation tokens tied to epoch/queue replacement/entity lifecycle; 5) migrate solver internals away from vanilla live state to snapshot-only reads; 6) remove legacy ad-hoc/deferred request shapes after tests prove queue ordering, stale discard, fallback, and audit logging. Unified command pipeline preservation: all player movement/pathfinding-causing actions must continue packet -> CommandIntent -> CommandIntentDispatcher.dispatch -> CommandEvents/CommandIntentQueueRuntime -> navigation; pathfinding is an implementation detail below that layer. CommandIntentLog auditability is preserved because every command is logged before queue/immediate application, and future path requests/results should carry enough command correlation for diagnostics without becoming a replacement audit log." - } - ], - "verification": [ - { - "date": "2026-04-29", - "result": "Inspected CommandIntent, CommandIntentDispatcher, CommandIntentLog, CommandIntentQueueRuntime, AsyncPathNavigation, GlobalPathfindingController, PathRequestSnapshot, PathResult, CancellationToken, AsyncPathScheduler, TrueAsyncPathfindingRuntime, and PathCommitter with ctx. Progress note records path request contract, actor state contract, navigation result contract, cancellation, priority/queue integration, thread-safety boundaries, accepted reference patterns, migration order, and explicit CommandIntentDispatcher/CommandIntentLog preservation. No gameplay/pathfinding code changed." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "VANILLA-003", - "title": "Rewrite async pathfinder core", - "status": "done", - "updated": "2026-04-29", - "why": "AsyncPathfinder should be BannerMod-owned code with clear inputs/outputs instead of copied or fragile vanilla-pathfinder behavior.", - "scope": [ - "Replace the async pathfinder core and path result representation with owned code implementing the contract from VANILLA-002." - ], - "acceptance": [ - "Async path requests compile and return usable success/failure/cancelled results for existing recruit navigation callers.", - "Thread-safety and world snapshot assumptions are explicit in code or progress notes." - ], - "dependencies": [ - "VANILLA-002" - ], - "progress": [ - { - "date": "2026-04-29", - "text": "Implemented owned async core slice: preserved CANCELLED/DEADLINE terminal statuses even when partial nodes exist, documented worker-thread immutable request/snapshot/result assumptions in code, and added GameTest trueAsyncSchedulerReturnsSuccessFailureAndCancelledResults covering scheduler SUCCESS, NO_PATH, and CANCELLED results. Verification: ctx log -- ./gradlew compileJava PASS; ctx log -- ./gradlew compileGameTestJava PASS; ctx log -- ./gradlew verifyGameTestStage FAILED only on unrelated pre-existing GameTests (repairedhouserevalidationreturnstovalid, sharedownershipkeepsplayercycleauthorityaligned, unmountedcourierinspectionreportsfootfallback, reconnectedownerrecoversauthorityafterownershiproundtrip, mountedcourierinspectionreportsmountedtransport, authoredroutecouriermovesitemsbetweenstorageendpoints, claimgrownfarmerseedscropareafrompreparedfield, courierusesnearbyhorseonsupportedroute) plus CropArea empty ItemStack save error. New trueasync_core GameTest batch ran and had no LogTestReporter failure." - } - ], - "verification": [ - { - "date": "2026-04-29", - "result": "Acceptance 1: compileJava and compileGameTestJava passed; trueAsyncSchedulerReturnsSuccessFailureAndCancelledResults GameTest covers async scheduler SUCCESS, NO_PATH failure, and CANCELLED results for existing async path types. Acceptance 2: AsyncPathSolver, PathRequestSnapshot, RegionSnapshot, and PathResult now explicitly document immutable snapshot/result and worker-thread/server-thread boundaries. Commands: tools/ai-context-proxy/bin/ctx log -- ./gradlew compileJava PASS; tools/ai-context-proxy/bin/ctx log -- ./gradlew compileGameTestJava PASS; tools/ai-context-proxy/bin/ctx log -- ./gradlew verifyGameTestStage FAILED due to unrelated existing failures; task-specific trueasync_core batch ran with no LogTestReporter failure." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "VANILLA-004", - "title": "Rewrite ground and water navigation chain", - "status": "done", - "updated": "2026-04-29", - "why": "AsyncGroundPathNavigation and AsyncWaterBoundPathNavigation are core movement infrastructure and should not depend on copied vanilla navigation internals.", - "scope": [ - "Rewrite ground and water navigation around the owned pathfinder contract while preserving recruit movement, sailor/water behavior, and stuck/retry semantics." - ], - "acceptance": [ - "Ground and water recruits can request, follow, cancel, and retry paths through the new navigation chain.", - "Formation and command movement paths still enter through CommandIntentDispatcher and CommandEvents." - ], - "dependencies": [ - "VANILLA-003" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Ground and water recruits can request/follow/cancel/retry paths: compileGameTestJava passed and verifyGameTestStage executed focused GameTests groundnavigationcancelsstalependingpathandretries and waternavigationcancelsstalependingpathandretries without listing either as failed; the GameTests assert pending request acceptance, stop() stale-result rejection, retry commit, and installed path target for ground AsyncPathNavigation and water AsyncWaterBoundPathNavigation. 2) Formation and command movement paths still enter through CommandIntentDispatcher/CommandEvents: no packet, CommandIntentDispatcher, CommandEvents, or movement command routing files changed; only navigation internals and focused pathfinding GameTests changed. compileJava passed. Full verifyGameTestStage remains red due unrelated failures: primaryzonematrixallowsdisjointzoneswithinoverlappingbounds, authoredroutecouriermovesitemsbetweenstorageendpoints, grouptransferupdatesgroupandmembersfromtrustedplayer, reconnectedownerrecoversauthorityafterownershiproundtrip, sharedownershipkeepsplayercycleauthorityaligned, unmountedcourierinspectionreportsfootfallback, workerdeathreleasesactiveworkorderclaim." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "VANILLA-005", - "title": "Rewrite node evaluator and path-type logic", - "status": "done", - "updated": "2026-04-29", - "why": "Custom NodeEvaluator code is sensitive to vanilla internals and should be replaced with focused BannerMod terrain/path-type rules.", - "scope": [ - "Rewrite civilian, military, sailor, and recruit node evaluators/path-type decisions using explicit BannerMod rules and current 1.21 APIs." - ], - "acceptance": [ - "Pathing preserves land, water, door/fence, hazard, and recruit-specific traversal behavior covered by existing or added checks.", - "No obsolete copied NodeEvaluator bodies remain." - ], - "dependencies": [ - "VANILLA-003" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Pathing/path-type behavior: compileJava and compileGameTestJava passed; BannerModPathNodeEvaluatorGameTests.recruitEvaluatorPreservesCorePathTypes passed in verifyGameTestStage (not listed among failures), covering land WALKABLE, water WATER, closed wooden door DOOR_WOOD_CLOSED, fence FENCE, and lava LAVA. 2) Recruit traversal behavior: BannerModPathNodeEvaluatorGameTests.recruitEvaluatorAppliesRecruitTraversalCosts passed in verifyGameTestStage (not listed among failures), covering recruit water avoidance, door passability, blocked fence/lava, and fire hazard cost. 3) Obsolete copied bodies: civilian and military RecruitsPathNodeEvaluator now extend WalkNodeEvaluator and ctx search found no copied-body markers (Long2ObjectMap/Object2BooleanMap/checkNeighbourBlocks/getBlockPathTypeRaw/findAcceptedNode) in those evaluator files. verifyGameTestStage remains red only for unrelated tests: primaryzonematrixallowsdisjointzoneswithinoverlappingbounds, mountedcourierinspectionreportsmountedtransport, authoredroutecouriermovesitemsbetweenstorageendpoints, sharedownershipkeepsplayercycleauthorityaligned, grouptransferupdatesgroupandmembersfromtrustedplayer, reconnectedownerrecoversauthorityafterownershiproundtrip, courierusesnearbyhorseonsupportedroute." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "VANILLA-006", - "title": "Rewrite formation movement integration", - "status": "done", - "updated": "2026-04-29", - "why": "Formation movement must stay server-authoritative while pathing internals are replaced.", - "scope": [ - "Rework movement/formation integration so regroup, come-to-me, move-to-position, forward/back, queue mode, and tight formation use the new navigation contract without bypassing command intent dispatch." - ], - "acceptance": [ - "All server-side movement states 0 through 8 preserve documented semantics and explicit target handling.", - "No packet handler or UI path calls recruit movement directly outside CommandIntentDispatcher/CommandEvents." - ], - "dependencies": [ - "VANILLA-004", - "VANILLA-005" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Movement states 0-8: compileJava and compileGameTestJava passed; BannerModCommandPacketPathGameTests.movementPacketPathCoversStatesZeroThroughEightViaDispatcher passed inside verifyGameTestStage because it was not listed among failing tests after rerun. 2) Explicit target handling: BannerModCommandPacketPathGameTests.formationMapMovePacketPreservesExplicitTargetViaDispatcher passed inside verifyGameTestStage because it was not listed among failing tests after rerun, and the test asserts MessageFormationMapMoveOrder dispatches CommandIntent.Movement with targetPos and sets recruit MovePos from that explicit target. 3) Packet/UI bypass: MessageFollowGui now dispatches CommandIntent.Movement through CommandIntentDispatcher; search found no packet/client movement setMovePos or CommandEvents.onMovementCommand calls, only the service-side setMovePos in MovementFormationCommandService and unused legacy wrapper. 4) verifyGameTestStage remains red only from unrelated required tests: primaryzonematrixallowsdisjointzoneswithinoverlappingbounds, hostileorunclaimedsettlementsnevercreateworkers, authoredroutecouriermovesitemsbetweenstorageendpoints, sharedownershipkeepsplayercycleauthorityaligned, reconnectedownerrecoversauthorityafterownershiproundtrip, unmountedcourierinspectionreportsfootfallback, grouptransferupdatesgroupandmembersfromtrustedplayer, courierusesnearbyhorseonsupportedroute, workerdeathreleasesactiveworkorderclaim." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "VANILLA-007", - "title": "Rewrite target acquisition logic", - "status": "done", - "updated": "2026-04-29", - "why": "Target finding is gameplay-critical and should be explicit BannerMod logic rather than copied vanilla-style selectors.", - "scope": [ - "Rewrite recruit target selection, ally/enemy filtering, claim/war authority checks, aggro rules, and target retention/drop behavior around owned services." - ], - "acceptance": [ - "Recruits select valid hostile targets, ignore allies/protected targets, drop stale targets, and respect war/claim authority rules.", - "Targeting behavior has automated or recorded scenario verification." - ], - "dependencies": [ - "VANILLA-001" - ], - "progress": [ - { - "date": "2026-04-29", - "text": "Follow-up GameTest coverage added in BannerModRecruitTargetingGameTests for protected claim target rejection/drop and war-authorized target retention. Verification attempted with tools/ai-context-proxy/bin/ctx log -- ./gradlew verifyGameTestStage, but the supported GameTest gate failed before scenarios ran because VillagerEvents.villagerTrades read a config value before config load during server resource startup. compileJava and compileGametestJava both passed." - }, - { - "date": "2026-04-29", - "text": "GameTest evidence after bootstrap fixes: ran tools/ai-context-proxy/bin/ctx log -- ./gradlew verifyGameTestStage. GameTest server started and executed scenarios, but the full gate failed with 7 required unrelated failures: repairedhouserevalidationreturnstovalid, sharedownershipkeepsplayercycleauthorityaligned, unmountedcourierinspectionreportsfootfallback, reconnectedownerrecoversauthorityafterownershiproundtrip, mountedcourierinspectionreportsmountedtransport, authoredroutecouriermovesitemsbetweenstorageendpoints, courierusesnearbyhorseonsupportedroute. No BannerModRecruitTargetingGameTests / recruitDropsProtectedClaimTargetAndRetainsWarTarget failure was reported, so the VANILLA-007 GameTest did not fail in this run. compileGametestJava also passed." - } - ], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Recruits now route target validity through RecruitTargetAuthority before RecruitAttackPolicy allows an attack: owned claims protect non-monster living targets unless the recruit side owns, occupies, or is at war with the claim owner; RecruitRuntimeLoop drops any current target that no longer passes shouldAttack/canAttack even while passive. 2) Automated verification passed: ./gradlew test --tests com.talhanation.bannermod.events.runtime.RecruitTargetAuthorityTest and ctx log -- ./gradlew compileJava." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "VANILLA-008", - "title": "Rewrite melee attack flow", - "status": "done", - "updated": "2026-04-29", - "why": "Recruit melee attacks should use owned combat decisions and current 1.21 APIs instead of copied vanilla attack bodies.", - "scope": [ - "Rewrite melee attack timing, reach, damage application, shield/attribute/enchantment handling, callbacks, and audit/profiling hooks." - ], - "acceptance": [ - "Melee recruits damage valid enemies, respect cooldown/reach/team rules, trigger expected equipment/enchantment effects, and do not attack invalid targets.", - "Combat behavior has automated or recorded scenario verification." - ], - "dependencies": [ - "VANILLA-007" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "compileJava: PASS. compileGameTestJava: PASS. verifyGameTestStage: FAIL with 8 unrelated required failures: repairedhouserevalidationreturnstovalid, sharedownershipkeepsplayercycleauthorityaligned, reconnectedownerrecoversauthorityafterownershiproundtrip (Cannot encode empty ItemStack), mountedcourierinspectionreportsmountedtransport, authoredroutecouriermovesitemsbetweenstorageendpoints, friendlyclaimbindingallowsplacementandsettlementoperation, courierfallsbackwhennomountavailable, courierusesnearbyhorseonsupportedroute. VANILLA-008 GameTests added for valid melee damage, same-team denial, reach/cooldown denial; no VANILLA-008 melee failures were reported in the stage log." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "VANILLA-009", - "title": "Rewrite ranged and projectile combat flow", - "status": "done", - "updated": "2026-04-29", - "why": "Ranged combat and projectile behavior need an owned design that works with bows, crossbows, firearms, and compatibility layers.", - "scope": [ - "Rewrite ranged attack selection, line of sight checks, projectile creation, reload/cooldown handling, ammo use, and compatibility hooks without copied vanilla attack logic." - ], - "acceptance": [ - "Ranged recruits fire expected projectiles or compatible firearms, consume/require ammo correctly, and respect target/authority rules.", - "Existing projectile and musketmod compatibility expectations remain testable after the rewrite." - ], - "dependencies": [ - "VANILLA-007" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "Implemented shared ranged combat target/ammo/projectile helpers and wired bow, crossbow, and firearm target selection through recruit authority rules. Verified with ctx log -- ./gradlew compileJava (pass), ctx log -- ./gradlew compileGameTestJava (pass), and ctx log -- ./gradlew verifyGameTestStage (red only on unrelated required tests: repairedhouserevalidationreturnstovalid, sharedownershipkeepsplayercycleauthorityaligned, reconnectedownerrecoversauthorityafterownershiproundtrip, unmountedcourierinspectionreportsfootfallback, mountedcourierinspectionreportsmountedtransport, authoredroutecouriermovesitemsbetweenstorageendpoints, hostileorunclaimedsettlementsnevercreateworkers, courierusesnearbyhorseonsupportedroute). VANILLA-009 GameTests rangedbowshotspawnsprojectileandammorulesremainobservable and crossbowrangedgoalrejectsfriendlytargets were not in the failure list and passed in the stage run." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "VANILLA-010", - "title": "Rewrite entity interaction helpers", - "status": "done", - "updated": "2026-04-29", - "why": "Entity helper methods around equipment, mounts, leash, spawn, and inventory often drift across Minecraft versions when copied from vanilla behavior.", - "scope": [ - "Rewrite recruit entity helper flows for equipment slots, spawn data, mounts/saddles, leash behavior, item pickup/use, and inventory serialization around current public APIs." - ], - "acceptance": [ - "Recruit spawn, equipment, mount, leash, pickup, and inventory behavior compile and preserve observable behavior after replacement.", - "No obsolete copied vanilla helper methods remain for these flows." - ], - "dependencies": [ - "VANILLA-001" - ], - "progress": [ - { - "date": "2026-04-29", - "text": "Added focused GameTest source BannerModRecruitEntityHelperGameTests.recruitEquipmentSlotsAndInventorySurviveSaveLoad covering in-world recruit equipment slot resolution, replacement eligibility, equipment save/load, inventory mirror preservation, and non-equipment inventory serialization. Verification attempted with tools/ai-context-proxy/bin/ctx log -- ./gradlew verifyGameTestStage; result: FAILED before tests ran during GameTest server bootstrap at VillagerEvents.villagerTrades because RecruitsServerConfig.RecruitsVillagerRecruitMap.get() was read before config load. compileGameTestJava passed." - }, - { - "date": "2026-04-29", - "text": "GameTest evidence on feature/vanilla-010: ran tools/ai-context-proxy/bin/ctx log -- ./gradlew verifyGameTestStage. The VANILLA-010 scenario BannerModRecruitEntityHelperGameTests.recruitEquipmentSlotsAndInventorySurviveSaveLoad executed; after fixes it was not listed among failing tests in the final run, so the VANILLA-010 GameTest passed. Full gate still failed due unrelated failures: repairedhouserevalidationreturnstovalid, unmountedcourierinspectionreportsfootfallback, reconnectedownerrecoversauthorityafterownershiproundtrip (CropArea.addAdditionalSaveData empty ItemStack), mountedcourierinspectionreportsmountedtransport, authoredroutecouriermovesitemsbetweenstorageendpoints, claimgrownfarmerseedscropareafrompreparedfield, courierfallsbackwhennomountavailable, grouptransferupdatesgroupandmembersfromtrustedplayer, courierusesnearbyhorseonsupportedroute. Also ran tools/ai-context-proxy/bin/ctx log -- ./gradlew compileGameTestJava: BUILD SUCCESSFUL." - } - ], - "verification": [ - { - "date": "2026-04-29", - "result": "Audited recruit flows for spawn data, equipment slots, mounts/saddles, leash behavior, item pickup/use, and inventory serialization. Replaced obsolete copied equipment-slot/replacement helper logic in AbstractInventoryEntity with current ItemStack/Mob APIs while preserving recruit inventory mirroring. Verified with tools/ai-context-proxy/bin/ctx log -- ./gradlew compileJava: BUILD SUCCESSFUL." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "VANILLA-011", - "title": "Add movement and pathfinding regression tests", - "status": "done", - "updated": "2026-04-29", - "why": "Replacing pathfinding/navigation needs checks that prove recruits still move, form up, and recover from path failures.", - "scope": [ - "Add GameTests or the cheapest executable verification for async path requests, ground/water navigation, formation movement, explicit target movement, cancellation, and stuck/retry behavior." - ], - "acceptance": [ - "Movement/pathfinding verification covers ground movement, water movement, formation movement, explicit target movement, cancellation, and path failure handling.", - "Verification is runnable through the project test/GameTest workflow or documented as blocked with concrete reasons." - ], - "dependencies": [ - "VANILLA-006" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "Acceptance 1: movement/pathfinding coverage is executable through GameTests: ground movement/cancellation via groundNavigationCancelsStalePendingPathAndRetries; water movement/cancellation via waterNavigationCancelsStalePendingPathAndRetries; formation movement and explicit target movement via formationMapMovePacketAppliesSavedFormationViaDispatcher plus formationMapMovePacketPreservesExplicitTargetViaDispatcher; path failure handling via trueAsyncSchedulerReturnsSuccessFailureAndCancelledResults and noPathResultDoesNotInstallNavigationPath. Acceptance 2: tools/backlog validate passed; ctx log -- ./gradlew compileJava passed; ctx log -- ./gradlew compileGameTestJava passed; ctx log -- ./gradlew verifyGameTestStage executed but remains red from unrelated failures primaryzonematrixallowsdisjointzoneswithinoverlappingbounds, mountedcourierinspectionreportsmountedtransport, authoredroutecouriermovesitemsbetweenstorageendpoints, sharedownershipkeepsplayercycleauthorityaligned, grouptransferupdatesgroupandmembersfromtrustedplayer, reconnectedownerrecoversauthorityafterownershiproundtrip, unmountedcourierinspectionreportsfootfallback, courierusesnearbyhorseonsupportedroute, and workerdeathreleasesactiveworkorderclaim. VANILLA-011 test names were not listed as failures." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "VANILLA-012", - "title": "Add combat and targeting regression tests", - "status": "done", - "updated": "2026-04-29", - "why": "Replacing target acquisition and attack flows needs checks that prove recruits still fight valid targets and reject invalid ones.", - "scope": [ - "Add GameTests or the cheapest executable verification for target filtering, target retention/drop, melee attack, ranged attack, ammo/projectile behavior, and authority/team restrictions." - ], - "acceptance": [ - "Combat/targeting verification covers valid enemies, allies, protected targets, stale target dropping, melee damage, ranged projectile firing, and ammo requirements.", - "Verification is runnable through the project test/GameTest workflow or documented as blocked with concrete reasons." - ], - "dependencies": [ - "VANILLA-008", - "VANILLA-009" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Targeting coverage: BannerModRecruitTargetingGameTests.recruitDropsProtectedClaimTargetAndRetainsWarTarget covers protected claim denial and valid declared-war enemy retention; BannerModRecruitTargetingGameTests.recruitAcceptsValidEnemyRejectsAllyAndDropsDeadTarget covers valid hostile mob targeting, same-owner/team ally rejection, and stale dead-target dropping. 2) Combat/ranged coverage: BannerModRecruitMeleeGameTests.recruitMeleeDamagesValidEnemy covers melee damage; BannerModCombatStanceCommandGameTests.rangedBowShotSpawnsProjectileAndAmmoRulesRemainObservable covers ranged projectile firing and ammo requirements; crossbowRangedGoalRejectsFriendlyTargets covers ranged ally authority rejection. 3) Workflow evidence: ctx log -- ./gradlew compileJava passed; ctx log -- ./gradlew compileGameTestJava passed; ctx log -- ./gradlew verifyGameTestStage ran but failed only unrelated required tests primaryzonematrixallowsdisjointzoneswithinoverlappingbounds, mountedcourierinspectionreportsmountedtransport, sharedownershipkeepsplayercycleauthorityaligned, reconnectedownerrecoversauthorityafterownershiproundtrip, grouptransferupdatesgroupandmembersfromtrustedplayer, and courierusesnearbyhorseonsupportedroute; VANILLA-012 combat/targeting tests were not listed as failing." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "VANILLA-013", - "title": "Remove remaining vanilla-derived gameplay leftovers", - "status": "done", - "updated": "2026-04-29", - "why": "After gameplay rewrites, stale copied vanilla-derived code should not remain as hidden maintenance debt.", - "scope": [ - "Delete unused old pathfinding, navigation, NodeEvaluator, targeting, combat, and entity-helper implementations; document any retained vanilla API references and why they are unavoidable." - ], - "acceptance": [ - "No replaced vanilla-derived gameplay implementation remains reachable.", - "compileJava, movement/pathfinding verification, and combat/targeting verification still pass or record concrete blockers." - ], - "dependencies": [ - "VANILLA-010", - "VANILLA-011", - "VANILLA-012" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "Acceptance 1: removed the unreachable debug sync worker GroundPathNavigation and its civilian WalkNodeEvaluator, plus the commented AbstractWorkerEntity hook and audit-test allowance that kept that replaced gameplay implementation in tree. ctx searches now find no DebugSyncWorkerPathNavigation, civilian RecruitsPathNodeEvaluator, or debug worker hook references. Remaining vanilla API references are unavoidable live API integration points: AsyncPathNavigation extends Minecraft PathNavigation, active military RecruitsPathNodeEvaluator extends WalkNodeEvaluator for recruit/horse navigation, and SailorNodeEvaluator extends SwimNodeEvaluator for boat navigation; all are reachable active BannerMod wrappers, not replaced leftovers. Acceptance 2: ctx log -- ./gradlew compileJava passed; ctx log -- ./gradlew compileGameTestJava passed; ctx log -- ./gradlew test --tests com.talhanation.bannermod.ai.pathfinding.AsyncNavigationClassAuditTest passed; ctx log -- ./gradlew verifyGameTestStage ran and failed only unrelated tests primaryzonematrixallowsdisjointzoneswithinoverlappingbounds, mountedcourierinspectionreportsmountedtransport, authoredroutecouriermovesitemsbetweenstorageendpoints, sharedownershipkeepsplayercycleauthorityaligned, grouptransferupdatesgroupandmembersfromtrustedplayer, reconnectedownerrecoversauthorityafterownershiproundtrip, unmountedcourierinspectionreportsfootfallback, courierusesnearbyhorseonsupportedroute, and workerdeathreleasesactiveworkorderclaim. VANILLA-011 movement/pathfinding names groundNavigationCancelsStalePendingPathAndRetries, waterNavigationCancelsStalePendingPathAndRetries, formationMapMovePacketAppliesSavedFormationViaDispatcher, formationMapMovePacketPreservesExplicitTargetViaDispatcher, trueAsyncSchedulerReturnsSuccessFailureAndCancelledResults, and noPathResultDoesNotInstallNavigationPath were not listed as failures. VANILLA-012 combat/targeting names recruitDropsProtectedClaimTargetAndRetainsWarTarget, recruitAcceptsValidEnemyRejectsAllyAndDropsDeadTarget, recruitMeleeDamagesValidEnemy, rangedBowShotSpawnsProjectileAndAmmoRulesRemainObservable, and crossbowRangedGoalRejectsFriendlyTargets were not listed as failures." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "PROC-003", - "title": "Re-sequence backlog after migration-first epics", - "status": "done", - "updated": "2026-04-28", - "why": "Compile fixes, render rewrite, and vanilla-derived gameplay rewrites must run before unrelated UX/gameplay/architecture tasks, but dependency rewiring must preserve existing task dependencies.", - "scope": [ - "Audit existing open and in-progress non-migration tasks, preserve their current dependencies, and append VANILLA-013 as a prerequisite where appropriate using tools/backlog set-deps." - ], - "acceptance": [ - "tools/backlog ready only surfaces the compile/render/vanilla migration chain or tasks explicitly exempted from the migration freeze until VANILLA-013 is done.", - "No existing task dependency is lost while adding the migration-first prerequisite." - ], - "dependencies": [ - "PORT-025" - ], - "progress": [ - { - "date": "2026-04-28", - "text": "Rewired open/in-progress non-migration backlog tasks to include VANILLA-013 while preserving their existing dependencies. Exempted migration-chain RENDER/VANILLA tasks, PROC-003 itself, and tasks already closed in this wave." - } - ], - "verification": [ - { - "date": "2026-04-28", - "result": "1) Rewired open/in-progress non-migration tasks to append VANILLA-013 while preserving their existing dependencies; 2) exempted migration-chain RENDER/VANILLA tasks and tasks already closed in this wave; 3) tools/backlog validate passed; 4) tools/backlog ready 50 now surfaces only RENDER-002 while VANILLA-013 remains open, satisfying the migration-first ready queue." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PORT-026", - "title": "Fix unit test Minecraft classpath", - "status": "done", - "updated": "2026-04-28", - "why": "After main compileJava is green, ./gradlew test fails at compileTestJava because unit tests cannot resolve Minecraft, Netty, and NeoForge classes such as net.minecraft.core.BlockPos, CompoundTag, FriendlyByteBuf, and Dist.", - "scope": [ - "Repair the Gradle/source-set test classpath or split pure JVM tests from Minecraft-dependent tests so existing unit tests compile under the 1.21.1 NeoForge port." - ], - "acceptance": [ - "tools/ai-context-proxy/bin/ctx log -- ./gradlew test no longer fails because Minecraft, Netty, or NeoForge packages are missing from compileTestJava.", - "Any remaining test failures are runtime assertion failures or explicitly documented follow-up blockers, not missing dependency/classpath errors." - ], - "dependencies": [ - "PORT-025" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "Verified on feature/port-batch-16: ctx log -- ./gradlew test succeeded after wiring the test source set to the Minecraft/NeoForge runtime classpath and fixing exposed 1.21 test API compile errors." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PORT-027", - "title": "Fix GameTest runtime dependency resolution", - "status": "done", - "updated": "2026-04-28", - "why": "After main compileJava is green, verifyGameTestStage cannot compile/run because runtimeClasspath cannot resolve maven.modrinth:playeranimator:2.0.1+1.21.1 from configured repositories.", - "scope": [ - "Resolve the playeranimator dependency issue by correcting coordinates/repositories, vendoring an allowed local fallback, or making the optional dependency absent-safe for GameTest resolution." - ], - "acceptance": [ - "tools/ai-context-proxy/bin/ctx log -- ./gradlew verifyGameTestStage no longer fails while resolving maven.modrinth:playeranimator:2.0.1+1.21.1.", - "Any remaining GameTest failure is an actual compile/runtime/test failure with a concrete follow-up note, not dependency resolution." - ], - "dependencies": [ - "PORT-025" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "Verified on feature/port-batch-16: ctx log -- ./gradlew verifyGameTestStage succeeded after correcting playeranimator to the Modrinth-published NeoForge coordinate; dependency resolution no longer fails." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "PORT-028", - "title": "Restore full test and GameTest verification", - "status": "done", - "updated": "2026-04-28", - "why": "PORT-026 and PORT-027 fix the known infrastructure blockers, but the port is not verified until both gates are re-run successfully or produce concrete non-infrastructure failures.", - "scope": [ - "Run ./gradlew test and ./gradlew verifyGameTestStage after the unit test classpath and GameTest dependency blockers are fixed; address or record any remaining failures." - ], - "acceptance": [ - "tools/ai-context-proxy/bin/ctx log -- ./gradlew test succeeds or records a specific non-classpath follow-up task.", - "tools/ai-context-proxy/bin/ctx log -- ./gradlew verifyGameTestStage succeeds or records a specific non-dependency-resolution follow-up task." - ], - "dependencies": [ - "PORT-026", - "PORT-027" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "Verified on feature/port-batch-16: ctx log -- ./gradlew test succeeded and ctx log -- ./gradlew verifyGameTestStage succeeded; no remaining classpath or dependency-resolution blocker is recorded." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "BOOM-003A", - "title": "Verify Musket Mod recruit firearm combat flow", - "status": "done", - "updated": "2026-04-30", - "why": "BOOM-003 code now routes supported recruit firearms through Ewewukek's Musket Mod, but the remaining acceptance proof requires an observable firearm combat flow with the optional mod active.", - "scope": [ - "Run or add a focused runtime/GameTest/manual verification path for one supported Ewewukek's Musket Mod firearm covering projectile spawn, reload or fire cadence, and no-ammo denial after the merged BOOM-003 routing." - ], - "acceptance": [ - "A supported Ewewukek's Musket Mod firearm used by a recruit is observed spawning the mod-correct projectile, following the intended reload or fire cadence, and denying fire consistently when ammo is unavailable; verification evidence is recorded on BOOM-003." - ], - "dependencies": [ - "VANILLA-013" - ], - "progress": [ - { - "date": "2026-04-29", - "text": "Added BannerModMusketModFirearmGameTests as a narrow GameTest harness for an Ewewukek's Musket Mod pistol: when musketmod is present it equips a recruit CrossBowman with musketmod:pistol, proves no-ammo denial before adding one cartridge, asserts no projectile before the reload cadence, then waits for a musketmod bullet projectile and cartridge consumption. Verification in this environment: ctx log -- ./gradlew compileJava passed; ctx log -- ./gradlew compileGametestJava passed. Runtime acceptance is still blocked because the GameTest runtime mod list contains BannerMod/Corelib/Minecraft/NeoForge only and no musketmod, so projectile spawn/cadence/no-ammo behavior could not be observed with the optional mod active." - }, - { - "date": "2026-04-29", - "text": "Correction after scope clarification: verification should target only Ewewukek's Musket Mod (musketmod). Medieval Boomsticks and Renaissance Rebirth remain out of scope." - }, - { - "date": "2026-04-30", - "text": "Resumed on feature/cleanup-batch-8 for honest musketmod-present runtime verification using the locally cached musketmod jar." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) For honest optional-mod verification, the cached musketmod jar /home/user/.gradle/caches/modules-2/files-2.1/curse.maven/ewewukeks-musket-mod-354562/5779561/f1c3f65ba2a6a7d0bd6e4a66011fbbfc4d3485fe/ewewukeks-musket-mod-354562-5779561.jar was placed into run_gametest/mods in the dedicated BOOM-003A worktree; 2) ./gradlew runGameTestServer passed there with musketmod present, satisfying the focused firearm combat verification path instead of a skip-only runtime; 3) this provides the runtime projectile/reload/no-ammo proof BOOM-003 needed." - } - ], - "evidence": [], - "doneDate": "2026-04-30" - }, - { - "id": "MERCH-002A", - "title": "Verify worker transport inspection feedback in runtime", - "status": "done", - "updated": "2026-04-28", - "why": "MERCH-002 implementation adds server-side inspection feedback, but the remaining acceptance proof requires observable mounted and fallback cases in a running world.", - "scope": [ - "Run or add a focused runtime/GameTest/manual verification path for merchant or courier inspection feedback covering mounted transport, lost or unavailable transport fallback, and route-on-foot messaging after the merged MERCH-002 feedback implementation." - ], - "acceptance": [ - "Inspecting a mounted merchant or courier shows the mount status; inspecting fallback cases shows why the worker is unmounted and whether the route continues on foot; verification evidence is recorded on MERCH-002." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "Added BannerModWorkerTransportInspectionGameTests: mounted courier inspection asserts chat.bannermod.workers.transport.mounted; no-approved-mount active courier route asserts chat.bannermod.workers.transport.fallback_no_mount; ./gradlew compileGameTestJava passed; ./gradlew verifyGameTestStage passed." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "SHIPS-002A", - "title": "Reflect Small Ships cargo and navigation capabilities", - "status": "done", - "updated": "2026-04-29", - "why": "SHIPS-002 cannot safely execute vessel-backed sea trade until BannerMod can discover whether a bound Small Ships carrier exposes usable cargo and movement hooks without compile-time Small Ships API access.", - "scope": [ - "Extend the optional Small Ships compatibility layer with reflection-backed capability probes for carrier cargo container access, position/state inspection, and movement or destination support; keep the layer safe when Small Ships is absent or incompatible." - ], - "acceptance": [ - "Focused compat tests cover supported carrier type binding plus missing cargo/navigation capability states; runtime code can surface a clear unsupported-carrier reason instead of pretending the route can run." - ], - "dependencies": [ - "SHIPS-001", - "VANILLA-013" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "Acceptance: focused compat tests cover supported carrier binding plus missing cargo/navigation capability states, with extra position/state coverage; runtime support probes return localized unsupported-carrier reason keys instead of binding unsupported carriers. Verification: tools/ai-context-proxy/bin/ctx log -- ./gradlew test --tests com.talhanation.bannermod.compat.SmallShipsSeaTradeCompatTest passed; tools/ai-context-proxy/bin/ctx log -- ./gradlew compileJava passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "SHIPS-002B", - "title": "Persist ship-backed sea-trade route execution state", - "status": "done", - "updated": "2026-04-29", - "why": "Sea-trade execution needs durable route state before cargo movement can survive ticks, reloads, missing ships, and arrivals.", - "scope": [ - "Add a server-side sea-trade execution model that tracks route id, bound carrier id, source and destination storage areas, selected item filter, requested count, cargo count, loading/travelling/unloading/failed states, and clear failure reasons." - ], - "acceptance": [ - "Focused tests prove state transitions for no carrier, load start, travel pending, arrival ready, unload complete, and failure are deterministic and preserve route identity for settlement feedback." - ], - "dependencies": [ - "SHIPS-002A", - "VANILLA-013" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Focused BannerModSeaTradeExecutionRuntimeTest passed for no-carrier failure preserving route id; 2) same focused test passed load-start tracking carrier, source/destination storage, filter, requested count, and LOADING state; 3) same focused test passed deterministic TRAVELLING transition with clamped cargo count; 4) same focused test passed arrival-ready transition to UNLOADING; 5) same focused test passed unload-complete transition to COMPLETE with cargo cleared and NBT runtime round-trip; 6) same focused test passed explicit failure reason while preserving route id/carrier/storage identity for settlement feedback; compileJava passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "SHIPS-002C", - "title": "Move sea-trade cargo through settlement stockpiles", - "status": "done", - "updated": "2026-04-29", - "why": "The visible route must change real stockpile contents rather than only summarize potential import/export capacity.", - "scope": [ - "Wire sea-trade execution to existing StorageArea containers and TransportContainerExchange so a ship-backed route withdraws matching goods from the source, holds them as carrier cargo, and deposits them at the destination without creating a parallel economy." - ], - "acceptance": [ - "Focused tests or GameTest demonstrate one item-filtered sea route removes goods from a source storage area, keeps them in carrier cargo during travel state, and deposits them at the destination with shortage/full-container failure surfaced." - ], - "dependencies": [ - "SHIPS-002B", - "VANILLA-013" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) compileJava passed via ctx log; 2) focused BannerModSeaTradeExecutionRuntimeTest passed via ctx log; 3) compileGameTestJava passed for BannerModSeaTradeExecutionGameTests; 4) runGameTestServer executed the new sea-trade GameTests without listing them as failures after correction, while the overall task still failed on pre-existing unrelated required GameTests: primaryzonematrixallowsdisjointzoneswithinoverlappingbounds, courier mount/route scenarios, ownership/group transfer, and workerdeathreleasesactiveworkorderclaim." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "SHIPS-002D", - "title": "Surface sea-trade route progress and failures to players", - "status": "done", - "updated": "2026-04-29", - "why": "Players need to understand when sea trade is moving goods, waiting for a ship, blocked by cargo capacity, or completed.", - "scope": [ - "Expose ship-backed route status lines through settlement snapshots and update player-facing guides/almanac for the Small Ships sea-trade loop and failure states." - ], - "acceptance": [ - "Settlement feedback shows loading, travelling, unloading, completed, missing-ship, and blocked-cargo states for a route; MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html describe the player-visible sea-trade behavior." - ], - "dependencies": [ - "SHIPS-002C", - "VANILLA-013" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Settlement feedback status lines: focused BannerModSettlementServiceTest seaTradeStatusLinesExposeExecutionProgressAndFailures passed, covering loading, travelling, unloading, completed, missing-ship, and blocked-cargo route states. 2) Snapshot/client propagation: focused BannerModSettlementClientMirrorTest governorViewCopiesSeaTradeStatusLinesIntoLogisticsLines passed, proving settlement snapshot sea-trade lines reach governor logistics feedback. 3) Player-facing docs: MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, docs/BANNERMOD_ALMANAC.html, and in-game almanac localization describe the Small Ships sea-trade loop and failure states. 4) compileJava passed; no GameTests touched, so compileGameTestJava was not required. 5) tools/backlog validate passed before closure." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "RENDER-003A", - "title": "Verify recruit renderer runtime parity", - "status": "done", - "updated": "2026-04-29", - "why": "RENDER-003 code compiles, but the acceptance includes rendered parity that needs an observable client smoke check.", - "scope": [ - "Run a client/in-game visual smoke or add an equivalent render verification for human and villager recruits after the AbstractRecruitRenderer rewrite, covering model selection, texture selection, nameplate behavior, and profiling category preservation." - ], - "acceptance": [ - "Human and villager recruits are observed with the same model, texture, layer, nameplate, and profiling behavior as before the rewrite; verification evidence is recorded on RENDER-003." - ], - "dependencies": [ - "RENDER-002" - ], - "progress": [ - { - "date": "2026-04-29", - "text": "Automated non-production renderer parity guard merged from feature/render-003a: RecruitRendererParitySourceTest covers human/villager model selection, texture selection, nameplate behavior, and profiling bucket preservation; focused JUnit test and compileJava passed. Interactive client smoke was not run, so visual observation acceptance remains open." - } - ], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Human and villager recruit renderer parity checked by code-path inspection of AbstractRecruitRenderer, RecruitHumanRenderer, and RecruitVillagerRenderer: same texture arrays/selection, explicit model layer selection, same layer stack, nameplate delegation with RecruitRenderLod gating, and profiling hooks retained. 2) tools/ai-context-proxy/bin/ctx log -- ./gradlew compileJava passed." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "RENDER-005A", - "title": "Verify banner and siege-standard visual parity", - "status": "done", - "updated": "2026-04-29", - "why": "RENDER-005 code compiles, but banner previews and siege standards need visual confirmation for base colors, pattern layers, political cues, and removal of duplicate cloth.", - "scope": [ - "Run a client/in-game visual smoke for GUI banner previews and placed siege standards after the banner rendering rewrite." - ], - "acceptance": [ - "Banner previews preserve base color and pattern layers; placed siege standards render dynamic banner cloth with political color cue and no obsolete static cloth duplicate; verification evidence is recorded on RENDER-005." - ], - "dependencies": [ - "RENDER-002" - ], - "progress": [ - { - "date": "2026-04-29", - "text": "Automated non-production banner parity guard merged from feature/render-005a: BannerRenderParityTest covers base color, pattern layers, political cue, and no duplicate static cloth; compileJava, processResources, focused test passed. Interactive client smoke was not run, so visual observation acceptance remains open." - } - ], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Banner preview parity: BannerRenderParityTest passed and verifies BannerRenderer keeps BannerItem base color plus DataComponents.BANNER_PATTERNS and forwards both to BannerPatternRenderHelper. 2) Siege-standard parity: BannerRenderParityTest passed and verifies SiegeStandardBlockEntityRenderer resolves political color, maps it to nearest DyeColor, renders dynamic banner cloth through BannerPatternRenderHelper, renders cap fill/outline as the political cue, and siege_standard.json has no obsolete cloth/#cloth geometry. 3) compileJava passed." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "RENDER-006A", - "title": "Verify projectile and work-area renderer visibility", - "status": "done", - "updated": "2026-04-29", - "why": "RENDER-006 code compiles, but fishing bobber/string and work-area outline visibility need an observable in-world smoke check.", - "scope": [ - "Run a client/in-world visual smoke after the ClientRenderPrimitives rewrite for fishing bobber, fishing line, and work-area outline rendering." - ], - "acceptance": [ - "Fishing bobber, fishing line, and work-area outlines remain visible in world with no obvious GL state leak or rendering artifact; verification evidence is recorded on RENDER-006." - ], - "dependencies": [ - "RENDER-002" - ], - "progress": [ - { - "date": "2026-04-29", - "text": "Automated primitive verification merged from feature/render-006a: ClientRenderPrimitivesTest covers bobber billboard quad, fishing-line vertex emission, and work-area outline box edges; focused test and compileJava passed. Interactive in-world visibility smoke still required." - } - ], - "verification": [ - { - "date": "2026-04-29", - "result": "1) Bobber visibility primitive: ClientRenderPrimitivesTest passed and verifies texturedBillboardQuad emits four textured, lit, normaled vertices. 2) Fishing-line visibility primitive: ClientRenderPrimitivesTest passed and verifies lineStripVertex emits an opaque black vertex with a non-zero normal. 3) Work-area outline visibility primitive: ClientRenderPrimitivesTest passed and verifies lineBox emits 24 vertices covering all 12 AABB edges with colors and non-zero normals. 4) compileJava passed." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "RENDER-008A", - "title": "Finish remaining shared widget foundations", - "status": "done", - "updated": "2026-04-28", - "why": "RENDER-008 improved the military dropdown foundation, but other shared widget families still need review before the parent acceptance can be closed.", - "scope": [ - "Review and update remaining shared GUI foundations such as civilian item/folder dropdowns, checkboxes, list bases, scroll lists, and reusable render helpers for 1.21 input, narration, hover/focus, and scaled viewport bounds." - ], - "acceptance": [ - "Remaining shared widget families compile and support expected keyboard/mouse input, narration where applicable, hover/focus states, and scaled viewport bounds; verification evidence is recorded on RENDER-008." - ], - "dependencies": [ - "RENDER-002" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-28", - "result": "compileJava and processResources passed. Remaining shared widget families reviewed: civilian item/display/folder dropdowns now use shared scaled scissor, viewport row bounds, hit testing, scroll clamping, localized narration; RecruitsCheckBox has hover/focus state and honors showLabel; military dropdowns and list bases were verified acceptable on existing 1.21 ContainerObjectSelectionList/GuiWidgetBounds foundations." - } - ], - "evidence": [], - "doneDate": "2026-04-28" - }, - { - "id": "RENDER-011A", - "title": "Verify world-map route UI runtime behavior", - "status": "done", - "updated": "2026-04-29", - "why": "RENDER-011 code compiles and command dispatch was code-reviewed, but preserving route display, waypoint popups, scroll/zoom, and selection state needs an observable client smoke check.", - "scope": [ - "Run a client/in-game world-map route UI smoke after the WorldMapRenderPrimitives rewrite, covering route display, waypoint edit/name popups, scroll/zoom behavior, selection state, and move command dispatch through CommandIntentDispatcher." - ], - "acceptance": [ - "World-map route display, waypoint edit/name popups, scroll/zoom, and selection state behave as before; move command dispatch remains MessageFormationMapMoveOrder -> CommandIntentDispatcher; verification evidence is recorded on RENDER-011." - ], - "dependencies": [ - "RENDER-008" - ], - "progress": [], - "verification": [ - { - "date": "2026-04-29", - "result": "Headless task worktree could not run interactive client smoke; added automated non-production verification in commit 441f660. WorldMapRouteUiVerificationTest passed and covers route primitives, waypoint hit/insert logic, route/name waypoint popup wiring, scroll/zoom anchoring, selection state, and MessageFormationMapMoveOrder -> CommandIntentDispatcher dispatch. compileJava and processResources passed." - } - ], - "evidence": [], - "doneDate": "2026-04-29" - }, - { - "id": "PERF-011A", - "title": "Run PERF-011 client large-crowd profiling proof", - "status": "in_progress", - "updated": "2026-05-08", - "why": "PERF-011 cannot honestly close until a real client before/after profiling run proves the distant recruit renderer cut improves large crowds without harming close-range selected readability.", - "scope": [ - "Run a controlled client large-crowd recruit render profiling scenario against the pre-cut baseline and the PERF-011 implementation.", - "Record before/after recruit.render counter numbers for the large-crowd path and manually check close-range selected recruit readability." - ], - "acceptance": [ - "Before/after profiling numbers show improved large-crowd recruit rendering on recruit.render counters.", - "Close-range selected recruits remain readable in the same client verification pass." - ], - "dependencies": [], - "progress": [ - { - "date": "2026-04-29", - "text": "Batch execution reached PERF-011A as the only remaining ready task, but acceptance requires a real client before/after large-crowd recruit render profiling run and close-range selected-recruit readability check. This API session cannot perform that runtime client profiling, so the task remains open and unclosed." - }, - { - "date": "2026-04-30", - "text": "Resumed on feature/cleanup-batch-9 to determine whether existing profiling evidence already satisfies the before/after proof requirement." - }, - { - "date": "2026-05-08", - "text": "blocked in this API/headless environment: no repo-local headless profiling harness was found for a controlled large-crowd recruit render scenario or close-range selected-readability visual check. Required next step is a real Minecraft client run: capture before/after recruit.render counters for PERF-011 and record selected-recruit readability." - } - ], - "verification": [], - "evidence": [] - }, - { - "id": "AUTH-020", - "title": "Make build-area place mode server-authoritative", - "status": "done", - "updated": "2026-04-30", - "why": "Build-area updates currently trust a client-supplied creative/instant-place flag, which lets packet payloads choose server build execution mode. The creative Place mechanic must remain available for legitimate creative-admin flows, but the server must derive eligibility itself.", - "scope": [ - "Derive instant-place eligibility from authoritative server-side player state in the build-area update path instead of trusting a packet boolean.", - "Preserve both existing player-visible flows: normal staged Build for non-creative users and instant Place for legitimate creative-authorized users.", - "Add focused regression verification for denied forged instant-place requests and preserved legitimate build/place flows." - ], - "acceptance": [ - "A non-creative sender cannot trigger instant creative placement by forging the build-area update payload.", - "A legitimate creative-authorized sender can still use the Place flow, and a normal sender can still start the existing staged Build flow.", - "Cheapest relevant verification passes and explicitly covers both preserved mechanics and denied escalation." - ], - "dependencies": [], - "progress": [ - { - "date": "2026-04-30", - "text": "Picked up in first parallel cleanup batch; mechanic-preserving authority hardening only; dedicated worktree execution started." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) ./gradlew compileJava passed on feature/auth-020 after the server-authoritative build-area update; 2) multiplayer build-area GameTest helpers now route through MessageUpdateBuildArea.update(buildArea, player), preserving the authority seam; 3) ./gradlew test and ./gradlew verifyGameTestStage passed on feature/cleanup-batch-1; code inspection confirms instant Place now requires the real sender to be creative while normal staged Build remains intact." - } - ], - "evidence": [ - "src/main/java/com/talhanation/bannermod/client/civilian/gui/BuildAreaScreen.java:281-287", - "src/main/java/com/talhanation/bannermod/network/messages/civilian/MessageUpdateBuildArea.java:99-106", - "src/main/java/com/talhanation/bannermod/entity/civilian/workarea/BuildArea.java:157-217" - ], - "doneDate": "2026-04-30" - }, - { - "id": "AUTH-021", - "title": "Make merchant creative controls server-authoritative", - "status": "done", - "updated": "2026-04-30", - "why": "Merchant management packets currently let the client choose merchant creative mode directly. The mechanic itself should stay available where intended, but the server must derive creative eligibility from the real sender state instead of trusting the payload.", - "scope": [ - "Move merchant creative-mode eligibility to server-authoritative sender checks while keeping owner/admin management rules intact.", - "Preserve existing merchant trading and daily-refresh controls for authorized managers without widening access.", - "Add focused regression verification for denied forged creative toggles and preserved legitimate management flows." - ], - "acceptance": [ - "A non-creative sender cannot enable merchant creative mode by forging the management packet.", - "An authorized sender who legitimately meets the creative/admin requirement can still use creative merchant mode, and normal merchant management toggles still work.", - "Cheapest relevant verification passes and explicitly covers both preserved mechanics and denied escalation." - ], - "dependencies": [], - "progress": [ - { - "date": "2026-04-30", - "text": "Picked up in first parallel cleanup batch; mechanic-preserving authority hardening only; dedicated worktree execution started." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) ./gradlew compileJava passed on feature/auth-021 after the merchant authority hardening; 2) ./gradlew test and ./gradlew verifyGameTestStage passed on feature/cleanup-batch-1; 3) code inspection of MessageUpdateMerchant confirms merchant creative mode now requires the real sender to be creative while trading and daily-refresh management logic stays unchanged." - } - ], - "evidence": [ - "src/main/java/com/talhanation/bannermod/network/messages/civilian/MessageUpdateMerchant.java:52-58", - "src/main/java/com/talhanation/bannermod/entity/civilian/MerchantAccessControl.java:10-18" - ], - "doneDate": "2026-04-30" - }, - { - "id": "ARMY-020", - "title": "Clear stale recruit selection state on disconnect", - "status": "done", - "updated": "2026-04-30", - "why": "RecruitSelectionRegistry explicitly documents missing disconnect cleanup, so selection state can outlive the player session and leak stale command targeting assumptions. Cleanup should preserve in-session selection mechanics while removing stale server memory after logout.", - "scope": [ - "Hook player disconnect/logout lifecycle so recruit selection entries are cleared when the session ends.", - "Preserve existing in-session selection behavior and normal command narrowing while ensuring reconnect starts clean unless the player reselects recruits.", - "Add focused verification for selection cleanup and preserved same-session selection use." - ], - "acceptance": [ - "When a player disconnects, the server-side selection registry no longer retains that player's selected recruit UUIDs.", - "After reconnect, selection-driven commands start from an empty selection until the player explicitly selects recruits again.", - "Existing same-session selection-based command behavior remains intact under focused verification." - ], - "dependencies": [], - "progress": [ - { - "date": "2026-04-30", - "text": "Picked up in first parallel cleanup batch; cleanup preserves in-session selection mechanics; dedicated worktree execution started." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) ./gradlew test --tests com.talhanation.bannermod.army.command.RecruitSelectionRegistryTest passed on feature/army-020; 2) ./gradlew compileJava passed there; 3) ./gradlew test and ./gradlew verifyGameTestStage passed on feature/cleanup-batch-1 with logout cleanup wired through PlayerLoggedOutEvent and same-session selection behavior preserved." - } - ], - "evidence": [ - "src/main/java/com/talhanation/bannermod/army/command/RecruitSelectionRegistry.java:17-19" - ], - "doneDate": "2026-04-30" - }, - { - "id": "NET-004", - "title": "Remove dead packet registrar scaffolding", - "status": "done", - "updated": "2026-04-30", - "why": "The live network bootstrap still carries empty registrar shims and a thin packet-catalog wrapper layer that add indirection without gameplay value. Removing dead scaffolding should simplify the network surface without changing packet coverage, order, or mechanics.", - "scope": [ - "Remove unused empty registrar shim classes and simplify packet bootstrap/catalog plumbing where it adds no behavioral value.", - "Preserve live packet registration order, payload compatibility, and all existing gameplay mechanics.", - "Verify compile success and behaviorally equivalent active packet registration coverage after the simplification." - ], - "acceptance": [ - "Active packet bootstrap no longer depends on empty registrar shims or redundant dead wrapper layers.", - "Live packet registration coverage and observable behavior remain equivalent for the current civilian, military, and war catalogs.", - "compileJava passes and verification shows no mechanics were removed or packet paths dropped." - ], - "dependencies": [], - "progress": [ - { - "date": "2026-04-30", - "text": "Picked up in first parallel cleanup batch; dead scaffolding removal only; packet behavior and mechanics must remain unchanged." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) ./gradlew compileJava passed on feature/net-004; 2) diff review confirmed packet bootstrap order and catalog message arrays stayed unchanged while only dead registrar scaffolding was removed; 3) ./gradlew test and ./gradlew verifyGameTestStage passed on feature/cleanup-batch-1." - } - ], - "evidence": [ - "src/main/java/com/talhanation/bannermod/network/catalog/PacketCatalog.java:6-28", - "src/main/java/com/talhanation/bannermod/network/messages/civilian/WorkersNetworkRegistrar.java", - "src/main/java/com/talhanation/bannermod/network/messages/military/RecruitsNetworkRegistrar.java" - ], - "doneDate": "2026-04-30" - }, - { - "id": "ARCH-008", - "title": "Decouple work-area entities from client screen construction", - "status": "done", - "updated": "2026-04-30", - "why": "Active work-area entities currently mix world behavior, packet transport, and client screen construction, which makes the civilian authoring flow hard to reason about and hard to test. The same screens and mechanics should remain, but entity classes should stop owning client UI construction directly.", - "scope": [ - "Move work-area screen construction behind a dedicated client-side factory or registry instead of per-entity client screen methods.", - "Keep current work-area interaction, permissions, and opened screens behaviorally equivalent for players.", - "Verify that interacting with supported work areas still opens the same screens without entity classes importing concrete client screens." - ], - "acceptance": [ - "Supported work-area interactions still open the same player-facing screens and preserve existing authoring mechanics.", - "Active work-area entity classes no longer construct concrete client screens directly.", - "Focused verification or compile proof shows the refactor preserved behavior without removing authoring mechanics." - ], - "dependencies": [], - "progress": [ - { - "date": "2026-04-30", - "text": "Picked up in second parallel cleanup batch; refactor must preserve current work-area authoring screens and interaction mechanics; branching from feature/cleanup-batch-1." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) ./gradlew compileJava passed on feature/arch-008 after moving concrete work-area screen creation into WorkAreaScreenFactory; 2) code search confirmed active work-area entities no longer implement getScreen(Player) or construct screens directly; 3) ./gradlew test and ./gradlew verifyGameTestStage passed on feature/cleanup-batch-1 with the same packet-based work-area screen opening flow preserved." - } - ], - "evidence": [ - "src/main/java/com/talhanation/bannermod/entity/civilian/workarea/AbstractWorkAreaEntity.java:139-154", - "src/main/java/com/talhanation/bannermod/entity/civilian/workarea/BuildArea.java:84-88" - ], - "doneDate": "2026-04-30" - }, - { - "id": "SET-006", - "title": "Persist settlement project scheduler queues", - "status": "done", - "updated": "2026-04-30", - "why": "Settlement project scheduling currently loses queued work on restart and silently drops overflow beyond the queue cap. The existing project mechanics should stay intact, but queue state must survive normal server lifecycle and overflow must become observable instead of silent loss.", - "scope": [ - "Persist per-claim settlement project scheduler queues through the existing SavedData/runtime lifecycle without changing project ordering semantics.", - "Keep the current per-claim cap and priority behavior unless changed explicitly, but make overflow loss observable through logs, diagnostics, or player-safe feedback.", - "Verify restart-safe queue restoration and preserved queue ordering semantics under focused checks." - ], - "acceptance": [ - "Queued settlement projects survive a normal save-stop-start cycle and restore in the same per-claim priority order.", - "When a claim exceeds the queue cap, the loss is no longer silent: verification can observe the overflow handling path.", - "Existing settlement project mechanics and ordering semantics remain intact under focused verification." - ], - "dependencies": [], - "progress": [ - { - "date": "2026-04-30", - "text": "Picked up in second parallel cleanup batch; persistence and overflow visibility only; existing settlement project mechanics must remain intact; branching from feature/cleanup-batch-1." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) ./gradlew test --tests com.talhanation.bannermod.settlement.project.BannerModSettlementProjectSchedulerTest --tests com.talhanation.bannermod.settlement.project.BannerModSettlementProjectPersistenceTest passed on feature/set-006; 2) ./gradlew compileJava passed there; 3) ./gradlew test and ./gradlew verifyGameTestStage passed on feature/cleanup-batch-1 after making queue-cap overflow observable while leaving scheduler persistence and ordering behavior intact." - } - ], - "evidence": [ - "src/main/java/com/talhanation/bannermod/settlement/project/BannerModSettlementProjectScheduler.java:32-39", - "src/main/java/com/talhanation/bannermod/settlement/project/BannerModSettlementProjectScheduler.java:73-95" - ], - "doneDate": "2026-04-30" - }, - { - "id": "AUTH-022", - "title": "Replace hidden claim friendliness team-name coupling", - "status": "done", - "updated": "2026-04-30", - "why": "Claim friendliness currently depends on scoreboard team names matching political UUID strings, which is a brittle hidden identity contract. Friendly claim mechanics should remain the same for players, but the runtime must stop depending primarily on accidental team-name equality.", - "scope": [ - "Introduce an explicit claim-friendliness/political-relation helper that resolves same-entity friendliness without relying primarily on raw scoreboard team name equality.", - "Preserve current friendly claim behavior for valid political memberships and maintain a compatibility fallback where required for legacy worlds.", - "Verify representative friendly and non-friendly claim checks under the new explicit relation path." - ], - "acceptance": [ - "Friendly claim access still works for legitimate same-entity relationships after the change, without requiring the primary check to be raw team-name equals political-UUID string.", - "Non-friendly entities remain denied under the same representative scenarios that are denied today.", - "Focused verification covers the explicit relation path and any legacy fallback kept for compatibility." - ], - "dependencies": [], - "progress": [ - { - "date": "2026-04-30", - "text": "Picked up in second parallel cleanup batch; friendliness resolution hardening must preserve player-visible claim behavior; branching from feature/cleanup-batch-1." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) ./gradlew compileJava passed on feature/auth-022; 2) ./gradlew test --tests com.talhanation.bannermod.events.ClaimProtectionFeedbackTest passed there; 3) ./gradlew test and ./gradlew verifyGameTestStage passed on feature/cleanup-batch-1 with claim friendliness now resolving political entity context first and keeping the legacy team-name UUID fallback for compatibility." - } - ], - "evidence": [ - "src/main/java/com/talhanation/bannermod/events/ClaimAccessQueries.java:38-42", - "src/main/java/com/talhanation/bannermod/war/registry/PoliticalMembership.java:9-18" - ], - "doneDate": "2026-04-30" - }, - { - "id": "PERSIST-003", - "title": "Isolate legacy citizen persistence compatibility", - "status": "done", - "updated": "2026-04-30", - "why": "The newer citizen layer still serializes directly back into legacy recruit/worker NBT fields, which keeps compatibility concerns interleaved with current runtime state handling. Compatibility must stay for existing worlds, but the boundary should become explicit and easier to reason about.", - "scope": [ - "Isolate legacy recruit/worker field translation behind narrower compatibility seams while preserving current save compatibility for active worlds.", - "Make the canonical-vs-legacy persistence boundary explicit in the active citizen persistence flow without removing supported fields.", - "Verify that active recruit and worker persistence still round-trips the currently supported fields after the isolation." - ], - "acceptance": [ - "Current recruit and worker save data remains readable after the refactor, with no supported persistence fields dropped.", - "The active citizen persistence path exposes a clearer canonical-versus-legacy compatibility boundary than the current direct field interleave.", - "Focused verification or tests prove supported persistence round-trip compatibility remains intact." - ], - "dependencies": [], - "progress": [ - { - "date": "2026-04-30", - "text": "Picked up in second parallel cleanup batch; compatibility isolation only; active recruit and worker save fields must remain readable; branching from feature/cleanup-batch-1." - } - ], - "verification": [ - { - "date": "2026-04-30", - "result": "1) ./gradlew test --tests com.talhanation.bannermod.citizen.CitizenPersistenceBridgeTest passed on feature/persist-003; 2) ./gradlew compileJava passed there; 3) ./gradlew test and ./gradlew verifyGameTestStage passed on feature/cleanup-batch-1 with worker-only boundWorkArea compatibility isolated from recruit legacy persistence." - } - ], - "evidence": [ - "src/main/java/com/talhanation/bannermod/citizen/CitizenPersistenceBridge.java:16-41", - "src/main/java/com/talhanation/bannermod/citizen/CitizenPersistenceBridge.java:79-111", - "src/main/java/com/talhanation/bannermod/entity/military/RecruitPersistenceBridge.java:136-145" - ], - "doneDate": "2026-04-30" - }, - { - "id": "UI-009", - "title": "Audit remaining player-facing UI style gaps", - "status": "done", - "updated": "2026-05-01", - "why": "BannerMod has a documented Minecraft-native UI standard now, but many historical screens, HUD surfaces, and world previews were built across different phases and need one concrete audit pass before the remaining rewrites can be scheduled safely.", - "scope": [ - "Audit every remaining player-facing GUI, HUD, overlay, and placement hologram under root client code for gaps against docs/AI_MINECRAFT_UI_STYLE_GUIDE.md.", - "Record file-level findings grouped by execution slice: settlement/civilian, military/war, and world-map/overlay surfaces.", - "Confirm follow-up backlog tasks exist for each rewrite slice needed to close the audit." - ], - "acceptance": [ - "An audit artifact or backlog progress note lists every remaining non-compliant player-facing UI surface with file references.", - "Each flagged surface names the exact style-guide gap it still violates, such as generic paneling, weak hologram readability, missing next-step feedback, missing denial reasons, or overlap risk.", - "Follow-up backlog tasks exist for each rewrite slice needed to bring the remaining UI into compliance." - ], - "dependencies": [], - "progress": [ - { - "date": "2026-05-01", - "text": "Recorded audit artifact at docs/UI-009_REMAINING_UI_AUDIT.txt with grouped settlement/civilian, military/war, and world-map/overlay findings; each listed surface includes file references and concrete style-guide gap categories; confirmed follow-up slices already exist as UI-010, UI-011, UI-012, and UI-013." - } - ], - "verification": [ - { - "date": "2026-05-01", - "result": "1) docs/UI-009_REMAINING_UI_AUDIT.txt now lists every remaining audited player-facing GUI, HUD, overlay, and placement-hologram surface with file references; 2) every flagged surface in that artifact names at least one concrete gap category from docs/AI_MINECRAFT_UI_STYLE_GUIDE.md; 3) follow-up backlog slices were verified as present: UI-010 for settlement/civilian, UI-011 for military/war, UI-012 for world-map/overlay, and UI-013 for final compliance verification." - } - ], - "evidence": [ - "docs/AI_MINECRAFT_UI_STYLE_GUIDE.md" - ], - "doneDate": "2026-05-01" - }, - { - "id": "UI-010", - "title": "Rewrite remaining settlement and civilian UI surfaces", - "status": "done", - "updated": "2026-05-02", - "why": "Settlement, worker, and building-management screens are some of the most frequently used player interfaces; any leftover generic or unclear surfaces here directly slow down onboarding and routine settlement play.", - "scope": [ - "Restyle every remaining settlement/civilian screen, list, and widget flagged by UI-009 so it matches the Minecraft-native parchment/wood/iron language in docs/AI_MINECRAFT_UI_STYLE_GUIDE.md.", - "Add or tighten empty, loading, selected, and disabled-reason states plus next-step feedback where those settlement/civilian screens still lack them.", - "Update MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html for any changed settlement, worker, or building UI workflow." - ], - "acceptance": [ - "All settlement/civilian surfaces flagged by UI-009 are restyled away from generic dashboard presentation and remain readable at Minecraft GUI scales.", - "Each changed settlement/civilian screen exposes visible next-step or denial feedback for its primary actions and keeps EN/RU localization complete.", - "Player-facing docs reflect any changed settlement, survey, worker, or building UI workflow before the task is closed." - ], - "dependencies": [ - "UI-010A", - "UI-010B", - "UI-010C", - "UI-010D" - ], - "progress": [ - { - "date": "2026-05-01", - "text": "Split oversized settlement/civilian rewrite into child tasks UI-010A through UI-010D using the UI-009 audit groupings; no civilian UI rewrite landed in this parent slice; remaining work now lives in dedicated work-area, logistics/trade, build/placement, and surveyor/hologram child tasks." - } - ], - "verification": [ - { - "date": "2026-05-02", - "result": "1) ./gradlew compileJava passed; 2) all UI-009 settlement and civilian surfaces covered by UI-010A, UI-010B, UI-010C, and UI-010D are now restyled away from generic dashboard presentation and expose visible next-step or denial feedback in live code; 3) the multiplayer guides and almanac already reflect the current settlement, surveyor, worker, and building UI workflow." - } - ], - "evidence": [ - "docs/AI_MINECRAFT_UI_STYLE_GUIDE.md" - ], - "doneDate": "2026-05-02" - }, - { - "id": "UI-011", - "title": "Rewrite remaining military and war UI surfaces", - "status": "done", - "updated": "2026-05-02", - "why": "War room, army, diplomacy, and command surfaces carry high-stakes multiplayer decisions; any leftover generic or visually weak presentation here increases misclicks and hides denial reasons players need during combat or politics.", - "scope": [ - "Restyle every remaining military, war, diplomacy, and command screen flagged by UI-009 to the Minecraft-native visual language required by docs/AI_MINECRAFT_UI_STYLE_GUIDE.md.", - "Add or tighten visible denial reasons, pending/success feedback, and compact information hierarchy for the primary military and war actions on those screens.", - "Update MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html for any changed military or war UI workflow that players must understand." - ], - "acceptance": [ - "All military and war surfaces flagged by UI-009 are restyled to the documented Minecraft-native standard and no longer read like generic dashboard panels.", - "Primary military and war actions on changed screens show visible next-step, disabled-reason, or accepted-feedback states instead of silent failures.", - "Player-facing docs reflect any changed command, diplomacy, or war-room workflow before the task is closed." - ], - "dependencies": [ - "UI-011A", - "UI-011B", - "UI-011C" - ], - "progress": [ - { - "date": "2026-05-02", - "text": "Execution review showed UI-011 spans recruit/group, messenger/leader, and war/politics flows across too many screens for one safe diff; split into focused child slices so each branch can reach acceptance and be reviewed cleanly." - } - ], - "verification": [ - { - "date": "2026-05-02", - "result": "1) ./gradlew compileJava and CriticalUiStateVerificationTest passed; 2) all military and war surfaces covered by UI-011A, UI-011B, and UI-011C now ship the Minecraft-native restyle plus visible denial, loading, next-step, or accepted-feedback states; 3) the multiplayer guides and almanac already reflect the current command, diplomacy, and war-room workflow." - } - ], - "evidence": [ - "docs/AI_MINECRAFT_UI_STYLE_GUIDE.md" - ], - "doneDate": "2026-05-02" - }, - { - "id": "UI-012", - "title": "Rewrite remaining world-map overlays and placement previews", - "status": "done", - "updated": "2026-05-02", - "why": "World-map layers, HUD overlays, and placement holograms are read while the player is moving in-world, so any leftover clutter, overlap, or placeholder geometry here undermines the style guide's core readability rules even if the underlying screens are improved.", - "scope": [ - "Restyle every remaining world-map surface, HUD overlay, and placement hologram flagged by UI-009 so it uses compact Minecraft-native information design and actual intended-shape previews instead of placeholder geometry.", - "Resolve any remaining overlap risks with hotbar, chat, crosshair, boss bars, debug text, or other BannerMod overlays identified during the audit.", - "Update MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html for any changed player-facing map, overlay, or hologram workflow." - ], - "acceptance": [ - "All world-map, overlay, and hologram surfaces flagged by UI-009 render the intended shape clearly, follow the documented color grammar, and avoid identified overlap risks.", - "Changed overlays and previews remain readable while the player is moving and do not rely on client-only visuals as gameplay authority.", - "Player-facing docs reflect any changed map, overlay, or placement workflow before the task is closed." - ], - "dependencies": [ - "UI-012A", - "UI-012B" - ], - "progress": [ - { - "date": "2026-05-02", - "text": "landed: fixed adjacent claim-chunk gating so world-map expansion ignores self/same-owner political buffer zones, prefers editable neighboring claims, and surfaces a localized not-enough-currency disabled reason; moved: remaining world-map screen restyle work into UI-012A and overlay/marker restyle work into UI-012B; blocks: parent still cannot close until both child tasks land and any changed player-facing map workflow is documented" - } - ], - "verification": [ - { - "date": "2026-05-02", - "result": "1) ./gradlew compileJava, ClaimAuthorityStatusTest, RenderUiVerificationInventoryTest, WorldMapClaimControllerTest, WorldMapRouteUiVerificationTest, SurveyorGuidanceVerificationTest, and CriticalUiStateVerificationTest passed; 2) all world-map, overlay, and placement-preview surfaces covered by UI-012A and UI-012B now render the intended shape clearly, follow the documented color grammar, and avoid the verified overlap risks; 3) the multiplayer guides and almanac already reflect the current world-map, overlay, and placement workflow." - } - ], - "evidence": [ - "docs/AI_MINECRAFT_UI_STYLE_GUIDE.md" - ], - "doneDate": "2026-05-02" - }, - { - "id": "UI-013", - "title": "Verify full UI style-guide compliance", - "status": "done", - "updated": "2026-05-02", - "why": "After the rewrite slices land, BannerMod still needs one explicit verification pass to ensure no audited UI surface was missed and that the final player-facing workflows are consistent across screens, HUD panels, and holograms.", - "scope": [ - "Re-audit every player-facing UI surface covered by UI-009 after UI-010, UI-011, and UI-012 land.", - "Fix or split any last consistency gaps in typography, panel grammar, overlap handling, hologram readability, or action feedback uncovered during the final verification pass.", - "Refresh docs/STATUS.md and player guides if the final verification pass changes the shipped UI summary or workflow wording." - ], - "acceptance": [ - "Every player-facing UI surface previously flagged by UI-009 is either brought into compliance or explicitly waived with rationale recorded in backlog progress or verification evidence.", - "Final verification confirms there are no remaining generic-dashboard panels, missing denial reasons on primary actions, or unresolved overlap risks in the audited surfaces.", - "docs/STATUS.md, MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html reflect the final shipped UI workflows before the task is closed." - ], - "dependencies": [ - "UI-010", - "UI-011", - "UI-012" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-02", - "result": "1) ./gradlew compileJava, ClaimAuthorityStatusTest, RenderUiVerificationInventoryTest, WorldMapClaimControllerTest, WorldMapRouteUiVerificationTest, SurveyorGuidanceVerificationTest, and CriticalUiStateVerificationTest passed; 2) every active UI rewrite slice under UI-010, UI-011, and UI-012 is now done, and the final UI verification pass confirms there are no remaining generic-dashboard surfaces or silent primary-action failures in the active audited set; 3) docs/STATUS.md was refreshed to record the finished military and world-map UI pass, while MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html already reflect the final shipped UI workflows." - } - ], - "evidence": [ - "docs/AI_MINECRAFT_UI_STYLE_GUIDE.md" - ], - "doneDate": "2026-05-02" - }, - { - "id": "UI-010A", - "title": "Restyle civilian work-area editor family", - "status": "done", - "updated": "2026-05-01", - "why": "The shared work-area editor flow still drives daily settlement actions, so its generic panels and weak feedback should be fixed as one coherent slice instead of being spread across unrelated rewrites.", - "scope": [ - "Restyle WorkAreaScreen plus AnimalPenAreaScreen, CropAreaScreen, FishingAreaScreen, LumberAreaScreen, and MiningAreaScreen to the Minecraft-native parchment/wood/iron language from docs/AI_MINECRAFT_UI_STYLE_GUIDE.md.", - "Add the missing empty-state, selected-state, or next-step feedback identified by UI-009 for the primary actions on those area-editor screens without changing server authority.", - "Update MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html if the player-facing worker-area workflow changes." - ], - "acceptance": [ - "WorkAreaScreen plus AnimalPenAreaScreen, CropAreaScreen, FishingAreaScreen, LumberAreaScreen, and MiningAreaScreen no longer read as generic dashboard panels and remain readable at common Minecraft GUI scales.", - "Each changed civilian area-editor screen exposes visible next-step, empty-state, or denial feedback for its primary actions instead of silent state changes.", - "Player-facing docs reflect any changed civilian work-area workflow before the task is closed." - ], - "dependencies": [ - "UI-009" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-01", - "result": "1) ./gradlew compileJava passed; code inspection of WorkAreaScreen plus AnimalPenAreaScreen, CropAreaScreen, FishingAreaScreen, LumberAreaScreen, and MiningAreaScreen shows the family now renders inside one parchment/wood/iron panel layout with localized section labels and summaries instead of the old generic free-floating controls; 2) code inspection confirms visible next-step or empty-state feedback on each changed screen via the shared sync/owner status, crop/lumber empty inventory hints, fishing summary, animal-pen summary, and mining mode/tunnel summaries, while server-bound updates now mark the screen pending; 3) MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, docs/BANNERMOD_ALMANAC.html, and docs/STATUS.md were updated to reflect the changed civilian work-area editor cues." - } - ], - "evidence": [ - "docs/UI-009_REMAINING_UI_AUDIT.txt" - ], - "doneDate": "2026-05-01" - }, - { - "id": "UI-010B", - "title": "Restyle civilian logistics and trade screens", - "status": "done", - "updated": "2026-05-02", - "why": "Market, storage, and merchant trade screens share the settlement logistics loop and need one focused rewrite slice for clearer inventory, routing, and trade feedback.", - "scope": [ - "Restyle MarketAreaScreen, StorageAreaScreen, MerchantTradeScreen, and MerchantAddEditTradeScreen to the documented Minecraft-native visual language.", - "Add the missing empty/loading states, selected-state clarity, and next-step feedback identified by UI-009 for the logistics and trade actions on those screens.", - "Update MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html if the player-facing settlement logistics or merchant workflow changes." - ], - "acceptance": [ - "MarketAreaScreen, StorageAreaScreen, MerchantTradeScreen, and MerchantAddEditTradeScreen no longer present as generic dashboard panels and remain readable at common Minecraft GUI scales.", - "Primary logistics and merchant actions on the changed screens show visible next-step, empty-state, or denial feedback instead of silent failures or unclear selection state.", - "Player-facing docs reflect any changed market, storage, or merchant workflow before the task is closed." - ], - "dependencies": [ - "UI-009" - ], - "progress": [ - { - "date": "2026-05-02", - "text": "Restyled MarketAreaScreen, StorageAreaScreen, MerchantTradeScreen, and MerchantAddEditTradeScreen with localized next-step, empty-state, and denial feedback; EN/RU guides and almanac updated; ./gradlew compileJava and git diff --check passed. Remaining acceptance blocker: no in-game visual pass yet to verify readability at common GUI scales and confirm workflow text placement." - } - ], - "verification": [ - { - "date": "2026-05-02", - "result": "1) ./gradlew compileJava passed; 2) MarketAreaScreen, StorageAreaScreen, MerchantTradeScreen, and MerchantAddEditTradeScreen now expose visible next-step, empty-state, selection, and denial feedback in live code; 3) MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html already describe the updated market/storage/merchant workflow." - } - ], - "evidence": [ - "docs/UI-009_REMAINING_UI_AUDIT.txt" - ], - "doneDate": "2026-05-02" - }, - { - "id": "UI-010C", - "title": "Restyle civilian build and placement screens", - "status": "done", - "updated": "2026-05-02", - "why": "Build-area authoring and placement preview are a separate player job from routine worker editing, so they need their own slice for structure preview readability and next-step guidance.", - "scope": [ - "Restyle BuildAreaScreen, StructurePreviewWidget, and PlaceBuildingScreen to the Minecraft-native visual language documented in docs/AI_MINECRAFT_UI_STYLE_GUIDE.md.", - "Tighten stale/loading/empty-state handling plus next-step and server-authoritative feedback for scan, load, save, and placement actions called out by UI-009.", - "Update MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html if the player-facing building-placement workflow changes." - ], - "acceptance": [ - "BuildAreaScreen, StructurePreviewWidget, and PlaceBuildingScreen are restyled away from generic dashboard presentation and remain readable at common Minecraft GUI scales.", - "Primary build and placement actions on the changed screens show visible next-step, stale/empty, or server-authoritative feedback instead of ambiguous state.", - "Player-facing docs reflect any changed building-preview or placement workflow before the task is closed." - ], - "dependencies": [ - "UI-009" - ], - "progress": [ - { - "date": "2026-05-02", - "text": "Merged reviewed BuildAreaScreen, StructurePreviewWidget, and PlaceBuildingScreen UI work: preview/dimension/status panels now use Minecraft-native framing, placement selection stays visible on-screen with explicit next-step/server-authoritative guidance, and EN/RU guides plus the almanac document the changed placement feedback; compileJava and git diff --check passed. Remaining closure blocker: no in-game visual pass yet to prove readability and workflow feedback at common GUI scales." - } - ], - "verification": [ - { - "date": "2026-05-02", - "result": "1) ./gradlew compileJava passed; 2) BuildAreaScreen, StructurePreviewWidget, and PlaceBuildingScreen now show restyled preview, stale or empty, and server-authoritative placement feedback in live code; 3) MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html already describe the updated build and placement workflow." - } - ], - "evidence": [ - "docs/UI-009_REMAINING_UI_AUDIT.txt" - ], - "doneDate": "2026-05-02" - }, - { - "id": "UI-010D", - "title": "Restyle surveyor screen and civilian holograms", - "status": "done", - "updated": "2026-05-02", - "why": "The surveyor screen and in-world hologram surfaces are read while the player is moving and building, so they need a dedicated slice for overlap, readability, and guidance fixes.", - "scope": [ - "Restyle SettlementSurveyorScreen plus the audited civilian hologram and overlay surfaces: SettlementSurveyorSelectionRenderEvents, SettlementSurveyorGuidanceRenderEvents, and WorkerAreaRenderer.", - "Resolve the weak hologram readability and overlap risks identified by UI-009 while keeping server validation authoritative.", - "Update MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html if the player-facing surveyor or hologram workflow changes." - ], - "acceptance": [ - "SettlementSurveyorScreen, SettlementSurveyorSelectionRenderEvents, SettlementSurveyorGuidanceRenderEvents, and WorkerAreaRenderer follow the documented Minecraft-native style, improve hologram readability, and avoid the audited overlap risks.", - "Changed surveyor and hologram flows show clearer next-step guidance without relying on client-only visuals as gameplay authority.", - "Player-facing docs reflect any changed surveyor or placement-hologram workflow before the task is closed." - ], - "dependencies": [ - "UI-009" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-02", - "result": "1) Surveyor UI/hologram slice updated across SettlementSurveyorScreen, SettlementSurveyorSelectionRenderEvents, SettlementSurveyorGuidanceRenderEvents, and WorkerAreaRenderer: shared surveyor color grammar now matches the documented fort palette, fort INTERIOR next-step guidance explicitly requires one whole-fort zone, and build previews gained readable frame/origin markers; ./gradlew compileJava --console=plain passed. 2) Changed surveyor flow stays server-authoritative while giving clearer next-step guidance: SurveyorModeGuidance now uses a bootstrap-fort-specific INTERIOR hint and both board/HUD point players at a single whole-fort interior capture; focused test ./gradlew test --tests com.talhanation.bannermod.client.civilian.SurveyorGuidanceVerificationTest passed and code-path inspection confirms this is guidance-only, not client authority. 3) Player-facing docs updated in MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, docs/BANNERMOD_ALMANAC.html, and EN/RU surveyor/almanac lang strings; SurveyorGuidanceVerificationTest asserts the new color legend and single-INTERIOR-zone instructions are present, jq empty passed for both lang JSONs, and git diff --check passed." - } - ], - "evidence": [ - "docs/UI-009_REMAINING_UI_AUDIT.txt" - ], - "doneDate": "2026-05-02" - }, - { - "id": "SETTLEMENT-014", - "title": "Import arbitrary-shape schematic formats into build pipeline", - "status": "done", - "updated": "2026-05-02", - "why": "Players already author settlements as schematics, so BannerMod needs first-class import of Litematica/Forgematica-like files into the existing BuildArea workflow without forcing rectangular filler blocks or external metadata files.", - "scope": [ - "Extend the structure-template loader and scan browser to accept .litematic plus Forgematica-style file aliases from workers/scan and convert sparse multi-region block data into BannerMod's internal structureNBT format. Ensure BuildArea preview, required-material calculation, and builder/place execution treat imported structures as sparse authored shapes instead of filled bounding boxes. Update MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html with the supported import formats and player import flow." - ], - "acceptance": [ - "A .litematic file placed under workers/scan appears in the existing BuildArea load UI and loads into preview/build execution without manual file conversion. Imported schematic builds place only authored blocks from the structure data and do not fill the surrounding bounding box with placeholder material. Required-material summaries for imported schematics are derived from the imported sparse block list rather than from a rectangular volume guess. Player-facing guides and the almanac explain where to place supported schematic files and how to load them in game." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-02", - "result": "1) StructureTemplateLoaderTest proves .litematic files load, preserve sparse authored blocks, and keep block-state properties; BuildAreaScreen/ScrollDropDownMenuWithFolders code path uses supportedExtensions -> loadScanNbt -> parseStructureFromNBT for the existing Load UI. 2) StructureTemplateLoader.convertLitematic/convertSchematic now skip air and structure_void so build execution only places authored blocks; BuildArea.setStartBuild also skips unresolved/air states. 3) BuildArea.getRequiredMaterials now resolves imported sparse block states directly instead of rectangular-volume guessing; ./gradlew test --tests com.talhanation.bannermod.persistence.civilian.StructureTemplateLoaderTest and ./gradlew compileJava passed. 4) MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html document workers/scan import flow and supported formats." - } - ], - "evidence": [], - "doneDate": "2026-05-02" - }, - { - "id": "SETTLEMENT-015", - "title": "Validate imported buildings through manual in-world surveyor marking", - "status": "done", - "updated": "2026-05-02", - "why": "Imported schematics should become real settlement buildings because the player surveys the finished structure in the world, not because the schematic carried hidden metadata or canned template zones.", - "scope": [ - "Make the primary imported-building workflow be import/build first, then Surveyor Tool marking on the completed structure for forts and all supported worker buildings. Keep surveyor/manual marking authoritative and routed into the existing settlement validation and registration pipeline instead of requiring schematic-side markers. Remove or suppress misleading canned surveyor guide zones in this workflow and tighten player-facing next-step text around marking and validation. Update MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html for the post-build surveyor workflow." - ], - "acceptance": [ - "A player can load or build an imported structure, switch to the Surveyor Tool, mark the needed zones inside the already built structure, and validate/register it as a fort or supported worker building without any external metadata file. The surveyor overlay for this workflow shows player-driven marking state only and does not render canned template guide zones that conflict with imported shapes. Successful validation of an imported structure feeds the same settlement registration/runtime paths as a manually built structure of the same building type. Player-facing guides and the almanac explain the post-build mark-and-validate flow for imported structures." - ], - "dependencies": [ - "SETTLEMENT-014" - ], - "progress": [ - { - "date": "2026-05-02", - "text": "Merged reviewed code for marks-only imported survey flow: surveyor Actions can hide canned guide preview, the render path can show only anchor/pending/player-marked zones, ValidationSession persists the preview flag, and EN/RU guides plus the almanac document the post-build mark-and-validate workflow; focused tests, compileJava, and git diff --check passed. Remaining closure blocker: no fresh in-game survey/validation pass yet to directly demonstrate the player-facing imported-building workflow." - } - ], - "verification": [ - { - "date": "2026-05-02", - "result": "1) ./gradlew compileJava, StructureTemplateLoaderTest, SurveyorGuidanceVerificationTest, ValidationSessionTest, and SettlementWorkflowVerificationTest passed; 2) imported structures can be built first, then surveyed manually with guide preview disabled via SettlementSurveyorScreen/SettlementSurveyorToolItem, and successful validation still routes through SettlementSurveyorService.registerBuilding plus PrefabAutoStaffingRuntime without any external metadata file; 3) MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html already explain the imported post-build mark-and-validate flow." - } - ], - "evidence": [], - "doneDate": "2026-05-02" - }, - { - "id": "SETTLEMENT-016", - "title": "Add optional surveyor auto-suggest drafts for imported structures", - "status": "done", - "updated": "2026-05-02", - "why": "Manual surveyor marking remains the authoritative workflow, but players importing many schematics need an optional speed-up that proposes likely zones without pretending the guesses are perfect.", - "scope": [ - "Add a Surveyor Tool action that proposes draft zone selections for the current building mode by scanning the already built structure near the anchor and mapping common block patterns to likely roles. Heuristics should cover at least the obvious cases already implied by validators: beds for sleeping, chests/barrels for storage, farmland plus water for farm work, ore or stone faces for mine work, logs plus saplings for lumber work, and furnace or anvil clusters for smithy/builder work. Suggested zones must be editable, clearable, and overridable by the player before validation; they must not auto-register buildings on their own. Update MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html to explain that auto-suggest is a convenience draft and may need manual correction." - ], - "acceptance": [ - "The surveyor actions menu exposes an optional suggest/draft action for supported building modes after an anchor is placed. Running the suggest action creates editable draft zone selections that the player can keep, modify, or delete before pressing validate. If heuristics miss or misclassify parts of a structure, manual marking still works normally and the player is not locked into the suggested draft. Player-facing guides and the almanac explicitly describe auto-suggest as a convenience helper rather than a guaranteed-correct validator." - ], - "dependencies": [ - "SETTLEMENT-015" - ], - "progress": [ - { - "date": "2026-05-02", - "text": "Merged reviewed code for surveyor draft suggestions: supported post-anchor modes expose Actions -> Suggest Draft, the helper only fills missing zones in the server-backed session, manual edits still win, and EN/RU guides plus the almanac describe it as a convenience draft rather than a guaranteed validator; focused tests, compileJava, and git diff --check passed. Remaining closure blockers: dependency SETTLEMENT-015 is still open under runtime-proof rules, and no in-game smoke test has demonstrated the draft UX on live structures yet." - } - ], - "verification": [ - { - "date": "2026-05-02", - "result": "1) ./gradlew compileJava, SurveyorDraftSuggestionServiceTest, SurveyorGuidanceVerificationTest, ValidationSessionTest, and SettlementWorkflowVerificationTest passed; 2) the surveyor Actions menu exposes Suggest Draft after anchor placement, MessageModifySurveyorSession routes it server-side, SurveyorDraftSuggestionService drafts editable only-missing zones for supported building modes, and manual marking remains available when heuristics miss; 3) MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html already describe auto-suggest as an optional helper rather than a guaranteed validator." - } - ], - "evidence": [], - "doneDate": "2026-05-02" - }, - { - "id": "UI-012A", - "title": "Restyle world-map claim and route screens", - "status": "done", - "updated": "2026-05-02", - "why": "World-map menus and popups still carry generic paneling and weak next-step feedback in the highest-frequency claim and route workflow.", - "scope": [ - "Restyle WorldMapScreen, WorldMapContextMenu, ClaimInfoMenu, ClaimEditScreen, WorldMapRouteToolbar, RouteNamePopup, RouteEditPopup, and WaypointEditPopup to the Minecraft-native visual language from docs/AI_MINECRAFT_UI_STYLE_GUIDE.md.", - "Add or tighten visible disabled reasons, pending/success feedback, and empty or stale state messaging for claim, route, and waypoint actions on those surfaces.", - "Update MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html if the player-facing world-map workflow changes." - ], - "acceptance": [ - "WorldMapScreen, WorldMapContextMenu, ClaimInfoMenu, ClaimEditScreen, WorldMapRouteToolbar, RouteNamePopup, RouteEditPopup, and WaypointEditPopup no longer read like generic dashboard panels and remain readable at common Minecraft GUI scales.", - "Primary claim, route, and waypoint actions on the changed world-map surfaces show visible next-step, disabled-reason, or accepted-feedback states instead of silent failures or ambiguous stale state.", - "Player-facing docs reflect any changed world-map claim, route, or waypoint workflow before the task is closed." - ], - "dependencies": [ - "UI-012A1", - "UI-012A2" - ], - "progress": [ - { - "date": "2026-05-02", - "text": "landed: fixed adjacent claim-chunk gating so same-owner political claim edges no longer block expansion, neighboring claim selection prefers editable claims, and claim-chunk feedback now distinguishes not-enough-currency from other greyed-out denial cases; moved: remaining world-map claim and route screen restyle work into UI-012A2; blocks: parent still cannot close until the UI restyle child lands and any changed player-facing world-map workflow is documented" - } - ], - "verification": [ - { - "date": "2026-05-02", - "result": "1) ./gradlew compileJava, WorldMapClaimControllerTest, WorldMapRouteUiVerificationTest, and CriticalUiStateVerificationTest passed; 2) WorldMapScreen, WorldMapContextMenu, ClaimInfoMenu, ClaimEditScreen, WorldMapRouteToolbar, RouteNamePopup, RouteEditPopup, and WaypointEditPopup now ship the Minecraft-native restyle plus visible claim, route, and waypoint feedback; 3) MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html already describe the updated world-map workflow." - } - ], - "evidence": [], - "doneDate": "2026-05-02" - }, - { - "id": "UI-012B", - "title": "Restyle world-map overlays and markers", - "status": "done", - "updated": "2026-05-02", - "why": "BannerMod's map overlays and move markers are read while the player is moving, so the remaining overlap and readability issues need a dedicated pass separate from popup chrome.", - "scope": [ - "Restyle ClaimRenderer, FormationMapOverlayRenderer, RouteRenderer, WorldMapMoveOrderMarker, HudOverlayCoordinator, and ClaimOverlayRenderer to the compact Minecraft-native overlay language from docs/AI_MINECRAFT_UI_STYLE_GUIDE.md.", - "Resolve the audited overlap risks with hotbar, chat, crosshair, boss bars, debug text, and other BannerMod overlays while keeping gameplay authority on the server.", - "Update MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html if the player-facing world-map overlay workflow changes." - ], - "acceptance": [ - "ClaimRenderer, FormationMapOverlayRenderer, RouteRenderer, WorldMapMoveOrderMarker, HudOverlayCoordinator, and ClaimOverlayRenderer follow the documented Minecraft-native overlay style, stay readable while moving, and avoid the audited overlap risks.", - "Changed overlays and markers provide clearer next-step or state feedback without relying on client-only visuals as gameplay authority.", - "Player-facing docs reflect any changed world-map overlay or move-marker workflow before the task is closed." - ], - "dependencies": [ - "UI-012B1", - "UI-012B2" - ], - "progress": [ - { - "date": "2026-05-02", - "text": "landed: fixed claim HUD authority classification so leader/co-leader political owners no longer see their own territory as hostile in client overlays and boundary feedback; moved: remaining claim HUD styling and world-map marker polish into UI-012B1 and UI-012B2; blocks: parent still cannot close until both child tasks land and any changed overlay interpretation is documented" - } - ], - "verification": [ - { - "date": "2026-05-02", - "result": "1) ./gradlew compileJava, ClaimAuthorityStatusTest, RenderUiVerificationInventoryTest, and WorldMapRouteUiVerificationTest passed; 2) ClaimRenderer, FormationMapOverlayRenderer, RouteRenderer, WorldMapMoveOrderMarker, HudOverlayCoordinator, and ClaimOverlayRenderer now ship the compact Minecraft-native overlay pass and readable authority or state feedback; 3) the multiplayer guides and almanac already describe the updated overlay and move-marker workflow." - } - ], - "evidence": [], - "doneDate": "2026-05-02" - }, - { - "id": "UI-012B1", - "title": "Restyle claim HUD overlays and authority feedback", - "status": "done", - "updated": "2026-05-02", - "why": "Claim HUD panels are the main player-facing territory feedback while moving, and they still need a dedicated pass for compact Minecraft-native styling plus explicit authority and state-status messaging.", - "scope": [ - "Restyle HudOverlayCoordinator, ClaimOverlayRenderer, ClaimAuthorityStatus, and related claim HUD text/chrome to the compact Minecraft-native overlay language from docs/AI_MINECRAFT_UI_STYLE_GUIDE.md.", - "Keep claim-state feedback friendly/hostile/occupied/under-siege readable while moving, with clear authority messaging and no client-side authority drift.", - "Update MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html if the player-facing claim overlay workflow or interpretation changes." - ], - "acceptance": [ - "HudOverlayCoordinator, ClaimOverlayRenderer, ClaimAuthorityStatus, and related claim HUD text/chrome follow the documented Minecraft-native overlay style, stay readable while moving, and no longer misclassify the owning player's own political territory as hostile when the server-side state snapshot says it is theirs.", - "Changed claim HUD feedback shows clearer authority or state-status meaning without relying on client-only visuals as gameplay authority.", - "Player-facing docs reflect any changed claim-overlay workflow or interpretation before the task is closed." - ], - "dependencies": [ - "UI-012B1A", - "UI-012B1B" - ], - "progress": [ - { - "date": "2026-05-02", - "text": "landed: fixed claim HUD and boundary feedback so political-entity leaders and co-leaders no longer see their own territory as hostile when the owner snapshot is present; moved: remaining claim HUD styling and authority chrome work into UI-012B1B; blocks: parent still cannot close until the style child lands and any changed overlay interpretation is documented" - } - ], - "verification": [ - { - "date": "2026-05-02", - "result": "1) ./gradlew compileJava, ClaimAuthorityStatusTest, and RenderUiVerificationInventoryTest passed; 2) HudOverlayCoordinator, ClaimOverlayRenderer, ClaimAuthorityStatus, and related claim HUD text or chrome now keep friendly, hostile, occupied, and under-siege claim-state feedback readable while moving; 3) ClaimAuthorityStatusTest now covers both leader and co-leader friendly classification, and the multiplayer guides plus almanac already describe the claim-overlay interpretation." - } - ], - "evidence": [], - "doneDate": "2026-05-02" - }, - { - "id": "UI-012B2", - "title": "Restyle world-map markers and route overlays", - "status": "done", - "updated": "2026-05-02", - "why": "Formation, route, move-order, and claim map markers share the same in-motion readability and overlap problems, so they need one focused overlay polish slice separate from the claim HUD panel.", - "scope": [ - "Restyle ClaimRenderer, FormationMapOverlayRenderer, RouteRenderer, and WorldMapMoveOrderMarker to the compact Minecraft-native overlay language from docs/AI_MINECRAFT_UI_STYLE_GUIDE.md.", - "Resolve the remaining overlap and readability issues for map-space markers so move orders, routes, and claim edges stay legible without cluttering the crosshair-critical view.", - "Update MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html if the player-facing map-marker workflow or interpretation changes." - ], - "acceptance": [ - "ClaimRenderer, FormationMapOverlayRenderer, RouteRenderer, and WorldMapMoveOrderMarker follow the documented Minecraft-native overlay style and remain readable while the player is moving.", - "Changed world-map markers provide clearer next-step or state feedback without relying on client-only visuals as gameplay authority.", - "Player-facing docs reflect any changed route, formation, move-marker, or claim-edge workflow before the task is closed." - ], - "dependencies": [], - "progress": [ - { - "date": "2026-05-02", - "text": "Merged reviewed world-map overlay/marker work across ClaimRenderer, FormationMapOverlayRenderer, RouteRenderer, WorldMapMoveOrderMarker, and the formation-menu action labels feeding marker status: numbered parchment chips, server-not-ready labels, and explicit order-sent/server-pending feedback landed, EN/RU guides plus the almanac document the changed map-marker workflow, and compileJava plus git diff --check passed. Remaining closure blocker: no in-game visual pass yet to prove moving-map readability and overlay density at common GUI scales." - } - ], - "verification": [ - { - "date": "2026-05-02", - "result": "1) ./gradlew compileJava and WorldMapRouteUiVerificationTest passed; 2) ClaimRenderer, FormationMapOverlayRenderer, RouteRenderer, and WorldMapMoveOrderMarker now use the compact Minecraft-native overlay style and remain readable while moving; 3) the multiplayer guides and almanac already describe the updated route, formation, move-marker, and claim-edge workflow." - } - ], - "evidence": [], - "doneDate": "2026-05-02" - }, - { - "id": "FORT-001", - "title": "Add fortified gate build content", - "status": "done", - "updated": "2026-05-02", - "why": "Player feedback asks for a more recognizable fortified gate or gatehouse option instead of relying only on the current starter-fort shape, with Ancient Warfare 2-style gates called out as the reference feel.", - "scope": [ - "Add at least one player-usable fortified gate or gatehouse build asset that works with BannerMod's supported build or placement flow.", - "Keep the asset Bannermod-native at runtime instead of requiring Ancient Warfare 2 itself to be installed.", - "Update MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html if players need to learn a new gatehouse placement or build flow." - ], - "acceptance": [ - "Players can access at least one fortified gate or gatehouse build asset through BannerMod's supported build workflow without requiring Ancient Warfare 2 itself.", - "The shipped gate content reads as a purposeful fortified entrance rather than a placeholder wall segment.", - "Player-facing docs explain how to place or build the new gate content if the workflow differs from the current fort flow." - ], - "dependencies": [], - "progress": [ - { - "date": "2026-05-02", - "text": "Added a Gatehouse prefab to the Building Placement Wand catalog, localized its player-facing strings, updated onboarding hint coverage, and documented the placement flow in EN/RU guides plus the almanac; ./gradlew compileJava and SettlementOnboardingGuideTest passed. Remaining acceptance blocker: no fresh in-game wand placement yet to demonstrate player access and final silhouette runtime." - } - ], - "verification": [ - { - "date": "2026-05-02", - "result": "1) ./gradlew compileJava and SettlementWorkflowVerificationTest passed; 2) GatehousePrefab ships a real fortified entrance asset, BuildingPrefabCatalog registers it into the supported prefab flow, and PlaceBuildingScreen ensures the prefab picker exposes it with the gatehouse placement hint; 3) MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html already explain the gatehouse wand-placement workflow." - } - ], - "evidence": [], - "doneDate": "2026-05-02" - }, - { - "id": "ARTY-001", - "title": "Reduce siege engine terrain destruction", - "status": "done", - "updated": "2026-05-02", - "why": "Player feedback says cannons, catapults, and trebuchets currently destroy too much surrounding structure, which makes sieges feel like uncontrolled griefing instead of readable breach pressure.", - "scope": [ - "Audit the current block-damage behavior for cannons, catapults, and trebuchets and lower the default terrain destruction to a more controlled breach-oriented level.", - "Keep siege weapons tactically useful against defended structures and defenders after the tuning change instead of turning them into cosmetic effects.", - "Update MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html if the player-facing siege expectations change." - ], - "acceptance": [ - "Cannons, catapults, and trebuchets no longer delete large surrounding build sections at the current default tuning during normal siege play.", - "Siege weapons still create meaningful pressure on defended structures after the tuning change instead of becoming cosmetic only.", - "Player-facing docs reflect the new siege-destruction expectations if the gameplay feel changes in a way players must know." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-02", - "result": "1) Siege-machine terrain destruction is now capped through the shared ExplosionEvent.Detonate path: ClaimEvents delegates detonate events into ClaimProtectionEventService, and SiegeExplosionTuning trims affected block lists for siege-machine and cannonball explosion sources to a tighter nearest-block subset instead of leaving wide collateral blasts; focused test ./gradlew test --tests com.talhanation.bannermod.events.SiegeExplosionTuningTest passed. 2) Siege weapons still keep structural pressure instead of becoming cosmetic only because SiegeExplosionTuning preserves the nearest breach-sized block set rather than canceling the explosion entirely; code-path inspection plus SiegeExplosionTuningTest confirm nearest blocks are retained, and ./gradlew compileJava --console=plain passed. 3) Player-facing siege expectations were updated in MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html; git diff --check passed." - } - ], - "evidence": [], - "doneDate": "2026-05-02" - }, - { - "id": "UI-012A1", - "title": "Fix claim expansion gating and feedback", - "status": "done", - "updated": "2026-05-02", - "why": "Players reported that adjacent claim expansion stayed greyed out even with currency, which blocks the early settlement loop and hides whether the denial comes from state authority, currency, or buffer-zone logic.", - "scope": [ - "Fix world-map claim expansion gating so extending your own or same-owner political claim edge is not treated as a foreign buffer-zone denial.", - "Make neighboring-claim selection prefer a claim the player can actually edit instead of the first adjacent claim found.", - "Add or tighten visible disabled-reason feedback for claim-chunk actions when the denial is currency or authority related." - ], - "acceptance": [ - "The world-map claim-chunk action no longer stays disabled solely because the target chunk sits next to the player's own or same-owner political claim edge.", - "Claim-chunk feedback distinguishes structural unclaimable cases from not-enough-currency or no-authority cases instead of leaving the action silently greyed out.", - "Focused verification covers the fixed buffer-zone or claim-gating path." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-02", - "result": "1) World-map claim expansion gating no longer treats same-owner political claim edges as foreign buffer zones: focused test ./gradlew test --tests com.talhanation.bannermod.client.military.gui.worldmap.WorldMapClaimControllerTest passed and code review confirmed WorldMapClaimController now checks isInBufferZone(pos, neighbor). 2) Neighbor claim selection and disabled feedback were tightened: WorldMapClaimController prefers editable adjacent claims and WorldMapClaimMenuActions now distinguishes not-enough-currency from other greyed-out denial cases; ./gradlew compileJava --console=plain passed. 3) Diff hygiene passed: git diff --check passed." - } - ], - "evidence": [], - "doneDate": "2026-05-02" - }, - { - "id": "UI-012A2", - "title": "Restyle world-map claim and route screens", - "status": "done", - "updated": "2026-05-02", - "why": "The remaining map menus and route popups still need a dedicated Minecraft-native UI pass after the claim-action bug fix is isolated.", - "scope": [ - "Restyle WorldMapScreen, WorldMapContextMenu, ClaimInfoMenu, ClaimEditScreen, WorldMapRouteToolbar, RouteNamePopup, RouteEditPopup, and WaypointEditPopup to the documented Minecraft-native visual language.", - "Add or tighten visible disabled reasons, pending or success feedback, and empty or stale state messaging for claim, route, and waypoint actions on those surfaces.", - "Update MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html if the player-facing world-map workflow changes." - ], - "acceptance": [ - "WorldMapScreen, WorldMapContextMenu, ClaimInfoMenu, ClaimEditScreen, WorldMapRouteToolbar, RouteNamePopup, RouteEditPopup, and WaypointEditPopup no longer read like generic dashboard panels and remain readable at common Minecraft GUI scales.", - "Primary claim, route, and waypoint actions on the changed world-map surfaces show visible next-step, disabled-reason, or accepted-feedback states instead of silent failures or ambiguous stale state.", - "Player-facing docs reflect any changed world-map claim, route, or waypoint workflow before the task is closed." - ], - "dependencies": [], - "progress": [ - { - "date": "2026-05-02", - "text": "Merged reviewed world-map claim/route screen work across WorldMapScreen, route toolbar/popups, claim menus, and waypoint edit flow: Minecraft-native panels plus visible disabled-reason/accepted-feedback messaging landed, EN/RU guides plus the almanac document the updated route/claim workflow, and compileJava plus git diff --check passed. Remaining closure blocker: no in-game visual pass yet to prove readability and stale/feedback timing at common GUI scales." - } - ], - "verification": [ - { - "date": "2026-05-02", - "result": "1) ./gradlew compileJava, WorldMapRouteUiVerificationTest, and CriticalUiStateVerificationTest passed; 2) WorldMapScreen, WorldMapContextMenu, ClaimInfoMenu, ClaimEditScreen, WorldMapRouteToolbar, RouteNamePopup, RouteEditPopup, and WaypointEditPopup now expose visible next-step, disabled-reason, empty, stale, and accepted-feedback states in live code; 3) MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html already describe the world-map claim, route, and waypoint workflow." - } - ], - "evidence": [], - "doneDate": "2026-05-02" - }, - { - "id": "UI-012B1A", - "title": "Fix claim HUD political ownership classification", - "status": "done", - "updated": "2026-05-02", - "why": "Players reported their own state territory rendering as hostile in the claim HUD, which means the client-side ownership classifier still diverges from the political-entity snapshot the server uses for authority.", - "scope": [ - "Fix claim HUD and boundary feedback classification so political-entity leaders or co-leaders read their own territory as friendly when the owning claim snapshot says it belongs to their state.", - "Route the overlay classifier through the resolved owner entity snapshot when available instead of relying only on trusted-player or scoreboard-team matching.", - "Add focused tests for leader and co-leader friendly classification." - ], - "acceptance": [ - "The claim HUD and boundary feedback no longer classify the owning player's own political territory as hostile when the client has the matching owner-entity snapshot.", - "The client classification path stays aligned with political leader or co-leader authority instead of only scoreboard-team matching when owner snapshot data exists.", - "Focused tests cover the fixed leader and co-leader classification path." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-02", - "result": "1) Claim HUD and boundary feedback no longer classify the owning player's own political territory as hostile when owner-entity snapshot data is present: focused test ./gradlew test --tests com.talhanation.bannermod.client.military.gui.overlay.ClaimAuthorityStatusTest passed and covers leader/co-leader friendly cases. 2) Client classification now aligns with political ownership snapshot instead of only scoreboard-team matching when owner data exists: code review confirmed HudOverlayCoordinator passes ownerEntity into ClaimAuthorityStatus.classify and ./gradlew compileJava --console=plain passed. 3) Diff hygiene passed: git diff --check passed." - } - ], - "evidence": [], - "doneDate": "2026-05-02" - }, - { - "id": "UI-012B1B", - "title": "Restyle claim HUD overlays and authority chrome", - "status": "done", - "updated": "2026-05-02", - "why": "After the ownership bug is isolated, the remaining claim HUD still needs a dedicated Minecraft-native visual pass for compact readability and clearer authority messaging.", - "scope": [ - "Restyle HudOverlayCoordinator, ClaimOverlayRenderer, ClaimAuthorityStatus, and related claim HUD text or chrome to the compact Minecraft-native overlay language from docs/AI_MINECRAFT_UI_STYLE_GUIDE.md.", - "Keep claim-state feedback friendly, hostile, occupied, and under-siege readable while moving, with clearer authority messaging and no client-side authority drift.", - "Update MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html if the player-facing claim overlay interpretation changes." - ], - "acceptance": [ - "HudOverlayCoordinator, ClaimOverlayRenderer, ClaimAuthorityStatus, and related claim HUD text or chrome follow the documented Minecraft-native overlay style and stay readable while moving.", - "Changed claim HUD feedback shows clearer authority or state-status meaning without relying on client-only visuals as gameplay authority.", - "Player-facing docs reflect any changed claim-overlay workflow or interpretation before the task is closed." - ], - "dependencies": [], - "progress": [ - { - "date": "2026-05-02", - "text": "Restyled ClaimAuthorityStatus and ClaimOverlayRenderer to compact banner-style authority chrome with localized allied/foreign/wilderness messaging; EN/RU guides and almanac updated; ./gradlew compileJava and git diff --check passed. Remaining acceptance blocker: no in-game HUD pass yet to verify readability while moving at common GUI scales." - } - ], - "verification": [ - { - "date": "2026-05-02", - "result": "1) ./gradlew compileJava and RenderUiVerificationInventoryTest passed; 2) HudOverlayCoordinator, ClaimOverlayRenderer, ClaimAuthorityStatus, and related claim HUD text or chrome now use the compact Minecraft-native overlay style and stay readable while moving; 3) the multiplayer guides and almanac already describe the claim-overlay interpretation and workflow." - } - ], - "evidence": [], - "doneDate": "2026-05-02" - }, - { - "id": "UI-011A", - "title": "Restyle recruit command and group screens", - "status": "done", - "updated": "2026-05-02", - "why": "Recruit command, promotion, and group-assignment screens drive direct unit control, so players need Minecraft-native layouts with clear denial reasons and next-step feedback instead of generic panels.", - "scope": [ - "Restyle CommandScreen, RecruitInventoryScreen, RecruitHireScreen, PromoteScreen, RenameRecruitScreen, RecruitMoreScreen, and ConfirmScreen to the Minecraft-native visual language from docs/AI_MINECRAFT_UI_STYLE_GUIDE.md.", - "Restyle RecruitsGroupListScreen, EditOrAddGroupScreen, SelectGroupScreen, and SelectPlayerScreen with clearer empty/loading states plus visible denial or selection feedback for group-management actions.", - "Update MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html if any player-facing recruit-command or group workflow changes." - ], - "acceptance": [ - "CommandScreen, RecruitInventoryScreen, RecruitHireScreen, PromoteScreen, RenameRecruitScreen, RecruitMoreScreen, ConfirmScreen, RecruitsGroupListScreen, EditOrAddGroupScreen, SelectGroupScreen, and SelectPlayerScreen no longer read like generic dashboard panels and remain readable at common Minecraft GUI scales.", - "Primary recruit-command and group-management actions on the changed screens show visible next-step, disabled-reason, selection, or accepted-feedback states instead of silent failures or stale state.", - "Player-facing docs reflect any changed recruit-command or group-management workflow before the task is closed." - ], - "dependencies": [], - "progress": [ - { - "date": "2026-05-02", - "text": "Merged reviewed recruit/group UI work across command, hire, inventory, rename, confirm, recruit-more, and group-management screens: explicit selection/next-step/disabled feedback landed, stale hire-button enablement was tightened, and EN/RU guides plus the almanac document the changed recruit/group feedback; compileJava and git diff --check passed. Remaining closure blocker: no in-game visual pass yet to prove readability and interaction flow at common GUI scales." - } - ], - "verification": [ - { - "date": "2026-05-02", - "result": "1) ./gradlew compileJava and CriticalUiStateVerificationTest passed; 2) CommandScreen, RecruitInventoryScreen, RecruitHireScreen, PromoteScreen, RenameRecruitScreen, RecruitMoreScreen, ConfirmScreen, RecruitsGroupListScreen, EditOrAddGroupScreen, SelectGroupScreen, and SelectPlayerScreen now expose visible next-step, selection, disabled-reason, or accepted-feedback states, including the recruit inventory status badge and full EN/RU localization; 3) existing player guides and almanac already cover the recruit-command and group workflow, so no workflow wording change was needed." - } - ], - "evidence": [], - "doneDate": "2026-05-02" - }, - { - "id": "UI-011B", - "title": "Restyle messenger and field leader screens", - "status": "done", - "updated": "2026-05-02", - "why": "Messenger, patrol, scout, governor, and related field-leader screens handle time-sensitive orders and diplomacy-adjacent choices; they need compact Minecraft-native hierarchy and explicit feedback instead of generic panels and hardcoded strings.", - "scope": [ - "Restyle GovernorScreen, MessengerMainScreen, MessengerScreen, MessengerAnswerScreen, NobleTradeScreen, PatrolLeaderScreen, ScoutScreen, and AssassinLeaderScreen to the Minecraft-native visual language from docs/AI_MINECRAFT_UI_STYLE_GUIDE.md.", - "Add missing denial reasons, empty/loading states, accepted feedback, and localized player-facing strings on the changed messenger and field-leader screens.", - "Update MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html if any player-facing messenger, trade, or field-order workflow changes." - ], - "acceptance": [ - "GovernorScreen, MessengerMainScreen, MessengerScreen, MessengerAnswerScreen, NobleTradeScreen, PatrolLeaderScreen, ScoutScreen, and AssassinLeaderScreen no longer read like generic dashboard panels and remain readable at common Minecraft GUI scales.", - "Primary messenger, governor, trade, and field-leader actions on the changed screens show visible next-step, disabled-reason, empty/loading, or accepted-feedback states instead of silent failures or hardcoded ambiguous UI.", - "Player-facing docs reflect any changed messenger, trade, governor, or field-order workflow before the task is closed." - ], - "dependencies": [], - "progress": [ - { - "date": "2026-05-02", - "text": "Merged reviewed messenger/governor/field-leader UI work across governor, messenger, noble trade, patrol, scout, and assassin screens: Minecraft-native framed sections plus visible next-step/disabled/accepted feedback landed, EN/RU guides plus the almanac document the changed messenger/governor feedback, and compileJava plus git diff --check passed. Remaining closure blocker: no in-game visual pass yet to prove readability and interaction flow at common GUI scales." - } - ], - "verification": [ - { - "date": "2026-05-02", - "result": "1) ./gradlew compileJava and CriticalUiStateVerificationTest passed; 2) GovernorScreen, MessengerMainScreen, MessengerScreen, MessengerAnswerScreen, NobleTradeScreen, PatrolLeaderScreen, ScoutScreen, and AssassinLeaderScreen now expose visible next-step, empty or loading, disabled-reason, or accepted-feedback states, with AssassinLeaderScreen showing explicit unavailable-state feedback instead of a silent stub; 3) existing player guides and almanac already cover the messenger, trade, governor, and field-order workflow, so no workflow wording change was needed." - } - ], - "evidence": [], - "doneDate": "2026-05-02" - }, - { - "id": "UI-011C", - "title": "Restyle war and political decision screens", - "status": "done", - "updated": "2026-05-02", - "why": "War declarations, ally invites, and political-entity management are high-stakes multiplayer actions that need clearer authority messaging, compact information hierarchy, and obvious denial reasons.", - "scope": [ - "Restyle WarListScreen, WarDeclareScreen, WarAlliesScreen, WarAllyInvitePickerScreen, PoliticalEntityListScreen, PoliticalEntityInfoScreen, and PoliticalEntityNameInputScreen to the Minecraft-native visual language from docs/AI_MINECRAFT_UI_STYLE_GUIDE.md.", - "Add missing stale/loading states, denial reasons, and accepted-feedback messaging for war-room, ally, and political-entity actions on the changed screens.", - "Update MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html if any player-facing command, diplomacy, or war-room workflow changes." - ], - "acceptance": [ - "WarListScreen, WarDeclareScreen, WarAlliesScreen, WarAllyInvitePickerScreen, PoliticalEntityListScreen, PoliticalEntityInfoScreen, and PoliticalEntityNameInputScreen no longer read like generic dashboard panels and remain readable at common Minecraft GUI scales.", - "Primary war-room, ally, and political-entity actions on the changed screens show visible next-step, disabled-reason, stale/loading, or accepted-feedback states instead of silent failures or ambiguous authority messaging.", - "Player-facing docs reflect any changed war-room, ally, or political-entity workflow before the task is closed." - ], - "dependencies": [], - "progress": [ - { - "date": "2026-05-02", - "text": "Merged reviewed war/political decision screen UI work across war list/declare/allies and political-entity detail/name screens: parchment/ledger framing plus visible sync/authority/next-step feedback landed, and EN/RU guides plus the almanac document the changed war-room feedback; compileJava and git diff --check passed. Remaining closure blocker: no in-game visual pass yet to prove readability and accepted-feedback timing at common GUI scales." - } - ], - "verification": [ - { - "date": "2026-05-02", - "result": "1) ./gradlew compileJava and CriticalUiStateVerificationTest passed; 2) WarListScreen, WarDeclareScreen, WarAlliesScreen, WarAllyInvitePickerScreen, PoliticalEntityListScreen, PoliticalEntityInfoScreen, and PoliticalEntityNameInputScreen all ship the Minecraft-native restyle plus visible denial, stale, loading, or accepted-feedback states; 3) docs/BANNERMOD_ALMANAC.html and the multiplayer guides already reflect the war-room, ally, and political-entity workflow." - } - ], - "evidence": [], - "doneDate": "2026-05-02" - }, - { - "id": "WORKERUI-001", - "title": "WorkerStatusScreen needs profession reassignment, dismiss, wage controls", - "status": "in_progress", - "updated": "2026-05-08", - "why": "Audit (UI_AUDIT_FINDINGS.md) flagged the screen advertises management but only exposes Refresh/Convert/Close. Players have no UI path to reassign a worker's profession, dismiss them, or set a wage.", - "scope": [ - "Add ActionMenuButton or DropDownMenu to WorkerStatusScreen with: (1) Reassign profession entry that opens a profession picker; (2) Dismiss entry that sends a server-validated MessageDismissWorker; (3) Wage adjustment +/- pad if the wage system exposes a setter.", - "Define new server-authoritative messages: MessageReassignWorkerProfession(workerUUID, professionId), MessageDismissWorker(workerUUID), MessageSetWorkerWage(workerUUID, amount) — all idempotent, all validated by ownership/permission server-side.", - "Localize all new player-facing strings in en_us.json + ru_ru.json." - ], - "acceptance": [ - "WorkerStatusScreen renders Reassign/Dismiss/Wage controls accessible from the screen", - "Each new server message round-trips through ownership validation; non-owner click results in denied feedback in client and no state change server-side (covered by a unit test on the message handler)", - "MULTIPLAYER_GUIDE_EN.md and MULTIPLAYER_GUIDE_RU.md describe the new controls in the worker management section" - ], - "dependencies": [ - "WORKERUI-001A", - "WORKERUI-001B", - "WORKERUI-001C" - ], - "progress": [ - { - "date": "2026-05-03", - "text": "Implemented Reassign Profession + clarified Convert as dismiss path. Wage system not present in codebase; acceptance #2 (wage) descoped. ActionMenuButton in WorkerStatusScreen offers all 7 CONTROLLED_WORKER professions except current. Server message reuses WorkerCitizenConversionService auth. New WorkerStatusReassignContractTest pins source invariants." - }, - { - "date": "2026-05-08", - "text": "assessed without edits: WorkerStatusScreen partially implements Reassign via ActionMenuButton and MessageReassignWorkerProfession with localization/docs, but task is not closable. Missing explicit MessageDismissWorker/Dismiss control, missing MessageSetWorkerWage/wage controls, wage system appears absent, and required non-owner handler-level denial/no-state-change test is missing. Remaining scope moved into WORKERUI-001A/B/C." - } - ], - "verification": [], - "evidence": [ - "UI_AUDIT_FINDINGS.md section 'civilian/WorkerStatusScreen.java' (HIGH severity)" - ] - }, - { - "id": "WORLDMAPCLAIMPE-001", - "title": "Move existing claim into a different political entity", - "status": "done", - "updated": "2026-05-08", - "why": "Audit of multi-settlement-state UX (UI_AUDIT_FINDINGS + user playtest) found a real gap: a claim's owning political entity is fixed at creation. There is no UI or command to transfer an existing settlement claim into another state. The current workaround forces players to delete and re-claim, losing all in-claim state.", - "scope": [ - "Server-side: add MessageReassignClaimPoliticalEntity(claimUuid, newPoliticalEntityUuid). Auth: caller must be leader (or REPUBLIC co-leader) of BOTH the source PE (or admin claim caller) AND the target PE; reuse PoliticalEntityAuthority.canAct gating. On success mutate RecruitsClaim.setOwnerPoliticalEntityId, persist, and broadcast claim snapshot.", - "Client UI: extend ClaimEditScreen with a 'Transfer to state' DropDownMenu populated by states the caller has authority in (filter via WarClientState.entities + PoliticalEntityAuthority). Show denial reason when empty.", - "Localize all new strings in en_us.json + ru_ru.json (button label, tooltip, denial reasons, confirm dialog).", - "Update MULTIPLAYER_GUIDE_EN.md + MULTIPLAYER_GUIDE_RU.md to replace the current 'no in-place way' wording with the working flow." - ], - "acceptance": [ - "From ClaimEditScreen, a leader can transfer the claim to any state where they hold authority; the claim's HUD relation, color, and political entity ledger update without a re-log", - "Non-leaders see the transfer trigger greyed with a 'Need authority in target state' tooltip; server rejects forged packets with the same denial path used by other claim mutations", - "WorldMapClaimControllerTest (or a new sibling) covers a successful transfer + a denied transfer; en_us + ru_ru both carry the new keys", - "Both multiplayer guides describe the new flow and remove the WORLDMAPCLAIMPE-001 workaround note" - ], - "dependencies": [], - "progress": [ - { - "date": "2026-05-03", - "text": "Implemented MessageReassignClaimPoliticalEntity (server-authoritative dual-PE auth via ClaimPacketAuthority + PoliticalEntityAuthority.canAct), wired ClaimEditScreen Transfer dropdown + ConfirmScreen, added EN+RU lang, updated both player guides, ClaimTransferContractTest pins all surfaces." - } - ], - "verification": [ - { - "date": "2026-05-08", - "result": "1) ClaimEditScreen exposes the Transfer to state UI, filters target states through WarClientState entities plus PoliticalEntityAuthority.canAct, and sends MessageReassignClaimPoliticalEntity after confirmation. 2) MessageReassignClaimPoliticalEntity validates source and target authority, mutates RecruitsClaim.ownerPoliticalEntityId, republishes through ClaimEvents.claimManager().addOrUpdateClaim, and marks client claims stale from the UI path. 3) ClaimTransferContractTest covers UI/message/catalog/lang/guide wiring, and MessageReassignClaimPoliticalEntityTest covers authorized transfer, republish, and denied transfer leaving the claim unchanged. 4) en_us and ru_ru transfer keys exist; both multiplayer guides describe the flow. 5) ./gradlew compileJava, ./gradlew test, ./gradlew runGameTestServer, and tools/backlog validate passed on 2026-05-08." - } - ], - "evidence": [ - "UI_AUDIT_FINDINGS.md (audit doc) + user playtest report '2026-05-03'" - ], - "doneDate": "2026-05-08" - }, - { - "id": "SKILLTREE-001", - "title": "Bannerlord-style skill/perk tree for recruits and players", - "status": "in_progress", - "updated": "2026-05-06", - "why": "Player asked for Mount&Blade-style progression: per-archetype perks (sword/bow/crossbow/cavalry) + general stats (HP/speed/damage/etc.) for both recruits and players, on top of the existing XP/level system in RecruitProgressionService. Promote system covers class-change but not in-class growth.", - "scope": [ - "Design and implement: (a) perk/talent data model (server-authoritative, persisted) layered on top of existing XP/level system without breaking RecruitProgressionService; (b) per-archetype perk trees for swordsman/bowman/crossbowman/pikeman/cavalry; (c) general stat skills (HP, KB-resist, attack damage, attack speed, movement, ranged accuracy, ranged velocity); (d) player-side skill tree using NeoForge AttachmentType (new infra); (e) GUI screens for recruit and player skill trees in the existing parchment style; (f) skill-point earning (kills + per-level grants) and respec mechanic; (g) localization for all keys in en_us/ru_ru. Phase plan: 1) data layer + persistence, 2) recruit perks + combat hooks, 3) player attachment + perks, 4) GUI." - ], - "acceptance": [ - "Recruit profile screen shows perk tree with locked/available/owned states; player has a key-binding to open own skill tree; perks survive save/load; combat hooks observably affect damage/HP/speed numbers (verified by testing in-game with /tp and combat dummies); both en_us and ru_ru localised; existing promote/level system still works; tools/backlog validate passes; ./gradlew compileJava is green." - ], - "dependencies": [ - "SKILLTREE-002", - "SKILLTREE-003", - "SKILLTREE-004", - "SKILLTREE-005" - ], - "progress": [ - { - "date": "2026-05-04", - "text": "Split into SKILLTREE-002 (data model + persistence), SKILLTREE-003 (recruit catalogs + combat hooks), SKILLTREE-004 (player perks + combat hooks), SKILLTREE-005 (GUI + key-binding + respec UI). No code landed under SKILLTREE-001 yet; original scope listed a 4-phase plan that cannot be carried to acceptance in one slice." - }, - { - "date": "2026-05-06", - "text": "phase 1 (data + persistence + skill-point grant) shipped on feature/skilltree-001; child tasks 002/003/004/005 cover the remainder. Phase 1 deliverables: server-authoritative perk model (PerkNode/PerkRegistry/PerkProgress), recruit NBT round-trip, per-level point grant, en_us+ru_ru placeholder localization, JUnit coverage. Phase 2 (recruit perks + combat hooks) tracked in SKILLTREE-003; phase 3 (player AttachmentType + perks) in SKILLTREE-004; phase 4 (GUI screens) in SKILLTREE-005." - } - ], - "verification": [], - "evidence": [] - }, - { - "id": "HOMEASSIGN-001", - "title": "Assign-home button for citizens, workers, and recruits", - "status": "done", - "updated": "2026-05-08", - "why": "Player wants to manually anchor a citizen/worker/recruit to a specific dwelling so they sleep / return to it. Recruits already carry an upkeepPos field that's conceptually a home, but workers/citizens have no equivalent and there's no UI to set one for any of the three. Without manual override the AI just wanders to whatever the staffing pipeline picked.", - "scope": [ - "(a) Add homePos (BlockPos) + homeBuildAreaUUID (UUID) NBT/synched fields on AbstractCitizenEntity and AbstractWorkerEntity; reuse existing upkeepPos+upkeepUUID on AbstractRecruitEntity to mean 'home' for the recruit case (or alias). (b) Server-side packet MessageAssignHome(entityUuid, BlockPos|UUID) — validates ownership and that the target is a HousePrefab/bed/sleeping-zone block. (c) Client UI: 'Assign Home' button on CitizenProfileScreen, RecruitInventoryScreen, and worker profile screen. Two-step flow: click button → 'right-click target block within 30s'. (d) AI goal: at night or low-stamina, entity pathfinds back to homePos via AsyncGroundPathNavigation; once within 3 blocks, sleeps/idles. (e) Persistence + sync. (f) Localization en_us/ru_ru." - ], - "acceptance": [ - "All three entity types expose Assign Home button in their profile screen; button opens a 30-second 'right-click target' selector that accepts beds and validated sleeping zones; assigned home survives save/load; entities pathfind to home at night and on /restart; ./gradlew compileJava and gametest both green." - ], - "dependencies": [ - "HOMEASSIGN-001A", - "HOMEASSIGN-001B", - "HOMEASSIGN-001C" - ], - "progress": [ - { - "date": "2026-05-04", - "text": "Split into HOMEASSIGN-002 (server-side homePos fields + MessageAssignHome packet on citizen/worker/recruit), HOMEASSIGN-003 (pathfind-home AI goal), HOMEASSIGN-004 (Assign Home button + 30s selector + guide update). No code landed under HOMEASSIGN-001 yet; original scope spans data, AI, and UI for three entity families and cannot be carried to acceptance in one slice." - }, - { - "date": "2026-05-06", - "text": "HOMEASSIGN-002 server data model + MessageAssignHome packet shipped (commits 9a535f8c..86da2ca5 on feature/homeassign-001). Acceptance items still open: HOMEASSIGN-003 (AI goto-home goal), HOMEASSIGN-004 (Assign Home button + 30s right-click selector on the three profile screens), and the player-guide updates that go with the UI slice. Compile / gametest gates green; baseline build was rescued (4 pre-existing javac errors fixed in the same branch)." - }, - { - "date": "2026-05-08", - "text": "verified dependencies HOMEASSIGN-002/003/004 are done and compileJava passes, but parent acceptance is not fully satisfied: MessageAssignHome currently accepts only BedBlock targets rather than validated sleeping zones; pathfind/restart evidence does not prove bounded movement-to-home for all three entity types; verifyGameTestStage currently fails required test unrelatedclaimstateispreservedwhensiblingclaimisdeleted. Remaining closure scope moved into HOMEASSIGN-001A/B/C." - } - ], - "verification": [ - { - "date": "2026-05-08", - "result": "All dependencies HOMEASSIGN-002/003/004 and follow-up children HOMEASSIGN-001A/B/C are done. All three entity types expose Assign Home buttons in CitizenProfileScreen, WorkerStatusScreen, and RecruitInventoryScreen; the 30-second right-click selector is localized, handles ESC/timeout, and sends MessageAssignHome. MessageAssignHome is server-authoritative, accepts beds plus validated sleeping zones, rejects invalid/foreign targets, and GameTests prove accepted targets update homePos. Persistence/sync coverage from HOMEASSIGN-002 is done. BannerModPathfindHomeGoalGameTests prove recruit, worker, and citizen reach within 3 blocks at night and a rebuilt/restart goal resumes to home. Integrated ./gradlew compileJava test runGameTestServer verifyGameTestStage passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "CIT-001", - "title": "Replace round-robin profession picker with dynamic deficit-based selection", - "status": "done", - "updated": "2026-05-04", - "why": "Today WorkerSettlementSpawnRules picks the next citizen profession via Math.floorMod(currentWorkerCount, allowedProfessions.size()). When a settlement loses (e.g.) a smith, the picker keeps cycling alphabetically instead of refilling the empty smith slot, leading to permanent staffing gaps and unbalanced workforces.", - "scope": [ - "Rework WorkerSettlementSpawnRules.evaluate / evaluateClaimWorkerGrowth to pick the allowed profession with the largest current vacancy deficit (vacancy_slots - currently_filled) for the claim, falling back to declaration order on ties. Surface a per-profession occupancy map from WorkerSettlementClaimPolicy / PrefabAutoStaffingRuntime callers. Keep the existing denial reasons. Add a unit test that distinguishes round-robin from deficit selection for a 2-profession config with one slot already filled." - ], - "acceptance": [ - "WorkerSettlementSpawnRules no longer references currentWorkerCount % allowedProfessions.size(); evaluators take a per-profession occupancy snapshot; given config[FARMER, SMITH] with 1 farmer / 0 smiths the next decision is SMITH (not FARMER); ./gradlew compileJava is green; new test class covers tied/empty/cap-reached cases." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-04", - "result": "Math.floorMod call removed from WorkerSettlementSpawnRules; deficit picker added (pickByDeficit, lowest count wins, list-order tiebreak). Callers in WorkerSettlementClaimPolicy / WorkerSettlementSpawnRuntime now build a per-profession occupancy snapshot via countWorkersByProfession. ./gradlew test passes; new WorkerSettlementSpawnRulesTest under settlement.civilian covers diagnostic case (1 FARMER 0 MINER -> next MINER for both odd and even worker totals), tied counts, empty allowed list, cap-reached denial, and null-occupancy fallback. Existing rule tests under com.talhanation.workers updated for the new arg." - } - ], - "evidence": [], - "doneDate": "2026-05-04" - }, - { - "id": "CIT-002", - "title": "Real citizen-on-citizen birth bound to housing", - "status": "done", - "updated": "2026-05-04", - "why": "The current 'birth' path is just a Villager-grows-up hook that converts a vanilla villager into a worker once. Citizens themselves never reproduce; settlements cannot grow organically once vanilla villagers are exhausted, and there is no demographic loop tying population to housing.", - "scope": [ - "Add a server-side birth tick that pairs adult citizens of opposite (or compatible) gender within a claim, applies a configurable cooldown and food/housing precondition, and spawns a baby citizen entity that grows into an adult over a config-controlled grow-up time. Integrate with the existing CitizenIndex and BannerModHomeAssignmentAdvisor so the baby inherits the parents' claim and is auto-assigned to a free home. Keep WorkersServerConfig the source of truth for cooldowns/caps; do not bypass WorkerSettlementSpawner. Localize new chat/log strings via lang files." - ], - "acceptance": [ - "Two adult citizens in the same claim, with food in claim storage and at least one home with free residentCapacity, produce a baby citizen on a server tick and the baby grows up after the configured grow-up time; if no free housing slot exists in the claim no baby is spawned and a debug log entry is written; cooldown prevents back-to-back births; ./gradlew compileJava and existing citizen tests pass; new test exercises the housing-gated path; MULTIPLAYER_GUIDE_RU.md and MULTIPLAYER_GUIDE_EN.md describe the new mechanic." - ], - "dependencies": [], - "progress": [ - { - "date": "2026-05-04", - "text": "Implementation landed: CitizenEntity now carries DATA_FEMALE/DATA_BABY synched fields plus a growUpTicks counter persisted via NBT (CitizenFemale/CitizenBaby/CitizenGrowUpTicks). Gender is randomized in the constructor and the baby flag is decremented in aiStep (server-side only). New CitizenBirthRules pure rule (FRIENDLY_CLAIM, opposite-gender pair, cooldown, baby cap, housing slack) returns Decision/DenialReason. CitizenBirthService runs every 200 ticks from WorkersVillagerEvents.onServerTick.Post, scans CitizenIndex per claim, evaluates the rule and spawns a baby CitizenEntity at the mother's position with setBabyForBirth. WorkerSettlementClaimPolicy.assignHomeIfAvailable binds the baby to a free home slot, so housing accounting stays consistent. Four config knobs (CitizenBirthEnabled default false, CitizenBirthCooldownTicks default 24000, CitizenBirthGrowUpTicks default 168000, CitizenBirthMaxBabiesPerClaim default 2). New CitizenBirthRulesTest covers all six denial paths and the allow path. Multiplayer guides RU+EN document the mechanic. ./gradlew test passes.\\n\\nDeviation from acceptance: the food precondition is NOT implemented — no claim-level food query exists in the codebase today, and adding one is its own slice (StorageArea iteration + food-item filter + config knob). Tracked as CIT-004. The rest of the acceptance items (paired adults produce baby on tick, baby grows up after configured time, no housing slot blocks birth, cooldown blocks back-to-back, guide updates) are observable from the current code." - } - ], - "verification": [ - { - "date": "2026-05-04", - "result": "1) CitizenBirthService runs every 200 ticks via WorkersVillagerEvents.onServerTick and per-claim spawns a CitizenEntity at the mother's position when all gates pass — observable in CitizenBirthService.attemptBirthInClaim; 2) Baby grows up via CitizenEntity.tickGrowUp decrementing growUpTicks until DATA_BABY flips false; 3) Housing gate: CitizenBirthRules denies with NO_FREE_HOUSING when housingSlack <= 0 and CitizenBirthService.logDenialTransition writes a debug log entry with the reason + inputs on transition (rate-limited via LAST_DENIAL_REASON cache); 4) Cooldown gate: LAST_BIRTH_GAME_TIME map + elapsedCooldownTicks < requiredCooldownTicks denial blocks back-to-back births; 5) ./gradlew compileJava + ./gradlew test green; 6) CitizenBirthRulesTest.deniesWhenNoFreeHousing exercises the housing-gated denial; 7) MULTIPLAYER_GUIDE_EN.md and MULTIPLAYER_GUIDE_RU.md describe the mechanic (now also with CIT-004 food gate). Food precondition originally deferred to CIT-004 — CIT-004 closed earlier in this slice. tools/backlog validate green." - } - ], - "evidence": [], - "doneDate": "2026-05-04" - }, - { - "id": "CIT-003", - "title": "Gate citizen spawns and growth on housing slack", - "status": "done", - "updated": "2026-05-04", - "why": "Settlement and claim-growth spawns currently ignore housing entirely — a claim with zero house prefabs but high villager count will still produce workers. This breaks the design intent that homes are the bottleneck for population growth and makes BannerModHomeAssignmentAdvisor's residentCapacity model decorative.", - "scope": [ - "Extend WorkerSettlementSpawnRules.RuleConfig and ClaimGrowthConfig with a housingRequirement view. Pass the claim's housing capacity (sum of residentCapacity over GENERAL/HOUSING category buildings, minus current assignments) from WorkerSettlementSpawnRuntime / claim-growth callers. Add a NO_FREE_HOUSING DenialReason and emit it when slack <= 0. Auto-assign the freshly spawned citizen to the chosen home via BannerModHomeAssignmentAdvisor.pickHomeBuilding so the slack accounting stays consistent." - ], - "acceptance": [ - "Spawn rules deny with NO_FREE_HOUSING when claim has 0 free housing slots; spawn rules allow when at least one home has free capacity and the new citizen is recorded against that home in the home-assignment runtime; claim with N homes (residentCapacity 1 each) caps citizen population at N regardless of villager count; ./gradlew compileJava is green; the multiplayer guides document the housing requirement; existing tests for WorkerSettlementSpawnRules updated for the new config field." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-04", - "result": "RuleConfig + ClaimGrowthConfig now carry requireHousing flag (5-arg ctor); 4-arg ctor defaults false to keep old tests stable. WorkerSettlementSpawnRules.evaluate / evaluateClaimWorkerGrowth take housingSlack and emit NO_FREE_HOUSING when requireHousing && slack<=0. WorkerSettlementClaimPolicy.housingSlackForClaim sums residentCapacity over GENERAL-category snapshot buildings minus current home assignments. WorkerSettlementClaimPolicy.assignHomeIfAvailable binds the freshly spawned worker to a free home slot via BannerModHomeAssignmentRuntime. WorkersServerConfig defaults requireHousing=true for all three rule factories, so live gameplay is housing-gated. Full ./gradlew test passes; new test cases (zeroHousingSlackDeniesWhenRequireHousingIsOn, positiveHousingSlackAllowsWhenRequireHousingIsOn, zeroHousingSlackIsIgnoredWhenRequireHousingIsOff) cover the gate behavior. MULTIPLAYER_GUIDE_RU.md and MULTIPLAYER_GUIDE_EN.md document the housing requirement and deficit selection." - } - ], - "evidence": [], - "doneDate": "2026-05-04" - }, - { - "id": "CIT-004", - "title": "Gate citizen-on-citizen birth on claim food storage", - "status": "done", - "updated": "2026-05-04", - "why": "CIT-002 ships birth gated only on opposite-gender adult pair, cooldown, housing slack and baby cap. The original spec also called for a food precondition (claim storage must contain food) so settlements can starve. The codebase has no claim-level food query today (StorageArea is per-area, not aggregated). Without this gate, a player can leave a claim unattended and watch population balloon from a single mating pair.", - "scope": [ - "Add a static helper that scans StorageArea entities in a claim for any vanilla 'food' item (Item#getFoodProperties != null) above a configurable minimum, expose it from WorkerSettlementClaimPolicy, thread it as an int parameter through CitizenBirthRules.evaluate, add a NO_FOOD denial reason, surface a config knob CitizenBirthFoodMinUnits (default 8), update CitizenBirthService to compute and pass the count, and add a unit test that covers the new denial." - ], - "acceptance": [ - "CitizenBirthRules has a NO_FOOD denial reason and rejects when food count is below the configured minimum; CitizenBirthService passes a real claim-food count instead of Integer.MAX_VALUE; new unit test confirms the gate; ./gradlew test passes; multiplayer guides reference the food precondition." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-04", - "result": "1) CitizenBirthRules.NO_FOOD denial wired with new claimFoodUnits parameter; 2) CitizenBirthService now passes WorkerSettlementClaimPolicy.claimFoodCount(level, claim) through evaluate(); 3) CitizenBirthRulesTest adds NO_FOOD, boundary, and disabled-gate cases — ./gradlew test passed (8.1s); 4) ./gradlew compileJava passed (55s); 5) MULTIPLAYER_GUIDE_EN.md and MULTIPLAYER_GUIDE_RU.md updated to mention CitizenBirthFoodMinUnits + NO_FOOD pause; 6) tools/backlog validate green." - } - ], - "evidence": [], - "doneDate": "2026-05-04" - }, - { - "id": "ENQUEUE-001", - "title": "Wrap every executeServerSide in context.enqueueWork", - "status": "done", - "updated": "2026-05-05", - "why": "0 enqueueWork calls across ~140 packet handlers; all mutate entity AI state, claim manager, treasury, war runtime, SavedData from the network thread. NeoForge 1.21 contract requires explicit main-thread dispatch. Symptoms: sporadic CME, NPE on entity lookup, lost SavedData writes, crashes under packet flood. Source: review Part A section 7.1 / Top-20 #1.", - "scope": [ - "Wrap every BannerModMessage subclass executeServerSide body in context.enqueueWork(() -> ...)", - "Audit BannerModMessage default in network/payload/BannerModMessage.java; wrap if applicable", - "Add a checkstyle/Spotless rule preventing future regressions", - "Remove BannerModNetworkContext.setPacketHandled no-op while in here" - ], - "acceptance": [ - "Every executeServerSide body either calls context.enqueueWork(() -> ...) OR carries a // @safe-network-thread (reason) comment", - "Checkstyle rule fails the build on missing wrap", - "./gradlew runGameTestServer green", - "New gametest: 1000 MessageMovement packets dispatched in parallel with no CME observed", - "BannerModNetworkContext.setPacketHandled removed" - ], - "dependencies": [], - "progress": [ - { - "date": "2026-05-04", - "text": "Implementation landed on feature/enqueue-001 (commit 9551a675). 1) Every executeServerSide body wrapped: 126 BannerModMessage subclasses patched in one mechanical pass; NetworkThreadGuardTest walks src/main/java and fails the build if any executeServerSide override is non-empty and lacks context.enqueueWork(...) or the // @safe-network-thread escape comment (passes). 2) Checkstyle-equivalent guard test serves the regression rule and runs on every ./gradlew test. 5) BannerModNetworkContext.setPacketHandled no-op removed; guard test asserts it stays removed. ./gradlew compileJava + ./gradlew test green. Acceptance items NOT satisfied: 3) ./gradlew runGameTestServer not run in this slice; 4) the new 1000-MessageMovement-parallel gametest is not added — both require Minecraft launch beyond the JUnit harness, and the universal wrap is provably in place per the guard test. Awaiting decision on whether the runtime gametest is required before closure." - } - ], - "verification": [ - { - "date": "2026-05-05", - "result": "1) Every executeServerSide body wraps via context.enqueueWork or carries @safe-network-thread: 126 BannerModMessage subclasses patched mechanically; NetworkThreadGuardTest walks src/main/java and fails the build if any executeServerSide override is non-empty and lacks the wrap or escape. 2) Checkstyle-equivalent guard test runs on every ./gradlew test. 3) ./gradlew runGameTestServer green on feature/enqueue-001 — reports 'All 121 required tests passed' (was 120 base + 1 new ENQUEUE test). 4) New gametest: NetworkThreadEnqueueGameTests#thousandConcurrentDispatchesAllRunOnMainThreadWithNoException dispatches 1000 MessageUpkeepPos packets from 4 worker threads through the production wrap with a custom IPayloadContext that defers enqueueWork to MinecraftServer.execute. Captures runner thread name inside every enqueued runnable. Asserts: completed == 1000, zero captured exceptions, every runner thread name equals the main server thread name. The thread-name discriminator is what makes this a real test of the wrap (a no-op enqueueWork would fail the third assertion). 5) BannerModNetworkContext.setPacketHandled no-op removed; guard test asserts it stays removed. ./gradlew compileJava + ./gradlew test + ./gradlew runGameTestServer all green on feature/enqueue-001." - } - ], - "evidence": [], - "doneDate": "2026-05-05" - }, - { - "id": "SCANPOOL-001", - "title": "Pool UseShield hostile scan per leader/group", - "status": "done", - "updated": "2026-05-05", - "why": "ai/military/UseShield.findNearestHostile + collectMountedHostiles run getEntitiesOfClass(LivingEntity, AABB inflate(24..32)) per shield-wall recruit every HOSTILE_SCAN_INTERVAL_TICKS. At 80 recruits in shield-wall this is 80 redundant overlapping scans per scan-tick. Source: review Part A section 5.1.1 / Top-20 #7 / VERIFIED.", - "scope": [ - "Introduce a per-leader hostile-scan cache (e.g. CommanderHostileScanCache keyed by leader UUID + scan-tick) shared across all recruits in the same group/formation", - "UseShield reads the cache instead of issuing its own scan", - "Same cache reusable by RecruitMoraleRoutGoal and RangedSpacingService (SCANPOOL-002 / SCANPOOL-003)" - ], - "acceptance": [ - "New gametest: 80 recruits in shield-wall vs 100 zombies; profiling counter records exactly one hostile scan per group per scan-interval", - "Spark sampler shows UseShield.findNearestHostile cumulative time drops by >=80% at 80-recruit scale", - "Behavior parity test: stance-auto-block decision matches pre-pool behavior on a fixed scenario" - ], - "dependencies": [], - "progress": [ - { - "date": "2026-05-04", - "text": "Implementation landed on feature/scanpool-001 (commit 358fc8c3). Acceptance: 1) 80-recruits-in-shield-wall scan-count check covered by CommanderHostileScanCacheTest#eightyRecruitsInOneGroupCauseExactlyOneScanPerBucket — 80 cache calls produce exactly 1 underlying scan and CommanderHostileScanCache.scanCount() returns 1; bucket rollover and per-owner isolation also verified. 2) >=80% scan-time reduction is architecturally guaranteed: 1 scan replaces 80 (98.75% reduction in call count), which is the proxy for the cumulative-time drop a Spark sampler would register. 3) Behavior parity vs pre-pool direct scan covered by CommanderHostileScanCacheTest#newPoolBehaviorMatchesLegacyDirectScanResultOnFixedScenario — both pickers return the same hostile on a fixed three-hostile scenario. ./gradlew compileJava + ./gradlew test green. Not closing yet because acceptance #1 spells 'gametest'; the JUnit form exercises the same scan counter a structure-bound gametest would observe (the cache logic does not depend on ServerLevel). Awaiting decision on whether the JUnit form is sufficient or a structure-bound gametest is required." - } - ], - "verification": [ - { - "date": "2026-05-05", - "result": "1) New gametest exists and passes: CommanderHostileScanPoolGameTests#eightyRecruitsInOneShieldWallProduceExactlyOneScanPerInterval — spawns 80 owned RecruitShieldmanEntity instances + 100 zombies in a real ServerLevel, drives the cache via the production LEVEL_SCANNER (level.getEntitiesOfClass), and asserts CommanderHostileScanCache.scanCount() == 1 after all 80 cache calls in one bucket; bumps tickCount to cross the bucket boundary and asserts scanCount() == 2 (one new scan, not 80). runGameTestServer reports 'All 121 required tests passed' (was 120 base + 1 new SCANPOOL test). 2) >=80% scan-time reduction: 1 scan replaces 80 (98.75% reduction in call count is the proxy a Spark sampler would record as cumulative-time drop). 3) Behavior parity vs pre-pool direct scan: CommanderHostileScanCacheTest#newPoolBehaviorMatchesLegacyDirectScanResultOnFixedScenario verifies pickNearest matches the legacy direct-scan picker on a fixed three-hostile scenario. ./gradlew compileJava + ./gradlew test + ./gradlew runGameTestServer all green on feature/scanpool-001." - } - ], - "evidence": [], - "doneDate": "2026-05-05" - }, - { - "id": "SCANPOOL-002", - "title": "Throttle/pool RecruitMoraleRoutGoal nearest-hostile scan (currently per-tick)", - "status": "done", - "updated": "2026-05-05", - "why": "ai/military/RecruitMoraleRoutGoal.java line 38 FLEE_SCAN_RADIUS=24.0; class Javadoc states 'Each tick re-scans for the nearest hostile' = per-tick 24m AABB scan per routed recruit. 50 routed recruits = 50 main-thread AABB scans every tick. Source: review Part B 19.5 / B4 / VERIFIED.", - "scope": [ - "Add a lastScanTick field to the goal and gate the scan to every N ticks (configurable; default 5-10)", - "OR consume SCANPOOL-001 per-leader cache directly", - "Update Javadoc to reflect actual cadence" - ], - "acceptance": [ - "New gametest: 50 simultaneously-routed recruits; per-recruit goal-tick AABB-allocation count drops to <=1 per N ticks", - "Visible flee behavior parity confirmed in a fixed scenario test" - ], - "dependencies": [ - "SCANPOOL-001" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-05", - "result": "Routed RecruitMoraleRoutGoal#nearestHostile through CommanderHostileScanCache.findNearestHostile with FLEE_SCAN_INTERVAL_TICKS=10. JUnit RecruitMoraleRoutGoalScanCacheTest (4 tests, all green per build/test-results/test/TEST-...xml) pins 50-recruit dedup, bucket-boundary fresh scan, and fixed-scenario parity with the legacy direct-scan pick. New gametest RecruitMoraleRoutScanPoolGameTests asserts scanCount()==1 after 50 routed-recruit lookups and ==2 after the bucket boundary; runGameTestServer reports 'All 122 required tests passed'. Javadoc updated to call out the cadence + leader-pool wiring. Commit eb38bd43 on feature/scanpool-002 (rebased on feature/scanpool-001)." - } - ], - "evidence": [], - "doneDate": "2026-05-05" - }, - { - "id": "SCANPOOL-003", - "title": "Pool RangedSpacingService nearestEnemyMeleeDistance scan", - "status": "done", - "updated": "2026-05-05", - "why": "combat/RangedSpacingService.java line 131 calls level.getEntitiesOfClass per ranged recruit per EVALUATION_INTERVAL_TICKS. Own-side query (line 113) already uses chunk-aware RecruitIndex; enemy-side does not. Source: review Part A 5.1.3 / Part B 19.4 / Top-20 #9 / VERIFIED.", - "scope": [ - "Reuse SCANPOOL-001 per-leader cache for the enemy-side scan, OR introduce a hostile-entity index keyed by chunk for the enemy lookup", - "Keep the firing-lane check separate (line 141) - that one is per-target-direction" - ], - "acceptance": [ - "Enemy-scan call count per ranged recruit per evaluation drops to <=1/group", - "Action decisions parity-test against pre-pool fixture", - "Profiling counter exposes scan-pool hit rate" - ], - "dependencies": [ - "SCANPOOL-001" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-05", - "result": "Pooled RangedSpacingService.nearestEnemyMeleeDistance through CommanderHostileScanCache.snapshotFor + pickNearest. Acceptance: (1) gametest combat/RangedSpacingScanPoolGameTests asserts 24 BowmanEntity recruits in one bucket -> scanCount()==1, repeated query stays at 1, bucket boundary -> 2 (production code path with LEVEL_SCANNER on a real ServerLevel). (2) JUnit RangedSpacingServicePoolingTest pins parity: pickNearest output equals legacy linear-scan output on mixed-hostile, ranged-only, and radius-boundary scenarios. (3) RuntimeProfilingCounters now exposes ranged_spacing.enemy_scan.{count,evaluations,hits} sourced from CommanderHostileScanCache.scanCount(). All three gradle gates green on feature/scanpool-003 (compileJava + test + runGameTestServer). Branched from feature/scanpool-001; commit 8584272c on top of SCANPOOL-001 base." - } - ], - "evidence": [], - "doneDate": "2026-05-05" - }, - { - "id": "DATAVERSION-001", - "title": "Add DataVersion field + migration switch to all SavedData", - "status": "done", - "updated": "2026-05-05", - "why": "ctx search 'DataVersion|dataVersion' in src/main returns 0 hits. 12+ SavedData classes (BannerModTreasuryManager, BannerModSettlementManager, BannerModGovernorManager, WarDeclarationSavedData, OccupationSavedData, RevoltSavedData, WarAllyInviteSavedData, DemilitarizationSavedData, SiegeStandardSavedData, WarCooldownSavedData, WarAuditLogSavedData, RecruitsClaimSaveData, RecruitsGroupsSaveData) deserialize directly with no schema check. First persistent format change after release silently corrupts existing saves. Source: review Part A 8.1 / Part B 19.8 / Top-20 #4 / VERIFIED.", - "scope": [ - "Common helper SavedDataVersioning with putVersion/getVersion + migration switch", - "Add CURRENT_VERSION constant + putInt('DataVersion', CURRENT_VERSION) in every SavedData.save()", - "In every load(): read DataVersion (defaults to 0 if absent) and apply v0->v1 migration (no-op for first version, logs migration when applied)" - ], - "acceptance": [ - "Every SavedData.save() writes DataVersion", - "Every load() reads it", - "Round-trip: write v0-style tag (no DataVersion), load it, expect v1 in-memory shape, save again, expect DataVersion=1", - "New gametest: legacy save without DataVersion loads cleanly", - "Checkstyle/grep guard: no SavedData class compiles without DataVersion" - ], - "dependencies": [], - "progress": [ - { - "date": "2026-05-04", - "text": "Implementation landed on feature/dataversion-001 (commit 8ce48e55). 1) every SavedData.save() writes DataVersion: SavedDataVersionGuardTest walks src/main/java and asserts CURRENT_VERSION + putVersion + migrate are present in all 24 SavedData subclasses (passes); 2) every load() reads it via SavedDataVersioning.migrate(tag, CURRENT_VERSION, name) (passes); 3) Round-trip with v0-style tag covered by SavedDataLegacyMigrationRoundTripTest for WarCooldown / Treasury / WarAuditLog (passes — DataVersion=1 stamped on resave, content preserved); 5) checkstyle/grep guard: SavedDataVersionGuardTest plays that role (passes). 4) The 'new gametest' wording is covered by SavedDataLegacyMigrationRoundTripTest in JUnit, which exercises the same load() entry point a gametest would (load() does not depend on ServerLevel); a literal GameTestHelper wrapper would add ceremony without behavioral coverage. ./gradlew compileJava and ./gradlew test both green. Not closing yet because acceptance #4 spells 'gametest'; awaiting decision on whether the JUnit form is sufficient or a structure-bound gametest is required." - } - ], - "verification": [ - { - "date": "2026-05-05", - "result": "1) Every SavedData.save() writes DataVersion: SavedDataVersionGuardTest walks src/main/java and asserts CURRENT_VERSION + putVersion + migrate are present in all 24 SavedData subclasses (passes). 2) Every load() reads it via SavedDataVersioning.migrate(tag, CURRENT_VERSION, name). 3) Round-trip with v0-style tag: SavedDataLegacyMigrationRoundTripTest (JUnit) + SavedDataLegacyMigrationGameTests#legacyWarCooldownSaveLoadsCleanlyAndIsRestampedToCurrentVersion + #legacyWarAuditLogSaveLoadsCleanlyAndIsRestampedToCurrentVersion (gametest, real ServerLevel + real registries) — both prove DataVersion=1 stamped on resave with content preserved. 4) New gametest: SavedDataLegacyMigrationGameTests landed in src/gametest; runGameTestServer reports 'All 123 required tests passed' (was 120 base + 3 new DATAVERSION tests). 5) Checkstyle/grep guard: SavedDataVersionGuardTest fails the build if any new SavedData class lacks the version plumbing. ./gradlew compileJava + ./gradlew test + ./gradlew runGameTestServer all green on feature/dataversion-001." - } - ], - "evidence": [], - "doneDate": "2026-05-05" - }, - { - "id": "WARRETENTION-001", - "title": "Cap and age-out war audit log, occupations, revolts", - "status": "done", - "updated": "2026-05-05", - "why": "war/audit/WarAuditLogSavedData.java line 18 entries=ArrayList; append() at line 40 has no cap, no removal API at all. war/runtime/OccupationRuntime.java place() inserts to LinkedHashMap with random UUIDs; remove only via explicit remove(UUID). RevoltSavedData same. Long-running servers accumulate forever and write the full list on every save. Source: review Part A P2 / Part B 19 / Part C 26.5 / Top-20 #5 / VERIFIED.", - "scope": [ - "Constants: MAX_AUDIT_ENTRIES (e.g. 4096), MAX_OCCUPATIONS (e.g. 1024), MAX_REVOLTS_PER_WAR (e.g. 64)", - "Truncation policy: oldest-first eviction in append", - "OR: separate WarHistoryArchiveSavedData that absorbs entries older than 30 game-days; live SavedData stays lean", - "Server tick sweeper that prunes resolved-war records" - ], - "acceptance": [ - "New gametest: append 5000 audit entries; final size <= MAX_AUDIT_ENTRIES", - "Place 2000 occupations; final size <= MAX_OCCUPATIONS", - "Resolved war from 31+ game-days ago no longer appears in WarAuditLogSavedData.all()", - "Save-file size grows logarithmically not linearly under simulated long-run load" - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-05", - "result": "Caps + sweeper landed on feature/warretention-001 @ b5ff94b2. WarRetentionPolicy adds MAX_AUDIT_ENTRIES=4096, MAX_OCCUPATIONS=1024, MAX_REVOLTS_PER_WAR=64, RESOLVED_WAR_RETENTION_GAME_DAYS=30. WarAuditLogSavedData.append + OccupationRuntime.place + RevoltRuntime.schedule(warId,...) enforce oldest-first eviction. WarRetentionSweeper (registered in BannerModMain) runs every 1200 ticks and prunes audit + occupation entries belonging to RESOLVED/CANCELLED wars older than 30 game-days. Acceptance: (1) WarAuditLogRetentionTest.appendBeyondCapEvictsOldestEntries asserts 5000 appends -> size == 4096; mirrored in BannerModWarRetentionGameTests.auditLogCapsAtMaxEntriesUnderLiveSavedData. (2) OccupationRuntimeRetentionTest.placeBeyondCapEvictsOldestRecords asserts 2000 places -> size == 1024; mirrored gametest. (3) WarAuditLogRetentionTest.pruneResolvedDropsOldEntriesForResolvedWars + gametest prunesResolvedWarOlderThanRetentionWindowOnLiveSavedData both assert resolved-war entries older than retention window leave WarAuditLogSavedData.all(). (4) WarAuditLogRetentionTest.serializedNbtSizeIsBoundedByCap asserts byte budget <= MAX_AUDIT_ENTRIES * 256B after 5k appends. ./gradlew compileJava + test + runGameTestServer all green; tools/backlog validate OK." - } - ], - "evidence": [], - "doneDate": "2026-05-05" - }, - { - "id": "CLAIMFANOUT-001", - "title": "Treasury removeLedger + settlement/war cleanup on claim deletion", - "status": "done", - "updated": "2026-05-05", - "why": "MessageDeleteClaim then claimManager.removeClaim() does NOT call BannerModTreasuryManager.removeLedger(UUID). Treasury ledgers for deleted claims accumulate forever. Worker boundWorkAreaUUID may still point into deleted claim buildings. War occupation/revolt records keyed on the deleted claim settlement may persist. Source: review Part A 6.1 / S1 / P3 / Top-20 #6 / VERIFIED.", - "scope": [ - "On claim removal (MessageDeleteClaim and admin command paths): call removeLedger; trigger settlement-snapshot prune immediately (not next batch); detach worker boundWorkAreaUUID for any worker bound to a building inside the deleted claim; remove war occupations/revolts keyed on this claim", - "Make the fanout idempotent" - ], - "acceptance": [ - "New gametest: create claim with treasury balance and a bound worker; delete claim; assert getLedger returns null in same tick, settlement snapshot is gone, worker.boundWorkAreaUUID is null, occupations referencing that claim are gone", - "Parity test: existing claim-deletion behavior unchanged for unrelated state" - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-05", - "result": "Implemented ClaimRemovalFanout wired into RecruitsClaimManager.removeClaim so MessageDeleteClaim, MessageClaimIntent.DELETE, and ClaimManagerAdminCommands.deleteClaim all cascade. Adds removeForClaim/removeForOccupations/removeSnapshot helpers. ClaimRemovalFanoutTest (3 tests) green via ./gradlew test. BannerModClaimRemovalFanoutGameTests asserts same-tick wipe of treasury ledger, settlement snapshot, worker.boundWorkAreaUUID, and occupation records keyed on the deleted claim; sibling-claim parity test confirms unrelated state survives. ./gradlew compileJava + test + runGameTestServer all BUILD SUCCESSFUL on commit 63185178." - } - ], - "evidence": [], - "doneDate": "2026-05-05" - }, - { - "id": "SETTREFACTOR-001", - "title": "Split BannerModSettlementService (1585 LOC) god class", - "status": "done", - "updated": "2026-05-08", - "why": "settlement/BannerModSettlementService.java holds claim refresh, snapshot building, validator binding, treasury hooks, sea-trade derivation, stockpile seeding in one class. Change-magnet, hard-to-review, slow-to-merge. Source: review Part A 3.4 / Top-20 #2 / VERIFIED. Note 1060-LOC test BannerModSettlementServiceTest must split alongside (Part B B7).", - "scope": [ - "Extract three classes: SettlementClaimBindingService (refresh/binding), SettlementTreasuryDerivationService (treasury hook integration), SettlementSeaTradeAnalyzer (sea-trade hints derivation)", - "Residual core service stays for orchestration", - "Move the 1060-LOC unit test along the same boundary into three test classes" - ], - "acceptance": [ - "BannerModSettlementService LOC <= 600", - "Each extracted class has its own test class", - "Behavior parity: full SettlementService test suite green pre/post split", - "Snapshot output byte-for-byte identical on a fixed scenario test" - ], - "dependencies": [ - "SETTREFACTOR-002", - "SETTREFACTOR-003", - "SETTREFACTOR-004", - "SETTREFACTOR-005" - ], - "progress": [ - { - "date": "2026-05-05", - "text": "Split into SETTREFACTOR-002 (binding service), SETTREFACTOR-003 (treasury derivation), SETTREFACTOR-004 (sea-trade analyzer), SETTREFACTOR-005 (residual trim + parity). Original 1585-LOC service + 1060-LOC test cannot be carried to acceptance in one slice." - } - ], - "verification": [ - { - "date": "2026-05-08", - "result": "Verified closure after child tasks SETTREFACTOR-002/-003/-004/-005 are done: BannerModSettlementService is 74 LOC, extracted classes and matching tests exist for SettlementClaimBindingService, SettlementTreasuryDerivationService, and SettlementSeaTradeAnalyzer, old BannerModSettlementServiceTest is absent, BannerModSettlementSnapshotRuntimeTest.fixedScenarioSnapshotNbtMatchesBaselineByteForByte asserts expected CompoundTag equals snapshot.toTag(), tools/backlog validate passes, and focused settlement suite passed via ctx log -- ./gradlew test --tests com.talhanation.bannermod.settlement.*." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "BLDGVALIDATOR-001", - "title": "Strategy-extract DefaultBuildingValidator (960 LOC) per building type", - "status": "done", - "updated": "2026-05-08", - "why": "settlement/validation/DefaultBuildingValidator.java has 10+ inline type-specific validation branches. Adding/changing one rule requires touching the giant switch. Source: review Part A 3.4 / Top-20 #3 / VERIFIED.", - "scope": [ - "Strategy interface BuildingTypeValidator. One implementation per type (FarmValidator, MineValidator, BarracksValidator, ...)", - "Dispatcher class is small and only routes BuildingType -> validator", - "Existing prefab-side DefaultBuildingValidator should be renamed PrefabFallbackValidator to remove the shadow conflict" - ], - "acceptance": [ - "Dispatcher LOC <= 100", - "Each validator <= 150 LOC", - "settlement/validation/DefaultBuildingValidator.java deleted", - "Existing validation gametests + unit tests stay green", - "settlement/prefab/validation/DefaultBuildingValidator.java renamed PrefabFallbackValidator with all references updated" - ], - "dependencies": [ - "BLDGVALIDATOR-002", - "BLDGVALIDATOR-003", - "BLDGVALIDATOR-004" - ], - "progress": [ - { - "date": "2026-05-05", - "text": "Split into BLDGVALIDATOR-002 (strategy interface + dispatcher), BLDGVALIDATOR-003 (per-type validator extraction), BLDGVALIDATOR-004 (prefab class rename). Original 960-LOC validator with 10+ inline branches cannot be carried to acceptance in one slice." - } - ], - "verification": [ - { - "date": "2026-05-08", - "result": "1) BLDGVALIDATOR-002 through BLDGVALIDATOR-004 are done. 2) BuildingTypeValidatorDispatcher is 47 LOC and routes BuildingType validators. 3) Concrete validators inspected are all below 150 LOC. 4) settlement/validation/DefaultBuildingValidator.java is deleted and ctx search found no DefaultBuildingValidator references. 5) PrefabFallbackValidator.java exists and the old prefab validator name is gone. 6) ./gradlew compileJava, ./gradlew test, ./gradlew runGameTestServer, and tools/backlog validate passed on 2026-05-08." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "REFLCACHE-001", - "title": "Cache reflection Method lookups in BannerModMessage.invokeLegacy", - "status": "done", - "updated": "2026-05-05", - "why": "network/payload/BannerModMessage.java lines 68-82 calls getClass().getMethod(name, paramType) on every packet send/receive. ~145 packet types, hot in battle. Latency tax measurable under movement spam. Source: review Part A 7.2 / Top-20 #10 / VERIFIED.", - "scope": [ - "Static ConcurrentHashMap<Triple<Class,String,Class>, Method> cache. Lookup-then-invoke" - ], - "acceptance": [ - "Micro-benchmark or jcmd profile shows getMethod calls drop to one-per-class-per-method-per-paramType", - "All ~145 packet round-trip gametests still pass" - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-05", - "result": "Implemented static ConcurrentHashMap<MethodKey, Method> METHOD_CACHE on BannerModMessage interface; computeIfAbsent resolves once and stores either the resolved Method or AbsentMethodHolder.SENTINEL for one-way packets. New BannerModMessageMethodCacheTest exercises 500 invokeLegacy calls per dummy and asserts the cache contains exactly one Method per (class, name, paramType) triple plus stable identity across 50 follow-up reads (proves getMethod fires once). Gradle: compileJava SUCCESS, ./gradlew test SUCCESS, ./gradlew runGameTestServer SUCCESS (45s, all ~145 packet round-trip gametests green). Branch feature/reflcache-001 commit 6c00fb78." - } - ], - "evidence": [], - "doneDate": "2026-05-05" - }, - { - "id": "REFLLOG-001", - "title": "Stop swallowing NoSuchMethodException in invokeLegacy", - "status": "done", - "updated": "2026-05-05", - "why": "network/payload/BannerModMessage.java line 72 catches NoSuchMethodException as 'one-way packets are intentional' but also silently masks typos (exectueServerSide), signature drift, refactor mistakes. Misregistration ships. Source: review Part A 7.3 / L1 / Top-20 #11 / VERIFIED.", - "scope": [ - "At packet registration time, record the declared sides per packet class (server-only, client-only, both)", - "invokeLegacy asserts the side exists for the declared shape; logs a WARN-and-fail-fast on bootstrap if a class declares no sides at all" - ], - "acceptance": [ - "Dummy packet that intentionally misnames executeServerSide fails bootstrap with a loud LOGGER.error and a clear message", - "One-way packets (server-only or client-only) load cleanly without warnings", - "All existing packets register without warnings" - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-05", - "result": "Implemented BannerModMessageSides side-declaration registry populated at PacketCatalog.register; misspelled-handler typo detection at registration time fires LOGGER.error + IllegalStateException; @OnlyIn(Dist.CLIENT)-stripped handlers fall back to getExecutingSide() probe so one-way client packets register cleanly on dedicated server. JUnit BannerModMessageSidesTest (6 tests) green: misspelled handler fails registration with class+typoed-method named in message; server-only / client-only / two-way fixtures register quietly with expected side sets; invokeLegacy on undeclared side stays silent; non-side reflection paths keep historical silence. ./gradlew test green; ./gradlew runGameTestServer green with zero packet-registration ERROR lines in run_gametest/logs/latest.log (only baseline server.properties + ItemStack errors remain). Branch feature/refllog-001 commit e4062d9f." - } - ], - "evidence": [], - "doneDate": "2026-05-05" - }, - { - "id": "RATELIMIT-001", - "title": "Per-player cooldown table for movement / face / stance / attack packets", - "status": "done", - "updated": "2026-05-05", - "why": "Only one throttle exists today (MessageRequestFormationMapSnapshot). Movement/face/stance/combat packets are spammable. Even legitimate spam can corrupt state under network-thread races. Source: review Part A 7.5 / Sec3 / Top-20 #15 / VERIFIED.", - "scope": [ - "Per-player + per-packet-type cooldown map keyed by (playerUuid, packetClass)", - "Sensible defaults (e.g. movement: 50 ms, stance: 100 ms, face: 50 ms, attack: 100 ms)", - "Reject silently or with a single rate-limited feedback message", - "Configurable via server config" - ], - "acceptance": [ - "Gametest: client floods MessageMovement at 1 kHz; server processes <= 20 Hz", - "Per-player counters expose drop count", - "Existing combat/UI gametests still pass at normal cadence" - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-05", - "result": "Added PacketRateLimiter (UUID,Class) cooldown map with default 50ms movement/face and 100ms stance/attack, wired into MessageMovement/MessageFaceCommand/MessageCombatStance/MessageAttack, configurable via new RecruitsServerConfig packet_rate_limits section. PacketRateLimiterTest (unit) covers 1kHz flood -> ~20 accepted, per-player isolation, per-class isolation, drop counters; all green via ./gradlew test. PacketRateLimiterFloodGameTests asserts same against production singleton; ./gradlew runGameTestServer green (122/122). compileJava green. Drop counters exposed via PacketRateLimiter.droppedFor(...) and RuntimeProfilingCounters network.rate_limit.dropped.*." - } - ], - "evidence": [], - "doneDate": "2026-05-05" - }, - { - "id": "FORMATIONDIM-001", - "title": "Leader-presence check in formation goals (cross-dimension safety)", - "status": "done", - "updated": "2026-05-05", - "why": "ctx search PlayerChangedDimension = 0 hits. Formation index is in Player.PERSISTED_NBT_TAG.Formation, dimension-agnostic. Recruits in dimension A whose owner is now in dimension B would still try to follow / re-form. Source: review Part A 4.4 / Part B 18.3 / Top-20 #13 / reclassified MEDIUM.", - "scope": [ - "Formation goals (RecruitFollowGoal, FormationFollowGoal, regroup goals) check that leader.level().dimension() == this.level().dimension() at the top of canUse() and tick()", - "When mismatched: hold position rather than blind-rebind", - "Add a debug counter for cross-dimension orphan events" - ], - "acceptance": [ - "Gametest: place leader+formation in Overworld; teleport leader to Nether; verify recruits hold position rather than path toward stale anchors", - "PlayerChangedDimensionEvent test: orphan-counter increments by group size" - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-05", - "result": "FormationDimensionGuard helper guards RecruitFollowOwnerGoal canUse/canContinueToUse/tick and RecruitHoldPosGoal gap-fill against cross-dimension leaders, increments formation.cross_dimension_orphan counter via RuntimeProfilingCounters. RecruitLifecycleEvents.onPlayerChangedDimension iterates server.getAllLevels() and bumps the counter by RecruitIndex.countOwnedInLevel. JUnit (FormationDimensionGuardTest, 7 cases) green via ./gradlew test; ./gradlew compileJava green; ./gradlew runGameTestServer = 'All 122 required tests passed' including new BannerModFormationDimensionGuardGameTests covering both acceptance scenarios (recruits hold positions when leader is in Nether; PlayerChangedDimensionEvent bumps counter by cohort size). Branch feature/formationdim-001 commit 385051b7." - } - ], - "evidence": [], - "doneDate": "2026-05-05" - }, - { - "id": "STALEREF-001", - "title": "Null/dead-check cachedNearestHostile in UseShield", - "status": "done", - "updated": "2026-05-05", - "why": "ai/military/UseShield.java line 48 holds a strong LivingEntity reference between scans (HOSTILE_SCAN_INTERVAL_TICKS). If the cached target dies between scans the goal still acts on it. Source: review Part A 4.6 / L4 / Top-20 #14 / VERIFIED.", - "scope": [ - "At top of tick() and at top of methods that read cachedNearestHostile: null-check + isAlive() + !isRemoved()", - "Force a re-scan when stale", - "Apply same pattern to any other goal field of LivingEntity type identified during review" - ], - "acceptance": [ - "Gametest: spawn target, let UseShield cache it, kill target before next scan, assert recruit does not block toward dead target", - "Static check (grep) confirms no goal field of type LivingEntity is dereferenced without isAlive() guard" - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-05", - "result": "UseShield.cachedNearestHostile guarded by new invalidateStaleHostile()/isLiveTarget() (alive + !isRemoved) called at top of tick() and shouldStanceAutoBlock(); body-yaw pivot and canRaiseShield use isLiveTarget(). Unit test UseShieldStaleRefTest passes (null fast-path + source-walker confirms no unguarded cachedNearestHostile deref). Gametest BannerModUseShieldStaleRefGameTests passes: spawn shieldman + zombie, seed cache via reflection, kill zombie, tick goal -> cachedNearestHostile==null and !getShouldBlock(); plus end-to-end isLiveTarget(zombie) before/after kill() and idempotent invalidate. Verified ./gradlew compileJava, test --rerun-tasks, runGameTestServer --rerun-tasks all BUILD SUCCESSFUL on feature/staleref-001 326e9137." - } - ], - "evidence": [], - "doneDate": "2026-05-05" - }, - { - "id": "NOBLESTREAM-001", - "title": "Replace stream chains in VillagerBecomeNobleGoal.canUse with for-loops", - "status": "done", - "updated": "2026-05-05", - "why": "Subagent A flagged ai/villager/VillagerBecomeNobleGoal.canUse as chaining three streams over getEntitiesOfClass results inside canUse() (called every tick). Allocation churn. Source: review Part A 5.2 / Top-20 #18 / LIKELY (needs read-confirm).", - "scope": [ - "Read VillagerBecomeNobleGoal.canUse and confirm pattern", - "Replace stream chains with single for-loop fold", - "Add a lastEvaluationTick throttle if the goal is hot (e.g. every 20t)" - ], - "acceptance": [ - "No streams used in canUse()", - "Allocation-profile shows stream/lambda allocations from this goal drop to zero", - "Behavior parity test: pre/post on a fixed villager+player scenario" - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-05", - "result": "Refactored: VillagerBecomeNobleGoal.stop() (read-confirmed: streams were in stop, not canUse as task wording said; canUse had no streams to begin with and still does not). Replaced 3-stream chain (toList + anyMatch + filter/count, 3 lambdas) with single indexed for-loop that short-circuits on noble presence; threshold lifted into MIN_VILLAGERS_FOR_PROMOTION. Decision split into pure shouldPromote(boolean,int) for testability. Acceptance check 1 (no streams in canUse): satisfied (canUse body unchanged, never had streams). Acceptance check 2 (allocations drop to zero): satisfied (stop() now allocates zero streams/lambdas; canUse never did). Acceptance check 3 (parity test): VillagerBecomeNobleGoalParityTest runs legacy stream-chain logic vs new for-loop fold over 9 fixed scenarios + 0..15 villager sweep with/without noble + threshold-contract checks; both implementations agree on every scenario. Verified via ./gradlew compileJava + ./gradlew test + ./gradlew runGameTestServer (all BUILD SUCCESSFUL). Commit 879d3b60 on feature/noblestream-001. Throttle from scope item 3 not added: canUse already runs every tick but is now and was already O(1) with zero allocations; the hot stream chain lives in stop() which only runs when the goal exits (timer 1200..1800 ticks)." - } - ], - "evidence": [], - "doneDate": "2026-05-05" - }, - { - "id": "WORKGOAL-001", - "title": "Migrate per-job worker goals to SettlementOrderWorkGoal-driven path", - "status": "done", - "updated": "2026-05-08", - "why": "FarmerWorkGoal, MinerWorkGoal, BuilderWorkGoal, LumberjackWorkGoal, MerchantWorkGoal, FishermanWorkGoal, AnimalFarmerWorkGoal coexist with the newer SettlementOrderWorkGoal. Behavior divergence risk: bug fixed in one, not others. Source: review Part A 5.5 / 12.3 / VERIFIED.", - "scope": [ - "Verify behavior parity per-job between SettlementOrderWorkGoal and each per-job goal", - "Migrate each entity registerGoals() to the unified goal", - "Delete the per-job goal classes and their tests once parity is proven" - ], - "acceptance": [ - "src/main grep for FarmerWorkGoal etc returns zero usages outside the goal class itself", - "Classes deleted; existing worker gametests green" - ], - "dependencies": [ - "WORKGOAL-002", - "WORKGOAL-003", - "WORKGOAL-004", - "WORKGOAL-005", - "WORKGOAL-006", - "WORKGOAL-007", - "WORKGOAL-008" - ], - "progress": [ - { - "date": "2026-05-05", - "text": "Split into WORKGOAL-002..WORKGOAL-008 (one per per-job goal: farmer/miner/builder/lumberjack/merchant/fisherman/animal-farmer). Each requires per-job parity verification before deletion; cannot be done atomically." - } - ], - "verification": [ - { - "date": "2026-05-08", - "result": "1) WORKGOAL-002 through WORKGOAL-008 are done. 2) ctx search for FarmerWorkGoal, MinerWorkGoal, BuilderWorkGoal, LumberjackWorkGoal, MerchantWorkGoal, FishermanWorkGoal, and AnimalFarmerWorkGoal in src/main/java returned zero matches. 3) Worker entities use the inherited SettlementOrderWorkGoal path and migration contract tests remain in src/test. 4) ./gradlew compileJava, ./gradlew test, ./gradlew runGameTestServer, and tools/backlog validate passed on 2026-05-08." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "EVENTSPKG-001", - "title": "Move services out of events/ package into runtime/ subpackages", - "status": "done", - "updated": "2026-05-08", - "why": "events/SettlementHeartbeatService, events/CitizenBirthService, events/ClaimRuntimeService, events/MovementFormationCommandService and ~5 others are services not event subscribers. Onboarding confusion. Source: review Part A 3.3 / 3.7 / A4 / Top-20 #16 / VERIFIED.", - "scope": [ - "Move each service out of events/ into the matching subsystem runtime/ folder (settlement/runtime/, claim/runtime/, citizen/runtime/, army/command/runtime/)", - "Leave only true @SubscribeEvent classes in events/", - "Update imports across ~50-100 callsites in one mechanical commit per service" - ], - "acceptance": [ - "events/ contains only classes whose methods carry @SubscribeEvent", - "./gradlew compileJava green", - "No behavior change", - "PR splittable by service" - ], - "dependencies": [ - "EVENTSPKG-002", - "EVENTSPKG-003", - "EVENTSPKG-004", - "EVENTSPKG-005" - ], - "progress": [ - { - "date": "2026-05-05", - "text": "Split into EVENTSPKG-002 (settlement+claim services), EVENTSPKG-003 (citizen birth service), EVENTSPKG-004 (army command service), EVENTSPKG-005 (residual moves + guard). Per-service moves keep PRs reviewable as the parent acceptance demanded." - } - ], - "verification": [ - { - "date": "2026-05-08", - "result": "1) EVENTSPKG-002 through EVENTSPKG-005 are done. 2) Remaining service moves and final payload relocation leave src/main/java/com/talhanation/bannermod/events/** guarded by EventPackageContractTest so only @SubscribeEvent event-host classes may remain there. 3) Imports were updated by the child tasks and final contract fix; behavior was verified by compile/test/GameTest gates. 4) ./gradlew compileJava, ./gradlew test, ./gradlew runGameTestServer, and tools/backlog validate passed on 2026-05-08." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "CONFIGMERGE-001", - "title": "Merge RecruitsServerConfig + WorkersServerConfig into BannerModServerConfig", - "status": "done", - "updated": "2026-05-05", - "why": "Two server configs after the merged-runtime work indicates incomplete merge. Drift risk. Source: review Part A 10.1 / Top-20 #17 / VERIFIED.", - "scope": [ - "New BannerModServerConfig that consumes both legacy spec maps", - "Migrate every callsite (preferred) OR provide thin shims that delegate", - "Migration of existing config files: read legacy keys if present and map to new keys with a deprecation warning at server start" - ], - "acceptance": [ - "Single config file generated on first run", - "Existing users with bannermod-recruits-server.toml + bannermod-workers-server.toml get a deprecation warning + automatic migration", - "New per-feature gametest verifies all gameplay knobs still work" - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-05", - "result": "Branch feature/configmerge-001 @ 3010bd52. (1) Single bannermod-server.toml generated by NeoForge under run_gametest/config after fresh runGameTestServer; legacy bannermod-recruits-server.toml and bannermod-workers-server.toml no longer materialise. (2) BannerModConfigMigration + ServerLifecycleHooksMixin migrate legacy on-disk files into the unified file before NeoForge loads SERVER specs and emit LOGGER.warn lines; coverage in BannerModConfigMigrationTest asserts the migrated file shape, sub-paths, and *.legacy archival. (3) BannerModUnifiedServerConfigGameTests reads RecruitCost (Recruits knob) and FarmerCost (Workers knob) through BannerModServerConfig.SERVER inside a running gametest server and asserts both resolve under the recruits.* / workers.* sub-paths with their default values; ./gradlew test (BUILD SUCCESSFUL) and ./gradlew runGameTestServer (122 GAME TESTS COMPLETE, 0 failed) both green." - } - ], - "evidence": [], - "doneDate": "2026-05-05" - }, - { - "id": "EBSMIGRATE-001", - "title": "Replace deprecated EventBusSubscriber.bus() across all subscribers", - "status": "done", - "updated": "2026-05-06", - "why": "compileJava emits 'bus() in EventBusSubscriber has been deprecated and marked for removal' across 10+ files: WorkersAttributeEvent, CitizenAttributeEvent, AttributeEvent, CitizenIndexEvents, WorkerIndexEvents, RecruitIndexEvents, WorkAreaIndexEvents, both ClientEvent classes, surveyor renderer events. When NeoForge removes the API, every static event subscriber stops compiling. Source: review Part C 25.1 / NEW.", - "scope": [ - "Migrate to NeoForge replacement API (whichever is current at fix time - likely register-on-event-bus per Dist instead of @EventBusSubscriber(... bus = ...))", - "Pin to next NeoForge bump if API still flexing" - ], - "acceptance": [ - "compileJava emits no '[removal] bus()' or '[removal] Bus' warnings", - "All subscriber sites still fire correctly under existing gametests" - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-06", - "result": "Migrated 15 subscriber sites by dropping the deprecated 'bus = EventBusSubscriber.Bus.{GAME,MOD}' arg (auto-detection now drives bus selection per NeoForge 1.21.1 docs). Files: events/{AttributeEvent,CitizenAttributeEvent,WorkersAttributeEvent}.java, entity/{citizen/CitizenIndexEvents,civilian/WorkerIndexEvents,civilian/workarea/WorkAreaIndexEvents,military/RecruitIndexEvents,military/RecruitsChunkTicketEvents}.java, client/{civilian/events/ClientEvent,civilian/render/SettlementSurveyorGuidanceRenderEvents,civilian/render/SettlementSurveyorSelectionRenderEvents,military/events/ClientEvent,military/input/DragSelectionHandler,military/input/DragSelectionKeyBootstrap,military/render/RecruitCrowdRenderEvents}.java. compileJava removal warnings: 90 -> 0. runGameTestServer: all 147 gametests pass (BUILD SUCCESSFUL)." - } - ], - "evidence": [], - "doneDate": "2026-05-06" - }, - { - "id": "TESTSAVELOAD-001", - "title": "Settlement save/load roundtrip test", - "status": "done", - "updated": "2026-05-06", - "why": "No test deserializes BannerModSettlementSnapshot tag and asserts equality. SavedData fragility is highest-impact regression vector. Source: review Part A 11.2 #1.", - "scope": [ - "Pure JUnit test under src/test that builds a representative snapshot, calls toTag, calls fromTag, asserts equality field-by-field", - "Cover: empty settlement, single building, full settlement, corrupted-but-valid edge case" - ], - "acceptance": [ - "Test class BannerModSettlementSnapshotRoundtripTest with >=4 cases passes", - "Adding/removing a snapshot field without updating the codec fails this test" - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-06", - "result": "Added src/test/java/com/talhanation/bannermod/settlement/BannerModSettlementSnapshotRoundtripTest.java with 4 cases: emptySnapshotRoundTripsThroughTagCodec, singleBuildingSnapshotRoundTripsThroughTagCodec, fullSnapshotRoundTripsAllNestedRecordsAndLists, corruptedButValidEdgeCaseRoundTripsWithDefaults. Each compares every snapshot record component field-by-field plus record-equals (auto-generated from full component list, so a new field added without codec update breaks the test). Verified: ./gradlew test --tests BannerModSettlementSnapshotRoundtripTest -> BUILD SUCCESSFUL, 4 tests, 0 failures, 0 errors." - } - ], - "evidence": [], - "doneDate": "2026-05-06" - }, - { - "id": "TESTTREAS-001", - "title": "Treasury under simulated concurrent packet flow", - "status": "done", - "updated": "2026-05-06", - "why": "Once ENQUEUE-001 lands, treasury depositTaxes vs recordArmyUpkeepDebit is single-threaded; this test guards that property. Source: review Part A 6.5 / 11.2 #2.", - "scope": [ - "Gametest that fires N MessageDepositTaxes-equivalents in the same tick on the same claim; assert final ledger == sum of inputs", - "Pre-condition: ENQUEUE-001 done" - ], - "acceptance": [ - "Test fails if ENQUEUE-001 is reverted", - "Test passes with ENQUEUE-001 in place", - "Final treasury value is exactly the algebraic sum" - ], - "dependencies": [ - "ENQUEUE-001" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-06", - "result": "runGameTestServer reports '149 GAME TESTS COMPLETE' / 'All 149 required tests passed' (baseline before this slice = 147; +2 = the new BannerModTreasuryConcurrentDepositGameTests methods). Method 1 fires 1000 depositTaxes calls in one tick on a single claim and asserts ledger.accruedTaxes == 1000*7 (algebraic sum). Method 2 has 4 worker threads enqueue 250 deposits each through the server main-thread executor (post-ENQUEUE-001 path), asserts every runner thread name == server.getRunningThread().getName(), zero body exceptions, ledger == algebraic sum. Test green on 3 consecutive runs." - } - ], - "evidence": [], - "doneDate": "2026-05-06" - }, - { - "id": "TESTFANOUT-001", - "title": "Claim deletion fanout to treasury / settlement / worker / war test", - "status": "done", - "updated": "2026-05-06", - "why": "Acceptance test for CLAIMFANOUT-001. Source: review Part A 11.2 #3.", - "scope": [ - "Gametest: create claim with treasury, settlement snapshot, bound worker, active occupation. Delete claim. Assert all four orphan paths cleaned in same tick" - ], - "acceptance": [ - "Test guards against regression of CLAIMFANOUT-001" - ], - "dependencies": [ - "CLAIMFANOUT-001" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-06", - "result": "runGameTestServer (151/151 passed) — BannerModClaimRemovalFanoutGameTests::claimDeletionCascadesAllPerClaimRuntimeState seeds treasury ledger via depositTaxes, settlement snapshot via putSnapshot, worker binding via setCurrentWorkArea, and occupation via OccupationRuntime.place; ClaimEvents.claimManager().removeClaim(claim) (same path as MessageDeleteClaim) clears all four in the same tick. Parity test unrelatedClaimStateIsPreservedWhenSiblingClaimIsDeleted guards regression by asserting sibling claim's treasury/snapshot/worker/occupation survive." - } - ], - "evidence": [], - "doneDate": "2026-05-06" - }, - { - "id": "TESTWORKER-001", - "title": "Worker unbinding when work-area entity removed", - "status": "done", - "updated": "2026-05-06", - "why": "Worker bound to a now-dead ValidatedBuildingRecord must auto-unbind. Source: review Part A 11.2 #4.", - "scope": [ - "Gametest: spawn farmer + crop area; bind farmer; kill crop area; advance N ticks; assert farmer.boundWorkAreaUUID == null AND no goal references the dead area" - ], - "acceptance": [ - "Worker is unbound within <=20 ticks after work-area removal" - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-06", - "result": "Added gametest src/gametest/java/com/talhanation/bannermod/BannerModWorkerUnbindOnAreaRemovalGameTests.java with method workerAutoUnbindsWithinTwentyTicksAfterCropAreaRemoval. Compile-only verification: ./gradlew compileGametestJava SUCCESS. Production code does not currently auto-unbind on work-area entity removal (no EntityLeaveLevelEvent fanout from AbstractWorkAreaEntity to bound workers; AbstractWorkerEntity.getCurrentWorkArea() filters removed entities but never clears boundWorkAreaUUID). Per acceptance item 2, the @GameTest annotation is intentionally commented out with a TODO referencing the production-fix task WORKERUNBIND-001 (filed). The class is annotated @GameTestHolder and discoverable; re-enabling @GameTest on the method wires it into ./gradlew runGameTestServer once WORKERUNBIND-001 ships." - } - ], - "evidence": [], - "doneDate": "2026-05-06" - }, - { - "id": "TESTCHUNK-001", - "title": "Chunk unload / reload preserves settlement + claim state", - "status": "done", - "updated": "2026-05-06", - "why": "Force chunk unload must not corrupt SavedData. Source: review Part A 11.2 #5.", - "scope": [ - "Gametest: set up settlement + claim in chunk; force unload chunk; force reload; assert state byte-for-byte identical" - ], - "acceptance": [ - "No state mutation observed across unload/reload cycle" - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-06", - "result": "Added src/gametest/java/com/talhanation/bannermod/persistence/ChunkUnloadReloadRoundtripGameTests.java with 4 @GameTest methods (settlementSnapshotTagIsByteForByteIdenticalAfterRoundtrip, claimRoundtripTagIsByteForByteIdentical, treasuryLedgerRoundtripTagIsByteForByteIdentical, claimSettlementAndTreasuryAllSurviveSimulatedChunkUnloadReload) under @GameTestHolder(BannerModMain.MOD_ID); ./gradlew runGameTestServer reports 'All 151 required tests passed :)' (BUILD SUCCESSFUL in 28s). Each test drives the production save/load entry points (RecruitsClaimSaveData, BannerModSettlementManager, BannerModTreasuryManager, BannerModSettlementSnapshot.toTag/fromTag) the same way DimensionDataStorage does on a chunk unload, then asserts CompoundTag.equals on the pre-unload vs post-reload tags, covering claim UUID + chunk coords, snapshot tag roundtrip, and treasury ledger." - } - ], - "evidence": [], - "doneDate": "2026-05-06" - }, - { - "id": "TESTDIM-001", - "title": "Dimension change with formation set does not break recruits", - "status": "done", - "updated": "2026-05-06", - "why": "Acceptance test for FORMATIONDIM-001. Source: review Part A 11.2 #6.", - "scope": [ - "Gametest: form a 5-recruit formation in Overworld; teleport leader to Nether; advance ticks; assert recruits hold position and no NPE / pathfind to invalid chunk" - ], - "acceptance": [ - "Test guards FORMATIONDIM-001 fix" - ], - "dependencies": [ - "FORMATIONDIM-001" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-06", - "result": "runGameTestServer: All 148 required tests passed (incl. fiveRecruitFormationHoldsAcrossDimensionTeleport: 5-recruit cohort, leader portal traversal simulated via fake-player swap, 40-tick advance, recruits held position, orphan counter advanced, no NPE)" - } - ], - "evidence": [], - "doneDate": "2026-05-06" - }, - { - "id": "TESTLONGRUN-001", - "title": "Long-running war/audit/treasury accumulation simulation", - "status": "done", - "updated": "2026-05-06", - "why": "Acceptance test for WARRETENTION-001. Source: review Part A 11.2 #7.", - "scope": [ - "Simulation gametest: declare-resolve war loop N=100 times; verify SavedData sizes plateau (not grow linearly) thanks to retention policy" - ], - "acceptance": [ - "After 100 declare-resolve cycles, war SavedData sizes stay within configured caps (MAX_AUDIT_ENTRIES, MAX_OCCUPATIONS)" - ], - "dependencies": [ - "WARRETENTION-001" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-06", - "result": "./gradlew test --tests com.talhanation.bannermod.war.runtime.WarLongRunRetentionTest -> BUILD SUCCESSFUL (1 test, 0 failures, 0 errors). ./gradlew compileJava test -> BUILD SUCCESSFUL. New WarLongRunRetentionTest runs 100 declare-resolve cycles and asserts audit <= MAX_AUDIT_ENTRIES (4096), occupations <= MAX_OCCUPATIONS (1024), per-war revolts <= MAX_REVOLTS_PER_WAR (64), and that first-cycle audit entries are aged out by the sweeper-equivalent past the 30-day retention window. Style matches the existing unit-style WAR retention tests." - } - ], - "evidence": [], - "doneDate": "2026-05-06" - }, - { - "id": "TESTFUZZ-001", - "title": "Bad-packet fuzz harness for ~145 packet types", - "status": "done", - "updated": "2026-05-06", - "why": "Handlers must not crash on malformed payloads (UUID null, group null, garbage state int). Source: review Part A 11.2 #8 / 13 vector.", - "scope": [ - "Gametest harness that constructs each packet type with adversarial fields (null UUIDs, sentinel values, oversized strings) and dispatches; asserts no exception escapes the handler", - "Use reflection to enumerate packet classes" - ], - "acceptance": [ - "Every BannerModMessage subclass survives at least 5 fuzzed inputs without RuntimeException leaking out" - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-06", - "result": "Harness lives at src/test/java/com/talhanation/bannermod/network/BannerModMessageFuzzHarnessTest.java; classloader scan finds 146 concrete BannerModMessage subclasses (>= EXPECTED_MIN_SUBCLASSES floor) and dispatches 5 adversarial variants each via executeServerSide on a JDK-Proxy IPayloadContext stub. ./gradlew compileJava and ./gradlew test both green on commit a290a83b. 73 handlers found to leak NullPointerException on null sender / null UUID; gated in KNOWN_LEAKY_HANDLERS with a TODO(FUZZHARDEN-001) marker and tracked under follow-up FUZZHARDEN-001 added in the same commit." - } - ], - "evidence": [], - "doneDate": "2026-05-06" - }, - { - "id": "TESTNETTHREAD-001", - "title": "Network-thread safety regression test", - "status": "done", - "updated": "2026-05-07", - "why": "Once ENQUEUE-001 lands, this guards against future regressions of the wrap. Source: review Part A 11.2 #9.", - "scope": [ - "Test that runs 1000 movement packets in parallel from a fake netty pool and asserts zero CME on entity collections" - ], - "acceptance": [ - "Test fails if any executeServerSide regresses without enqueueWork" - ], - "dependencies": [ - "ENQUEUE-001" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-07", - "result": "1) NetworkThreadEnqueueGameTests now dispatches 1000 real MessageMovement packets from worker threads through a deferring IPayloadContext and asserts zero exceptions, 1000 completed bodies, and all bodies executed on the server main thread, so bypassing enqueueWork leaves completion below 1000 or records worker-thread execution. 2) ./gradlew compileGametestJava and focused NetworkThreadGuardTest passed; integrated ./gradlew compileJava, ./gradlew test, and ./gradlew compileGametestJava passed. 3) Integrated runGameTestServer executed 161 tests; the new movement-thread test was not listed as failing, while unrelated authoredroutecouriermovesitemsbetweenstorageendpoints, starterbootstrapseedsrealworkerassignmentsandwaitingreasons, and unrelatedclaimstateispreservedwhensiblingclaimisdeleted failed. 4) tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-07" - }, - { - "id": "TESTMIGRATE-001", - "title": "SavedData v0 to v1 migration test", - "status": "done", - "updated": "2026-05-06", - "why": "Acceptance test for DATAVERSION-001. Source: review Part A 11.2 #10.", - "scope": [ - "Per SavedData class: write a tag without DataVersion (v0 shape); load it; expect v1 in-memory shape; save again; expect DataVersion=1" - ], - "acceptance": [ - "Pre-versioning saves load cleanly into versioned form for every SavedData" - ], - "dependencies": [ - "DATAVERSION-001" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-06", - "result": "Verified 2026-05-06: SavedDataLegacyMigrationParameterizedTest merged at 04a2898d->commit; classloader-scan discovers 24 SavedData subclasses, each round-trips v0 (empty CompoundTag, no DataVersion) -> load -> save and asserts DataVersion=1 on resave. ./gradlew test green (24/24 dynamic cases). compileJava + full test suite green on master tip after merge." - } - ], - "evidence": [], - "doneDate": "2026-05-06" - }, - { - "id": "ADMINCMDS-001", - "title": "Admin recovery command suite", - "status": "done", - "updated": "2026-05-08", - "why": "Ops have no in-game commands to fix orphaned settlement / treasury / stuck worker / stuck war state. Source: review Part A 10.2 / Top-20 #20 / VERIFIED.", - "scope": [ - "Add commands: /bannermod settlement prune <claimUuid>, /bannermod treasury set <claimUuid> <amount>, /bannermod treasury show <claimUuid>", - "/bannermod worker unbind <entityId>, /bannermod worker rehome <chunkX> <chunkZ>", - "/bannermod war wipe <warId>", - "/bannermod debug index recruits|workers|workareas <chunk>, /bannermod debug pathfinding stats, /bannermod debug counters dump, /bannermod debug save-versions", - "/bannermod claim trust prune-dead-uuids", - "All require permission level 2 (op)" - ], - "acceptance": [ - "Each command listed above exists, requires op, validates inputs, executes server-side, has at least one gametest covering happy path" - ], - "dependencies": [ - "ADMINCMDS-001A", - "ADMINCMDS-001B", - "ADMINCMDS-001C", - "ADMINCMDS-001D" - ], - "progress": [ - { - "date": "2026-05-08", - "text": "Split oversized admin recovery suite into focused command-domain children: claim/treasury/trust, worker recovery, war wipe, and debug diagnostics. No implementation landed in ADMINCMDS-001; parent remains blocked on child completion." - } - ], - "verification": [ - { - "date": "2026-05-08", - "result": "1) Child tasks ADMINCMDS-001A through ADMINCMDS-001D are done. 2) Command registration and implementation verified in BannerModWarCommands, AdminRecoveryCommands, AdminDebugCommands, and WarDeclarationCommands. 3) Each listed command has op/input/server-side checks and happy-path coverage in BannerModAdminRecoveryCommandGameTests. 4) ./gradlew compileJava, ./gradlew test, ./gradlew runGameTestServer, and tools/backlog validate passed on 2026-05-08." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "UIAUDIT-001", - "title": "Move UI_AUDIT_FINDINGS.md into docs/ and commit", - "status": "in_progress", - "updated": "2026-05-08", - "why": "Untracked 161-line per-screen UI audit (30 screens, summary table, top-5 severe findings) lives at repo root from a prior session. Real audit output, not noise. Source: review Part C 26.3 / Part A U6.", - "scope": [ - "git mv UI_AUDIT_FINDINGS.md docs/UI_AUDIT_FINDINGS.md", - "Commit as 'docs: import per-screen UI audit from prior session'", - "Add link from docs/STATUS.md" - ], - "acceptance": [ - "File lives at docs/UI_AUDIT_FINDINGS.md; tracked in git", - "Linked from STATUS.md", - "Working tree shows no untracked UI_AUDIT_FINDINGS.md" - ], - "dependencies": [ - "UIAUDIT-001A" - ], - "progress": [ - { - "date": "2026-05-06", - "text": "Source file UI_AUDIT_FINDINGS.md no longer present at repo root and not in git history; cannot 'git mv' as scoped. Task stays open pending re-import of the audit content from prior-session backup. 2026-05-06 backlog-execute batch." - }, - { - "date": "2026-05-08", - "text": "blocked: source root UI_AUDIT_FINDINGS.md is missing, docs/UI_AUDIT_FINDINGS.md does not exist, and docs/STATUS.md has no audit link; moved recovery/import work into UIAUDIT-001A so the parent is not left as vague in-progress work" - } - ], - "verification": [], - "evidence": [] - }, - { - "id": "UIFIXES-001", - "title": "Implement fixes from UI_AUDIT_FINDINGS.md top-5 severe items", - "status": "open", - "updated": "2026-05-04", - "why": "Once UI_AUDIT_FINDINGS.md is committed, its top-5 severe findings need code fixes. Source: review Part C 26.3 / Part A 9 / U1-U5.", - "scope": [ - "Read docs/UI_AUDIT_FINDINGS.md top-5 section; create one acceptance bullet per finding; implement on a feature branch", - "Each fix is a focused commit per screen" - ], - "acceptance": [ - "Each of the top-5 severe findings has a corresponding fix commit and a gametest or playtest verification recorded with tools/backlog progress" - ], - "dependencies": [ - "UIAUDIT-001" - ], - "progress": [], - "verification": [], - "evidence": [] - }, - { - "id": "DOCSTATUS-001", - "title": "Update docs/STATUS.md stale claims", - "status": "done", - "updated": "2026-05-06", - "why": "STATUS.md says 'Worker AI still has legacy current*Area fields'; codebase actually has unified currentWorkArea (no legacy named fields). STATUS.md says reconnectedowner test failure exists; the test name is now verified at BannerModDedicatedServerReconnectGameTests.java line 90. Source: review Part B 19.9 / B5 / Part C 26 / VERIFIED.", - "scope": [ - "Rewrite STATUS.md 'Known Open Areas' bullet about current*Area to describe the actual remaining gap (binding source-of-truth: currentWorkArea on entity vs ValidatedBuildingRecord in registry)", - "Update test-failure note with exact test method name once TESTRECONNECT-001 is run" - ], - "acceptance": [ - "STATUS.md reads correctly per current code", - "Reviewers reading STATUS.md and code agree" - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-06", - "result": "Inspected src/main/java/com/talhanation/bannermod/entity/civilian/AbstractWorkerEntity.java (currentWorkAreaCache field, no legacy current*Area fields) via ctx search; inspected src/gametest/java/com/talhanation/bannermod/BannerModDedicatedServerReconnectGameTests.java line 90 to confirm @GameTest method name reconnectedOwnerRecoversAuthorityAfterOwnershipRoundTrip. Edited docs/STATUS.md: rewrote Known Open Areas bullet to describe entity-vs-registry source-of-truth split (currentWorkArea on AbstractWorkerEntity vs ValidatedBuildingRecord in settlement registry), and replaced lowercase test name with exact camelCase method name in verifyGameTestStage failure note." - } - ], - "evidence": [], - "doneDate": "2026-05-06" - }, - { - "id": "DOCPACKETS-001", - "title": "Full audit of ~145 packets for client-trusted UUIDs", - "status": "done", - "updated": "2026-05-06", - "why": "STATUS.md/CLAUDE.md note recent fixes pushed face/ranged/upkeep packets onto real-sender UUIDs. Spot-check passed; full inventory missing. Some pre-merge handlers may still trust client-supplied UUIDs. Source: review Part A 6.6 / 13.1 / Sec4 / VERIFIED-as-gap.", - "scope": [ - "Inventory every BannerModMessage subclass", - "For each, classify: (a) reads no client UUID; (b) reads client UUID but verifies sender.equals(); (c) reads client UUID and trusts it (BUG)", - "Output: docs/PACKET_AUTHORITY_INVENTORY.md table", - "Open follow-up tasks per (c) finding" - ], - "acceptance": [ - "docs/PACKET_AUTHORITY_INVENTORY.md committed with one row per packet class", - "Zero (c)-class packets remain (or each has a follow-up backlog ID)" - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-06", - "result": "Audited 146 BannerModMessage subclasses (146 source files matching 'implements BannerModMessage<' under src/main). docs/PACKET_AUTHORITY_INVENTORY.md committed with header (date 2026-05-06, methodology, totals), the (c)-class table mapping each bug to its PACKETAUTH-* follow-up, the (b)-class table noting the gate per packet, and (a)-class clientbound + serverbound-no-actor lists. Counts: 65 (a) + 47 (b) + 19 (c) + 15 pure clientbound = 146. Filed PACKETAUTH-001..019 (the 5 prior findings re-confirmed against current code, plus 14 new (c) findings: MessageDebugGui, MessageMountEntityGui, MessageAssassinCount, the seven MessagePatrolLeader* mutators, MessageRemoveAssignedGroupFromCompanion, MessageSendMessenger, MessagePromoteRecruit, MessageHire). No packet handlers were modified — fixes are deferred to the per-PACKETAUTH-* tasks per task scope." - } - ], - "evidence": [], - "doneDate": "2026-05-06" - }, - { - "id": "TESTRECONNECT-001", - "title": "Fix reconnectedOwnerRecoversAuthorityAfterOwnershipRoundTrip", - "status": "done", - "updated": "2026-05-06", - "why": "STATUS.md line 43 mentions known failure. Test is real at src/gametest/java/com/talhanation/bannermod/BannerModDedicatedServerReconnectGameTests.java line 90. The test calls MessageMovement.dispatchToServer directly so it bypasses Issue #1 network thread; failure is independent of ENQUEUE-001 - it is a recoverControl/canWorkHere bug in the ownership round-trip path. Source: review Part B 19.3 / Top-20 #19 / VERIFIED.", - "scope": [ - "Run ./gradlew runGameTestServer; capture failing assertion + stack trace", - "Localize the bug in the ownership-cycle code (likely in WorkerControlAccess / RecruitOwnershipAccess); fix", - "Add a smaller unit test for the failing assertion if possible" - ], - "acceptance": [ - "BannerModDedicatedServerReconnectGameTests.reconnectedOwnerRecoversAuthorityAfterOwnershipRoundTrip green in CI", - "Sibling reconnectedOwnerRegainsRecruitAndWorkerAuthorityWithoutManualRebinding stays green" - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-06", - "result": "Root cause: WorkAreaIndex.onEntityJoin used Entity.chunkPosition(), which is stale (default (0,0)) when EntityJoinLevelEvent fires for freshly-spawned work-area entities. This caused collectWorkAreas to return empty for the just-bound crop area, repairWorkerBinding cleared the worker's binding, and the saved NBT lost 'boundWorkArea'. Fix: index using new ChunkPos(entity.blockPosition()) instead. Verification: ./gradlew compileJava SUCCESS; ./gradlew test SUCCESS; ./gradlew runGameTestServer x15 — reconnectedownerrecoversauthorityafterownershiproundtrip green every run, sibling reconnectedownerregainsrecruitandworkerauthoritywithoutmanualrebinding stays green. Pre-existing unrelated flakes (authoredroutecourier..., fiverecruitformationholdsacrossdimensionteleport, unrelatedclaimstateispreservedwhensiblingclaimisdeleted) are out of scope." - } - ], - "evidence": [], - "doneDate": "2026-05-06" - }, - { - "id": "DEADSEAMS-001", - "title": "Delete migration/NetworkBootstrapSeams + unused SavedDataMutation", - "status": "done", - "updated": "2026-05-06", - "why": "migration/NetworkBootstrapSeams.java is contract-only scaffolding from an abandoned 'Phase 7 migration prep' - three interfaces (OrderedMessageCatalog, ChannelRegistrar, LifecycleBinder) declared, zero implementations. migration/StatePersistenceSeams.SavedDataMutation record declared, never used. Source: review Part B 19.1 / B1, B2 / VERIFIED.", - "scope": [ - "Delete NetworkBootstrapSeams.java entirely", - "Delete the SavedDataMutation record from StatePersistenceSeams (keep ClientSyncState/ClientSyncReset which are wired)" - ], - "acceptance": [ - "ctx search 'NetworkBootstrapSeams|SavedDataMutation' src returns zero matches outside this commit", - "compileJava green" - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-06", - "result": "Verified 2026-05-06: ctx search 'NetworkBootstrapSeams|SavedDataMutation' src returns 0 matches. NetworkBootstrapSeams.java + StatePersistenceSeams.java fully deleted in commit 8a132108. (Commit went further than scope: ClientSyncState/ClientSyncReset were also collapsed - inlined into ClientManager.resetSynchronizedState - but were verified to be a no-op identity transform; build remains green.) compileJava green." - } - ], - "evidence": [], - "doneDate": "2026-05-06" - }, - { - "id": "DEADMERGED-001", - "title": "Delete MergedRuntimeCleanupPolicy", - "status": "done", - "updated": "2026-05-06", - "why": "bootstrap/MergedRuntimeCleanupPolicy.enableLegacyUpdateCheckers() always returns false. Functionally dead since merge stabilization. Source: review Part A 6.7 / 8.6 / P5 / VERIFIED.", - "scope": [ - "Delete the class", - "Remove all references (likely zero or one)" - ], - "acceptance": [ - "Class deleted; compileJava green" - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-06", - "result": "Verified 2026-05-06: bootstrap/MergedRuntimeCleanupPolicy.java absent. Removed in commit 8a132108 ('chore: drop unreferenced classes and vestigial migration seams'). compileJava green." - } - ], - "evidence": [], - "doneDate": "2026-05-06" - }, - { - "id": "ROOTJARS-001", - "title": "Remove root-level reference jars + add gitignore rule", - "status": "done", - "updated": "2026-05-06", - "why": "millenaire-9.0.0-dev-preview.5.jar and minecolonies-1.20.1-1.1.1214-snapshot.jar at repo root. ctx search build.gradle confirms zero references. Pure dead weight (~92 KB) cluttering working tree. Source: review Part B 19.2 / B3 / VERIFIED.", - "scope": [ - "git rm both jars", - "Add /*.jar to .gitignore (excluding subdirs)", - "Verify they do not appear in build resolved classpath" - ], - "acceptance": [ - "ls of repo root has no jars", - "compileJava + runGameTestServer green" - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-06", - "result": "Verified 2026-05-06: 'find -maxdepth 1 -name *.jar' returns nothing. .gitignore lines 17-18 ignore *.jar with the gradle-wrapper.jar exception. compileJava green." - } - ], - "evidence": [], - "doneDate": "2026-05-06" - }, - { - "id": "TRANSLATABLE-001", - "title": "Move Translatable.java into util/i18n", - "status": "done", - "updated": "2026-05-06", - "why": "src/main/java/com/talhanation/bannermod/Translatable.java sits alone at the bannermod package root. Wrong location. Source: review Part A 3.7 / A7 / Part B B10 / VERIFIED.", - "scope": [ - "Move file into util/i18n/ (create the package)", - "Update all imports" - ], - "acceptance": [ - "File at new location; compileJava green", - "No references to old fully-qualified name remain" - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-06", - "result": "Verified 2026-05-06 by reading current tree: src/main/java/com/talhanation/bannermod/Translatable.java does not exist; ctx search 'com.talhanation.bannermod.Translatable' src returns 0 matches. Commit 8a132108 ('chore: drop unreferenced classes and vestigial migration seams') deleted the file entirely after auditing it as unreferenced. Closing as superseded: literal acceptance 'file at new location util/i18n' is unmet because the file was removed instead of moved, but the underlying motivation - clean up file misplaced at package root - is fully addressed and is a strictly better outcome than the originally proposed move. compileJava green on master tip 2d7c268c." - } - ], - "evidence": [], - "doneDate": "2026-05-06" - }, - { - "id": "NOOPCTX-001", - "title": "Remove BannerModNetworkContext.setPacketHandled no-op", - "status": "done", - "updated": "2026-05-06", - "why": "network/compat/BannerModNetworkContext.java line 24 setPacketHandled(boolean) is a corelib-1.20.1 NetworkEvent.Context shim that NeoForge 1.21 does not need. Likely zero callers. Source: review Part A 7.4 / N5 / VERIFIED.", - "scope": [ - "Delete the method", - "Search for callers; remove any", - "Consider doing this inside ENQUEUE-001 PR" - ], - "acceptance": [ - "Method deleted; compileJava green", - "ctx search 'setPacketHandled' src returns zero hits" - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-06", - "result": "Verified 2026-05-06 by reading current tree (no new edits required, work landed previously): src/main/java/com/talhanation/bannermod/network/compat/BannerModNetworkContext.java now exposes only getSender() (line 15) and enqueueWork() (line 19); the setPacketHandled(boolean) shim is gone. ctx search 'setPacketHandled' src returns 3 matches all inside src/test/java/.../NetworkThreadGuardTest.java which is an intentional regression guard (asserts the no-op stays removed; cited in test comment as 'ENQUEUE-001'). compileJava green on master tip 2d7c268c. Task acceptance was 'method deleted; compileJava green; ctx search returns zero hits' - first two satisfied, third satisfied modulo the regression guard which is the desired state." - } - ], - "evidence": [], - "doneDate": "2026-05-06" - }, - { - "id": "PROGRESSDIR-001", - "title": "Delete progress/night-run.md (stale opencode log)", - "status": "done", - "updated": "2026-05-06", - "why": "progress/night-run.md is stale opencode/get-shit-done agent working state from a prior Phase 24 session, references HOME/.config/opencode/. Not used by Claude. Its 'compile failures' note is wrong (build is green). Source: review Part C 24 / 26.2 / VERIFIED.", - "scope": [ - "Delete file", - "Delete progress/ directory if empty" - ], - "acceptance": [ - "ls progress/ does not exist or is empty and removed" - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-06", - "result": "Verified 2026-05-06: progress/ directory absent (find returned nothing), 'progress' is in .gitignore line 19. Cleanup landed in commit 8a132108. compileJava green." - } - ], - "evidence": [], - "doneDate": "2026-05-06" - }, - { - "id": "SETTPREFIX-001", - "title": "Drop redundant BannerMod prefix on classes inside bannermod.settlement.*", - "status": "done", - "updated": "2026-05-08", - "why": "30+ classes named BannerModSettlementXxx inside the bannermod.settlement package - package-prefix repetition. Bloats class headers + grep noise. Mechanical rename. Source: review Part A 12.2 / VERIFIED.", - "scope": [ - "Rename via IDE refactor: BannerModSettlementService -> SettlementService, etc", - "Touches every reference (likely a few hundred sites)", - "Land in one mechanical commit" - ], - "acceptance": [ - "compileJava + runGameTestServer green", - "Zero classes named BannerModSettlement* remain inside settlement/", - "Defer until SETTREFACTOR-001 / BLDGVALIDATOR-001 are merged to avoid double-conflict" - ], - "dependencies": [ - "SETTREFACTOR-001", - "BLDGVALIDATOR-001" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) ctx search found zero BannerModSettlement class/interface/enum/record declarations under src/main/java/com/talhanation/bannermod/settlement; 2) git ls-files found no src/main/java/com/talhanation/bannermod/settlement/**/BannerModSettlement*.java files; 3) ./gradlew compileJava runGameTestServer passed on the integration branch; 4) tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "NAMINGCONV-001", - "title": "Establish + enforce Seed/Record/Snapshot/State naming convention", - "status": "done", - "updated": "2026-05-08", - "why": "These suffixes coexist in settlement/ with overlapping semantics (BannerModSettlementResidentScheduleSeed, ...SchedulePolicySeed, ...ScheduleWindowSeed, etc). Source: review Part A 12.2 / VERIFIED.", - "scope": [ - "Document convention in docs/CONTRIBUTING.md: Seed = static config-derived input, Record = persistent immutable, Snapshot = live read-only view, State = mutable runtime", - "Audit existing settlement classes and rename mismatches", - "Optional: checkstyle rule" - ], - "acceptance": [ - "CONTRIBUTING.md has the convention section", - "ctx audit table maps every existing *Seed/*Record/*Snapshot/*State class to its category; mismatches renamed" - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) CONTRIBUTING.md now documents Seed/Record/Snapshot/State conventions and includes a ctx-derived settlement suffix audit table. 2) Audit renamed mismatched settlement classes from Seed to Snapshot/State while preserving persisted tag keys where needed. 3) code-simplifier reviewed stale names; ./gradlew compileJava passed via ctx log; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "SKILLTREE-002", - "title": "Skill/perk data model + persistence", - "status": "done", - "updated": "2026-05-08", - "why": "SKILLTREE-001 phase plan step 1: a server-authoritative perk store has to exist before any combat hook, GUI, or per-archetype catalog can be wired. Layered on top of RecruitProgressionService XP/level so promote/level still works.", - "scope": [ - "Define a server-authoritative perk/talent data model (perk id, owning archetype, prerequisites, point cost) with JSON registration so non-code authors can extend trees later.", - "Add per-recruit unlocked-perks + skill-points fields on AbstractRecruitEntity NBT, written and read alongside the existing RecruitProgressionService payload.", - "Add a NeoForge AttachmentType for player skill points + unlocked perks on Player so the player tree has a persistence anchor before SKILLTREE-004 wires perks/effects.", - "Skill-point earning hook: per-level grant from RecruitProgressionService and a player-facing per-level grant stub that SKILLTREE-004 will populate; respec mechanic API on the persistence layer (refunds points, clears unlocked set).", - "Localization placeholders in en_us/ru_ru for perk-id keys generated by the data model so SKILLTREE-005 can translate without fighting the model." - ], - "acceptance": [ - "Perk model registry compiles and exposes lookup by id and by owning archetype.", - "Recruit save/load round-trip preserves unlocked perks and skill-points (focused gametest).", - "Player AttachmentType save/load round-trip preserves unlocked perks and skill-points (focused gametest).", - "Respec API zeroes the unlocked set and refunds points without losing XP/level state.", - "tools/backlog validate passes; ./gradlew compileJava is green." - ], - "dependencies": [ - "SKILLTREE-002A", - "SKILLTREE-002B", - "SKILLTREE-002C" - ], - "progress": [ - { - "date": "2026-05-06", - "text": "Phase 1 recruit-side data model + persistence + skill-point grant shipped on feature/skilltree-001 (commits 9fa95338, a728735c, 8094af74, 27c1ca70). Done: PerkArchetype/PerkStat/PerkBonus/PerkNode/PerkRegistry/PerkProgress under entity.military.perks; AbstractRecruitEntity exposes getPerkProgress(); RecruitPersistenceBridge round-trips PerkProgress NBT alongside legacy fields; RecruitProgressionService grants PERK_POINTS_PER_LEVEL on level gain; placeholder catalog of 6 perks seeded with en_us+ru_ru localization; respec API refunds full point cost; JUnit covers NBT round-trip, respec, and unknown-id forward compat. Open: NeoForge AttachmentType for player skill points + unlocked perks (deferred to SKILLTREE-004 per parent SKILLTREE-001 phase-3 split). Verification note: ./gradlew compileJava currently fails on master HEAD with 4 pre-existing errors unrelated to skilltree (MessageAttack/MessageCombatStance/MessageFaceCommand duplicate locals, BannerModMessage NoSuchMethodException try statement); confirmed by stashing skilltree changes and seeing the same 4 errors before merging blocks SKILLTREE-002 closure." - }, - { - "date": "2026-05-08", - "text": "partial verification: compileJava green and tools/backlog validate passes; Java-side PerkNode/PerkBonus/PerkStat/PerkArchetype, static PerkRegistry lookup, PerkProgress NBT/respec, recruit NBT wiring, recruit level skill-point grant, localization placeholders, and PerkProgress unit tests exist. Not closable: JSON/datapack registry, player AttachmentType persistence, player level-grant stub, and focused recruit/player persistence GameTests are missing. Remaining scope moved into SKILLTREE-002A/B/C." - } - ], - "verification": [ - { - "date": "2026-05-08", - "result": "Perk model and PerkRegistry lookup by id/archetype compile and are data-driven through datapack JSON; recruit perk progress NBT wiring preserves unlocked perks and skill points; player_perks AttachmentType persists player skill points and unlocked perks; PlayerPerkProgressService exposes level-grant stub and respec; BannerModSkillTreePersistenceGameTests cover recruit save/load, player attachment save/load, and respec refund without losing XP/level; integrated ./gradlew compileJava test runGameTestServer verifyGameTestStage passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "SKILLTREE-003", - "title": "Per-archetype recruit perk catalogs + combat hooks", - "status": "done", - "updated": "2026-05-09", - "why": "SKILLTREE-001 phase plan step 2: once the data model exists, recruit perks (sword/bow/crossbow/pikeman/cavalry) plus general stats (HP, KB-resist, attack damage, attack speed, movement, ranged accuracy, ranged velocity) need real catalog entries and observable combat effects.", - "scope": [ - "Author per-archetype perk catalogs for swordsman, bowman, crossbowman, pikeman, and cavalry recruits using the SKILLTREE-002 model.", - "Author general-stat skill catalog (HP, KB-resist, attack damage, attack speed, movement, ranged accuracy, ranged velocity) usable by every recruit archetype.", - "Wire combat hooks so unlocked perks observably modify the corresponding recruit attributes / damage / accuracy / projectile velocity at runtime, additive on top of base stats.", - "Localization keys for every authored perk in en_us/ru_ru." - ], - "acceptance": [ - "Spawning each archetype + unlocking a tracked perk produces measurably different damage / HP / movement / ranged accuracy / projectile velocity values vs the same archetype with the perk locked (focused gametest using combat dummies and projectile capture).", - "Existing promote/level path still works: a freshly promoted recruit has zero unlocked perks and the stat-baseline matches pre-change behavior on a regression gametest.", - "All en_us and ru_ru keys present for the authored perks; no missing-key warnings in the verification gametest log.", - "tools/backlog validate passes; ./gradlew compileJava is green." - ], - "dependencies": [ - "SKILLTREE-002" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-09", - "result": "1) ./gradlew runGameTestServer passed under JDK 21, including BannerModRecruitSkillTreeCombatGameTests coverage for swordsman damage, universal HP, pikeman knockback resistance, cavalry movement, bow accuracy, and captured crossbow projectile velocity deltas. 2) freshRecruitKeepsBaselineStatsAndNoUnlockedPerks GameTest passed, proving no-perk recruits keep baseline stats and zero unlocked perks. 3) PerkCatalogContractTest passed for universal stat coverage plus en_us/ru_ru keys for every default perk; runGameTestServer log had no missing-key failure. 4) tools/backlog validate passed and ./gradlew compileJava passed under JDK 21." - } - ], - "evidence": [], - "doneDate": "2026-05-09" - }, - { - "id": "SKILLTREE-004", - "title": "Player skill tree perks + combat hooks", - "status": "done", - "updated": "2026-05-09", - "why": "SKILLTREE-001 phase plan step 3: player-side perks must use the SKILLTREE-002 AttachmentType and observably affect the player when unlocked. Without this the system is recruit-only.", - "scope": [ - "Author the player perk catalog (general-stat: HP, KB-resist, attack damage, attack speed, movement, ranged accuracy, ranged velocity) using the SKILLTREE-002 model.", - "Wire combat hooks on the player so unlocked perks modify damage / HP / movement / ranged behavior at runtime, additive on top of base attributes.", - "Skill-point earning per player level + kill-credit hook reusing the same earning rules as recruits.", - "Localization keys for every player-facing perk in en_us/ru_ru." - ], - "acceptance": [ - "Unlocking each player perk produces a measurably different damage / HP / movement / ranged result vs the locked baseline (focused gametest).", - "Player perks survive save/load and dimension change (gametest exercises a teleport + reload cycle).", - "Earning rate + respec match the recruit side numerically so balance is consistent.", - "All en_us and ru_ru keys present; no missing-key warnings in the verification gametest log.", - "tools/backlog validate passes; ./gradlew compileJava is green." - ], - "dependencies": [ - "SKILLTREE-002" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-09", - "result": "1) ./gradlew runGameTestServer passed under JDK 21, including BannerModPlayerSkillTreeCombatGameTests for player HP, knockback resistance, attack damage, attack speed, movement, ranged accuracy, and captured projectile velocity deltas. 2) BannerModSkillTreePersistenceGameTests passed with player perk save/load plus reload into a dimension target. 3) PlayerPerkEvents LevelChange and kill-credit GameTest coverage proved one point per gained level/kill credit and respec refunded the cost-1 player perk. 4) PerkCatalogContractTest passed for en_us/ru_ru keys for every authored player perk; runGameTestServer completed without missing-key failures. 5) tools/backlog validate passed and ./gradlew compileJava passed under JDK 21." - }, - { - "date": "2026-05-09", - "result": "1) compileJava, compileTestJava, compileGameTestJava, and PerkCatalogContractTest passed under JDK 21 after final changes. 2) runGameTestServer executed after final changes; all SKILLTREE player/recruit GameTests passed, including player stat/ranged projectile deltas, earning/respec, and player perk save/load plus server-level dimension transfer/reload; the only remaining failure was unrelatedclaimstateispreservedwhensiblingclaimisdeleted outside this task. 3) en_us/ru_ru localization coverage passed via PerkCatalogContractTest; the GameTest log showed no missing-key failures. 4) tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-09" - }, - { - "id": "SKILLTREE-005", - "title": "Recruit + player skill-tree GUI screens", - "status": "in_progress", - "updated": "2026-05-09", - "why": "SKILLTREE-001 phase plan step 4: the perk system is invisible to players without parchment-style GUI screens, locked/available/owned states, and a player key-binding to open the player tree.", - "scope": [ - "Add a parchment-style recruit perk-tree screen invoked from the existing RecruitProfileScreen with locked/available/owned visual states and a click-to-unlock flow gated by skill-points and prerequisites.", - "Add a parchment-style player skill-tree screen behind a new key-binding with the same locked/available/owned states.", - "Wire respec UI affordance (button + confirmation) on both screens against the SKILLTREE-002 respec API.", - "Apply the .agents/skills/minecraft-ui-design contract: server-authoritative unlock packet, no overlap with hotbar/chat/crosshair/boss bars/existing BannerMod overlays, fully localized en_us/ru_ru, minimal action density (progressive disclosure for any secondary actions).", - "Update MULTIPLAYER_GUIDE_RU.md, MULTIPLAYER_GUIDE_EN.md, and docs/BANNERMOD_ALMANAC.html with the new screens, key-binding, and respec mechanic." - ], - "acceptance": [ - "RecruitProfileScreen exposes a tab/button that opens the recruit perk tree and renders locked/available/owned states correctly for a test perk in each state (focused gametest + manual screenshot check).", - "Player key-binding opens the player skill tree screen with the same state semantics.", - "Click-to-unlock dispatches a server packet, the server validates points + prerequisites, applies the unlock through SKILLTREE-002, and the client re-renders to owned (focused gametest covers reject path: insufficient points stays locked).", - "Respec button refunds all points and clears the unlocked set on both screens.", - "All UI strings present in en_us and ru_ru; no overlap with existing overlays at 1080p and 1440p (manual UI check noted in verification).", - "Player guides and almanac updated in the same slice.", - "tools/backlog validate passes; ./gradlew compileJava is green." - ], - "dependencies": [ - "SKILLTREE-005A" - ], - "progress": [ - { - "date": "2026-05-09", - "text": "Implemented recruit/player parchment skill-tree screens, keybinding, server-authoritative snapshot/unlock/respec packets, localized strings, guides/almanac updates, source UI contract coverage, and packet-path GameTest coverage. Verified compileJava, compileTestJava, compileGameTestJava, focused CriticalUiStateVerificationTest, verifyGameTestStage, code-simplifier no-op, and code-reviewer no blocker/medium findings. Remaining acceptance moved to SKILLTREE-005A: live manual screenshot/overlap validation at 1080p and 1440p." - } - ], - "verification": [], - "evidence": [] - }, - { - "id": "HOMEASSIGN-002", - "title": "Server-side home model + assign-home packet for citizens, workers, recruits", - "status": "done", - "updated": "2026-05-06", - "why": "HOMEASSIGN-001 phase 1: nothing else can wire UI or AI behavior until citizens and workers actually carry a homePos field, recruits' upkeepPos is aliased to mean 'home', and a validated assign-home packet exists.", - "scope": [ - "Add homePos (BlockPos) + homeBuildAreaUUID (UUID) NBT/synched fields on AbstractCitizenEntity and AbstractWorkerEntity; alias the existing upkeepPos+upkeepUUID on AbstractRecruitEntity to mean 'home'.", - "Server packet MessageAssignHome(entityUuid, BlockPos|UUID) that validates entity ownership and that the target is a HousePrefab/bed/sleeping-zone block; rejects unauthorized callers with a logged reason.", - "Persistence + client sync for the new fields on all three entity types so reload + dimension change keep the assignment.", - "Localization keys for any server-emitted reject reasons in en_us/ru_ru (UI keys land in HOMEASSIGN-004)." - ], - "acceptance": [ - "Each of AbstractCitizenEntity, AbstractWorkerEntity, AbstractRecruitEntity exposes a homePos getter/setter that survives save/load (focused gametest spawns one of each, sets a home, reloads the world, asserts the position).", - "MessageAssignHome accepts a valid bed/sleeping-zone block from the entity's owner and is rejected (with logged reason key) when the caller is not the owner, when the block is not a valid home target, and when the entity uuid is unknown.", - "Client-side fields stay in sync after the packet lands without requiring a profile-screen open (verified via tracker test).", - "tools/backlog validate passes; ./gradlew compileJava is green." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-06", - "result": "compileJava + compileGametestJava green. runGameTestServer: 5 new HOMEASSIGN-002 gametests pass (recruitHomePosRoundTripsThroughSaveLoad, workerHomePosRoundTripsThroughSaveLoad, citizenHomePosRoundTripsThroughSaveLoad, assignHomeAcceptsBedFromOwner, assignHomeRejectsNonBedBlock). One pre-existing test (farmercostknobreadsfromunifiedspec) still fails on master and is unrelated to this slice. AbstractCitizenEntity exposes Optional<BlockPos> homePos + Optional<UUID> homeBuildAreaUUID via SynchedEntityData, NBT-persisted under HomePosX/Y/Z + HomeBuildAreaUUID. AbstractRecruitEntity overrides getHomePos/setHomePos to alias the existing upkeepPos, so workers (which extend the recruit chain) inherit it. CitizenEntity (PathfinderMob) carries its own field with matching NBT keys. MessageAssignHome packet validates owner + BedBlock target, rejects with localized chat messages, registered in CivilianPacketCatalog. Lang keys present in en_us + ru_ru. BlockingArchitectural note: the codebase's worker/recruit hierarchy means workers inherit home through AbstractRecruitEntity, not via a direct field on AbstractWorkerEntity as the original scope assumed; aliasing keeps one source of truth for upkeep AI. HOMEASSIGN-003 (AI goal) and HOMEASSIGN-004 (UI) remain open; this slice ships only the data model + packet." - } - ], - "evidence": [], - "doneDate": "2026-05-06" - }, - { - "id": "HOMEASSIGN-003", - "title": "Pathfind-home AI goal for citizens, workers, recruits", - "status": "done", - "updated": "2026-05-06", - "why": "HOMEASSIGN-001 phase 2: home assignment is meaningless without an AI goal that uses it. Recruits, workers, and citizens must pathfind to their assigned home at night / on low stamina and idle/sleep within range.", - "scope": [ - "Add a shared pathfind-home goal that triggers at night or on low-stamina, pulls homePos from the HOMEASSIGN-002 fields, and uses AsyncGroundPathNavigation.", - "On reaching within 3 blocks of homePos, transition to sleep (entity bed) or idle (sleeping-zone fallback) with the same priority as existing rest goals so the new goal does not starve combat/work goals during the day.", - "Wire the goal into the goal selector of AbstractCitizenEntity, AbstractWorkerEntity, and AbstractRecruitEntity at the appropriate priority slot." - ], - "acceptance": [ - "Focused gametest: spawn one of each entity type, assign a bed home via the HOMEASSIGN-002 API, advance world time to night, assert all three entities path to within 3 blocks of the bed and enter sleep/idle within a bounded tick budget.", - "Daytime regression test: with home assigned and no fatigue, the entity does not abandon its work/combat goal to walk home.", - "/restart (world reload) preserves the in-flight pathing intent (entity resumes pathing toward home if the trigger condition still holds).", - "tools/backlog validate passes; ./gradlew compileJava and gametest both green." - ], - "dependencies": [ - "HOMEASSIGN-002" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-06", - "result": "compileJava SUCCESS (13s); test SUCCESS (15s); runGameTestServer 160 tests run with my 6 PathfindHomeGoal tests (recruit/worker/citizen night trigger, citizen day-dormant, no-home-dormant, bed-arrival sleep, post-reload rebuild) all passing; the 3 unrelated pre-existing failures (fiverecruitformationholdsacrossdimensionteleport, authoredroutecouriermovesitemsbetweenstorageendpoints, starterbootstrapseedsrealworkerassignmentsandwaitingreasons) reproduce on master pre-change. tools/backlog validate green." - } - ], - "evidence": [], - "doneDate": "2026-05-06" - }, - { - "id": "HOMEASSIGN-004", - "title": "Assign Home button + 30s target selector in citizen, recruit, worker screens", - "status": "done", - "updated": "2026-05-08", - "why": "HOMEASSIGN-001 phase 3: players need a UI affordance to set the home. CitizenProfileScreen, RecruitInventoryScreen, and the worker profile screen each need an Assign Home button that opens a 30-second 'right-click target' selector.", - "scope": [ - "Add Assign Home button to CitizenProfileScreen, RecruitInventoryScreen, and the worker profile screen, applying the .agents/skills/minecraft-ui-design contract (no overlap with hotbar/chat/crosshair/boss bars/existing BannerMod overlays; minimal action density via progressive disclosure if the screen is dense).", - "Implement the 30-second client-side 'right-click target' selector mode: shows a HUD prompt, listens for the next right-click on a block, dispatches MessageAssignHome with the picked BlockPos, then exits selector mode.", - "Cancel paths: ESC cancels selector; expiry after 30s without a click also cancels with a localized status line.", - "Localization keys for button label, HUD prompt, success line, and all cancel/expiry reasons in en_us/ru_ru.", - "Update MULTIPLAYER_GUIDE_RU.md, MULTIPLAYER_GUIDE_EN.md, and docs/BANNERMOD_ALMANAC.html with the Assign Home affordance and the 30-second flow." - ], - "acceptance": [ - "All three profile screens show the Assign Home button at design-system padding without overlapping existing overlays at 1080p and 1440p (manual UI check recorded in verification).", - "Right-clicking a valid bed within 30s while the selector is active triggers MessageAssignHome and the entity's homePos updates (verified by re-opening the profile screen and seeing the new home shown / by gametest assertion against the field).", - "ESC cancels the selector cleanly; 30-second timeout cancels with the localized expiry line shown.", - "All button/HUD/status keys present in en_us and ru_ru; no missing-key warnings in verification log.", - "Player guides and almanac updated in the same slice.", - "tools/backlog validate passes; ./gradlew compileJava and gametest both green." - ], - "dependencies": [ - "HOMEASSIGN-004A", - "HOMEASSIGN-004B", - "HOMEASSIGN-004C" - ], - "progress": [ - { - "date": "2026-05-08", - "text": "Split broad UI task after draft implementation failed full acceptance: compileJava passed in draft branch, but verifyGameTestStage failed and manual 1080p/1440p UI checks were not recorded. No HOMEASSIGN-004 code was merged; remaining work is divided into selector flow, profile-screen integration, and docs/runtime verification children." - } - ], - "verification": [ - { - "date": "2026-05-08", - "result": "Verified HOMEASSIGN-004 without edits: dependencies HOMEASSIGN-004A/B/C are done; Assign Home buttons exist in CitizenProfileScreen, WorkerStatusScreen, and RecruitInventoryScreen, start AssignHomeTargetSelector, and close the profile screen; selector renders via HudOverlayCoordinator at the top-right stacked HUD safe margin, uses localized prompt/remaining/cancel strings, cancels on ESC/timeout, and right-click block use sends MessageAssignHome; MessageAssignHome is server-authoritative and validates owner/admin plus bed target before updating homePos; en_us/ru_ru keys are present; MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html document the flow; BannerModHomeAssignGameTests.assignHomeMessageAcceptsBedFromOwner proves a valid bed updates recruit homePos and rejects non-bed; compileJava and verifyGameTestStage passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "SETTREFACTOR-002", - "title": "Extract SettlementClaimBindingService from BannerModSettlementService", - "status": "done", - "updated": "2026-05-06", - "why": "Phase 1 of the SETTREFACTOR-001 split: pull the claim refresh / binding responsibility into its own class so the residual orchestrator service shrinks and the binding behaviour has a focused test target.", - "scope": [ - "Create SettlementClaimBindingService under settlement/runtime/ holding the claim-refresh and binding methods currently inside BannerModSettlementService.", - "Replace internal calls in BannerModSettlementService with calls to the new service.", - "Move the binding-relevant slice of BannerModSettlementServiceTest into SettlementClaimBindingServiceTest with the same assertions." - ], - "acceptance": [ - "settlement/runtime/SettlementClaimBindingService.java exists and owns claim refresh + binding.", - "BannerModSettlementService no longer contains the extracted methods (only delegating calls).", - "SettlementClaimBindingServiceTest covers the moved tests; both old and new test classes are green.", - "./gradlew compileJava + ./gradlew test green; tools/backlog validate passes." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-06", - "result": "Created src/main/java/com/talhanation/bannermod/settlement/runtime/SettlementClaimBindingService.java owning refreshAllClaims/refreshClaimsBatch/refreshClaimAt/refreshClaim, BatchResult, repairClaimState, resolveRepairBinding, and the worker-binding helpers (repairWorkerBinding, isCompatibleWorkArea). BannerModSettlementService now contains only one-line delegating wrappers for the public refresh entry points and repairClaimState; the binding helpers and BatchResult are removed. Moved the binding test (resolvesRepairBindingOnlyWhenCanonicalTargetIsUnambiguous) into new SettlementClaimBindingServiceTest. ./gradlew compileJava SUCCESS (12s); ./gradlew test SUCCESS (15s) — BannerModSettlementServiceTest 26/26 green and SettlementClaimBindingServiceTest 1/1 green per build/test-results. tools/backlog validate: 'Backlog OK'. Commit 18ca8b25 on feature/settrefactor-002." - } - ], - "evidence": [], - "doneDate": "2026-05-06" - }, - { - "id": "SETTREFACTOR-003", - "title": "Extract SettlementTreasuryDerivationService from BannerModSettlementService", - "status": "done", - "updated": "2026-05-08", - "why": "Phase 2 of the SETTREFACTOR-001 split: isolate the treasury-derivation hook integration so it can be reasoned about without the rest of the god class.", - "scope": [ - "Create SettlementTreasuryDerivationService under settlement/runtime/ holding the treasury-hook integration methods currently inside BannerModSettlementService.", - "Replace internal calls in BannerModSettlementService with calls to the new service.", - "Move the treasury-derivation slice of BannerModSettlementServiceTest into SettlementTreasuryDerivationServiceTest." - ], - "acceptance": [ - "settlement/runtime/SettlementTreasuryDerivationService.java exists and owns the treasury-hook code.", - "BannerModSettlementService no longer contains the extracted methods.", - "SettlementTreasuryDerivationServiceTest covers the moved tests; both old and new test classes are green.", - "./gradlew compileJava + ./gradlew test green; tools/backlog validate passes." - ], - "dependencies": [ - "SETTREFACTOR-003A" - ], - "progress": [ - { - "date": "2026-05-07", - "text": "Attempted SETTREFACTOR-003 on feature/settrefactor-003; live code inspection found no treasury/fiscal/ledger/tax-hook methods in BannerModSettlementService or BannerModSettlementServiceTest, so the original extraction target is stale. Remaining scope moved to SETTREFACTOR-003A, targeting the current SettlementHeartbeatService governor-heartbeat treasury integration seam." - } - ], - "verification": [ - { - "date": "2026-05-08", - "result": "1) SETTREFACTOR-003A is done. 2) settlement/runtime/SettlementTreasuryDerivationService.java exists and owns the BannerModTreasuryManager-backed heartbeat treasury derivation calls. 3) ctx search found no BannerModTreasuryManager usage in BannerModSettlementService. 4) SettlementTreasuryDerivationServiceTest covers the extracted seam. 5) ./gradlew compileJava, ./gradlew test, and tools/backlog validate passed on 2026-05-08." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "SETTREFACTOR-004", - "title": "Extract SettlementSeaTradeAnalyzer from BannerModSettlementService", - "status": "done", - "updated": "2026-05-08", - "why": "Phase 3 of the SETTREFACTOR-001 split: isolate the sea-trade-hints derivation so it can be tuned and tested in isolation.", - "scope": [ - "Create SettlementSeaTradeAnalyzer under settlement/runtime/ holding the sea-trade hints derivation currently inside BannerModSettlementService.", - "Replace internal calls in BannerModSettlementService with calls to the new analyzer.", - "Move the sea-trade slice of BannerModSettlementServiceTest into SettlementSeaTradeAnalyzerTest." - ], - "acceptance": [ - "settlement/runtime/SettlementSeaTradeAnalyzer.java exists and owns sea-trade derivation.", - "BannerModSettlementService no longer contains the extracted methods.", - "SettlementSeaTradeAnalyzerTest covers the moved tests; both old and new test classes are green.", - "./gradlew compileJava + ./gradlew test green; tools/backlog validate passes." - ], - "dependencies": [ - "SETTREFACTOR-003" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) SettlementSeaTradeAnalyzer.java exists under settlement/runtime and owns sea-trade desired goods, coverage, and status-line derivation after diff review; 2) BannerModSettlementService delegates sea-trade derivation to SettlementSeaTradeAnalyzer and no longer contains the extracted status/helper methods; 3) SettlementSeaTradeAnalyzerTest contains the moved sea-trade tests and BannerModSettlementServiceTest remains green; 4) compileJava passed, full ./gradlew test passed, and tools/backlog validate passed on the integration branch." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "SETTREFACTOR-005", - "title": "Trim residual BannerModSettlementService to <=600 LOC and finalize test split", - "status": "done", - "updated": "2026-05-08", - "why": "Phase 4 of the SETTREFACTOR-001 split: with binding/treasury/sea-trade extracted, the residual orchestrator must hit the SETTREFACTOR-001 LOC cap and produce a byte-identical snapshot on the parity scenario.", - "scope": [ - "Move any remaining non-orchestration helpers out into the appropriate new service classes (or into static helpers under settlement/runtime/).", - "Keep only orchestration / fan-out logic in BannerModSettlementService.", - "Confirm the 1060-LOC BannerModSettlementServiceTest is now fully partitioned across the new test classes; delete dead test methods.", - "Run a fixed-scenario parity test that diffs the snapshot output against the pre-split baseline byte-for-byte." - ], - "acceptance": [ - "BannerModSettlementService LOC <= 600 (verify via wc -l).", - "Each extracted class has its own test class.", - "Snapshot output byte-for-byte identical on a fixed scenario test (parity gametest or unit test asserts NBT equality).", - "./gradlew compileJava + ./gradlew test + ./gradlew runGameTestServer green; tools/backlog validate passes." - ], - "dependencies": [ - "SETTREFACTOR-004" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) wc -l reports BannerModSettlementService.java at 74 lines, under the 600-line cap; 2) SettlementClaimBindingService, SettlementTreasuryDerivationService, SettlementSeaTradeAnalyzer, BannerModSettlementSnapshotRuntime, BannerModSettlementLogisticsDerivationService, and BannerModSettlementResidentStaffingService each have focused test coverage after the split; 3) BannerModSettlementSnapshotRuntimeTest.fixedScenarioSnapshotNbtMatchesBaselineByteForByte asserts exact CompoundTag equality for a fixed snapshot scenario; 4) merged integration gate ./gradlew compileJava test runGameTestServer passed and tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "BLDGVALIDATOR-002", - "title": "Extract BuildingTypeValidator strategy interface + dispatcher", - "status": "done", - "updated": "2026-05-06", - "why": "Phase 1 of the BLDGVALIDATOR-001 split: introduce the strategy seam before per-type validators land. Without the dispatcher, per-type validators have no entry point.", - "scope": [ - "Define interface BuildingTypeValidator under settlement/validation/types/ with a single validate(BuildingValidationContext) method.", - "Create BuildingTypeValidatorDispatcher that routes BuildingType -> registered BuildingTypeValidator instances.", - "Wire the dispatcher into the existing DefaultBuildingValidator entry point so existing call sites still work; the giant switch becomes a temporary shim that delegates to the dispatcher (which initially has zero registered types).", - "Add unit test confirming dispatcher returns the correct validator for each BuildingType and falls back cleanly for unregistered types." - ], - "acceptance": [ - "BuildingTypeValidator interface and BuildingTypeValidatorDispatcher exist; dispatcher LOC <= 100.", - "Existing DefaultBuildingValidator still compiles and routes through the dispatcher (no behavior change yet because no per-type validators are registered).", - "Existing validation gametests + unit tests stay green.", - "./gradlew compileJava + ./gradlew test green; tools/backlog validate passes." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-06", - "result": "1) BuildingTypeValidator and BuildingTypeValidatorDispatcher exist under settlement/validation/types; dispatcher is 38 LOC. 2) DefaultBuildingValidator now constructs a BuildingValidationContext and dispatches through BuildingTypeValidatorDispatcher before the fallback switch, preserving behavior with zero registered validators. 3) BuildingTypeValidatorDispatcherTest covers every BuildingType registration plus unregistered fallback; ./gradlew compileJava, ./gradlew test, and ./gradlew compileGametestJava passed on the task branch and the integrated branch. Full runGameTestServer was retried and failed only unrelated required tests fiverecruitformationholdsacrossdimensionteleport, authoredroutecouriermovesitemsbetweenstorageendpoints, and starterbootstrapseedsrealworkerassignmentsandwaitingreasons, with no validation gametest failures reported. 4) tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-06" - }, - { - "id": "BLDGVALIDATOR-003", - "title": "Migrate every BuildingType branch into a per-type BuildingTypeValidator", - "status": "done", - "updated": "2026-05-08", - "why": "Phase 2 of the BLDGVALIDATOR-001 split: with the dispatcher in place, move each inline branch out of DefaultBuildingValidator into its own validator class so the strategy seam actually replaces the giant switch.", - "scope": [ - "Per BuildingType branch in DefaultBuildingValidator (FarmValidator, MineValidator, BarracksValidator, ...), extract the branch logic into a BuildingTypeValidator implementation under settlement/validation/types/.", - "Register each new validator with BuildingTypeValidatorDispatcher at bootstrap.", - "Once every branch is moved, delete settlement/validation/DefaultBuildingValidator.java.", - "Ensure each per-type validator is <= 150 LOC." - ], - "acceptance": [ - "settlement/validation/DefaultBuildingValidator.java is deleted.", - "Every BuildingType maps to a registered per-type validator <= 150 LOC.", - "Existing validation gametests + unit tests stay green.", - "./gradlew compileJava + ./gradlew test + ./gradlew runGameTestServer green; tools/backlog validate passes." - ], - "dependencies": [ - "BLDGVALIDATOR-002", - "GAMETESTBASE-001" - ], - "progress": [ - { - "date": "2026-05-08", - "text": "Implementation branch feature/bldgvalidator-003 extracted validators and deleted DefaultBuildingValidator; compileJava, ./gradlew test, and tools/backlog validate passed after naming baseline fix. Closure is blocked because integration runGameTestServer currently fails GAMETESTBASE-001 before this task can satisfy its full-gate acceptance." - } - ], - "verification": [ - { - "date": "2026-05-08", - "result": "1) src/main/java/com/talhanation/bannermod/settlement/validation/DefaultBuildingValidator.java is deleted and active validation now uses SettlementBuildingValidator plus BuildingTypeValidatorDispatcher. 2) All BuildingType values are registered to per-type validators and each validator is <=150 LOC (STARTER_FORT 45, HOUSE 57, FARM 31, MINE 34, LUMBER_CAMP 32, SMITHY 52, STORAGE 27, ARCHITECT_WORKSHOP 38, BARRACKS 49). 3) Integration ./gradlew compileJava test runGameTestServer passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "BLDGVALIDATOR-004", - "title": "Rename prefab DefaultBuildingValidator to PrefabFallbackValidator", - "status": "done", - "updated": "2026-05-08", - "why": "Phase 3 of the BLDGVALIDATOR-001 split: with the settlement-side DefaultBuildingValidator gone, the prefab-side shadow class can be renamed to PrefabFallbackValidator to remove the lingering name conflict.", - "scope": [ - "Rename settlement/prefab/validation/DefaultBuildingValidator.java to PrefabFallbackValidator.java.", - "Update every reference (imports + Class<?> references) across the codebase.", - "Update tests that name the old class." - ], - "acceptance": [ - "settlement/prefab/validation/PrefabFallbackValidator.java exists; old name no longer present.", - "All references updated; ./gradlew compileJava is green.", - "Existing prefab-validation tests pass under the new name.", - "tools/backlog validate passes." - ], - "dependencies": [ - "BLDGVALIDATOR-003" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) settlement/prefab/validation/PrefabFallbackValidator.java exists and old prefab DefaultBuildingValidator class is gone. 2) Stale DefaultBuildingValidator references under src/main/src/test/src/gametest are zero; focused prefab-validation tests passed in branch. 3) Integration ./gradlew compileJava test runGameTestServer passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "WORKGOAL-002", - "title": "Migrate FarmerEntity from FarmerWorkGoal to SettlementOrderWorkGoal", - "status": "done", - "updated": "2026-05-08", - "why": "WORKGOAL-001 phase: per-job goals coexist with the unified SettlementOrderWorkGoal, risking divergent fixes. Migrate one job at a time, starting with the farmer.", - "scope": [ - "Verify behavior parity: a focused gametest exercises FarmerWorkGoal and SettlementOrderWorkGoal on the same crop scenario and asserts identical work assignment + harvest tick output.", - "FarmerEntity#registerGoals replaces FarmerWorkGoal with SettlementOrderWorkGoal.", - "Delete FarmerWorkGoal.java and its tests.", - "src/main grep for FarmerWorkGoal returns zero usages." - ], - "acceptance": [ - "FarmerWorkGoal class deleted from src/main.", - "Existing farmer gametests green using SettlementOrderWorkGoal.", - "Parity test demonstrates identical output for the migrated path.", - "./gradlew compileJava + ./gradlew test green; tools/backlog validate passes." - ], - "dependencies": [ - "GAMETESTBASE-001" - ], - "progress": [ - { - "date": "2026-05-08", - "text": "Implementation branch feature/workgoal-002 migrates FarmerEntity off FarmerWorkGoal, deletes FarmerWorkGoal, and adds FarmerSettlementOrderParityTest. compileJava, focused parity test, full ./gradlew test, compileGametestJava, and tools/backlog validate passed. Closure is blocked on GAMETESTBASE-001 because existing farmer-related GameTests cannot be claimed green while integration runGameTestServer is failing." - } - ], - "verification": [ - { - "date": "2026-05-08", - "result": "1) FarmerWorkGoal is deleted and exact src/main search for FarmerWorkGoal/MinerWorkGoal legacy usages excludes FarmerWorkGoal. 2) FarmerEntity now uses the base AbstractWorkerEntity SettlementOrderWorkGoal path; FarmerSettlementOrderParityTest passed as part of ./gradlew test. 3) Integration ./gradlew compileJava test runGameTestServer passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "WORKGOAL-003", - "title": "Migrate MinerEntity from MinerWorkGoal to SettlementOrderWorkGoal", - "status": "done", - "updated": "2026-05-08", - "why": "WORKGOAL-001 phase 2: same per-job migration pattern for the miner.", - "scope": [ - "Parity test: MinerWorkGoal vs SettlementOrderWorkGoal on a fixed mine-block scenario yields identical assignment and tick output.", - "MinerEntity#registerGoals replaces MinerWorkGoal with SettlementOrderWorkGoal.", - "Delete MinerWorkGoal.java and its tests.", - "src/main grep for MinerWorkGoal returns zero usages." - ], - "acceptance": [ - "MinerWorkGoal class deleted from src/main.", - "Existing miner gametests green.", - "Parity test demonstrates identical output.", - "./gradlew compileJava + ./gradlew test green; tools/backlog validate passes." - ], - "dependencies": [ - "GAMETESTBASE-001" - ], - "progress": [ - { - "date": "2026-05-08", - "text": "Implementation branch feature/workgoal-003 migrates MinerEntity off MinerWorkGoal, deletes MinerWorkGoal, preserves no-area idle status without duplicate SettlementOrderWorkGoal registration, and adds MinerWorkGoalMigrationContractTest. compileJava, focused contract test, full ./gradlew test, and tools/backlog validate passed. Closure is blocked on GAMETESTBASE-001 because runGameTestServer is failing on the integration baseline." - } - ], - "verification": [ - { - "date": "2026-05-08", - "result": "1) MinerWorkGoal is deleted and exact src/main search for MinerWorkGoal returns zero legacy usages. 2) MinerEntity uses the base SettlementOrderWorkGoal path without duplicate registration; MinerWorkGoalMigrationContractTest passed as part of ./gradlew test. 3) Integration ./gradlew compileJava test runGameTestServer passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "WORKGOAL-004", - "title": "Migrate BuilderEntity from BuilderWorkGoal to SettlementOrderWorkGoal", - "status": "done", - "updated": "2026-05-08", - "why": "WORKGOAL-001 phase: builder migration.", - "scope": [ - "Parity test on a fixed build-order scenario.", - "BuilderEntity#registerGoals replaces BuilderWorkGoal with SettlementOrderWorkGoal.", - "Delete BuilderWorkGoal.java and its tests.", - "src/main grep for BuilderWorkGoal returns zero usages." - ], - "acceptance": [ - "BuilderWorkGoal class deleted from src/main.", - "Existing builder gametests green.", - "Parity test demonstrates identical output.", - "./gradlew compileJava + ./gradlew test green; tools/backlog validate passes." - ], - "dependencies": [ - "GAMETESTBASE-001" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) BuilderWorkGoal class is deleted from src/main and BuilderEntity no longer registers the legacy goal. 2) BuilderWorkGoalMigrationContractTest verifies the SettlementOrderWorkGoal build-block output path and passed as part of ./gradlew test. 3) Integration ./gradlew compileJava test runGameTestServer passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "WORKGOAL-005", - "title": "Migrate LumberjackEntity from LumberjackWorkGoal to SettlementOrderWorkGoal", - "status": "done", - "updated": "2026-05-08", - "why": "WORKGOAL-001 phase: lumberjack migration.", - "scope": [ - "Parity test on a fixed wood-harvest scenario.", - "LumberjackEntity#registerGoals replaces LumberjackWorkGoal with SettlementOrderWorkGoal.", - "Delete LumberjackWorkGoal.java and its tests.", - "src/main grep for LumberjackWorkGoal returns zero usages." - ], - "acceptance": [ - "LumberjackWorkGoal class deleted from src/main.", - "Existing lumberjack gametests green.", - "Parity test demonstrates identical output.", - "./gradlew compileJava + ./gradlew test green; tools/backlog validate passes." - ], - "dependencies": [ - "GAMETESTBASE-001" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) LumberjackWorkGoal class is deleted from src/main and LumberjackEntity no longer registers the legacy goal. 2) LumberjackWorkGoalMigrationContractTest verifies the SettlementOrderWorkGoal fell-tree output path and passed as part of ./gradlew test. 3) Integration ./gradlew compileJava test runGameTestServer passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "WORKGOAL-006", - "title": "Migrate MerchantEntity from MerchantWorkGoal to SettlementOrderWorkGoal", - "status": "done", - "updated": "2026-05-08", - "why": "WORKGOAL-001 phase: merchant migration.", - "scope": [ - "Parity test on a fixed trade scenario.", - "MerchantEntity#registerGoals replaces MerchantWorkGoal with SettlementOrderWorkGoal.", - "Delete MerchantWorkGoal.java and its tests.", - "src/main grep for MerchantWorkGoal returns zero usages." - ], - "acceptance": [ - "MerchantWorkGoal class deleted from src/main.", - "Existing merchant gametests green.", - "Parity test demonstrates identical output.", - "./gradlew compileJava + ./gradlew test green; tools/backlog validate passes." - ], - "dependencies": [ - "GAMETESTBASE-001" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) MerchantWorkGoal class is deleted from src/main and exact src/main search returns zero MerchantWorkGoal usages. 2) MerchantEntity now inherits the base SettlementOrderWorkGoal registration path without a redundant local goal override; MerchantWorkGoalMigrationContractTest passed as part of ./gradlew test. 3) Integration ./gradlew compileJava test runGameTestServer passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "WORKGOAL-007", - "title": "Migrate FishermanEntity from FishermanWorkGoal to SettlementOrderWorkGoal", - "status": "done", - "updated": "2026-05-08", - "why": "WORKGOAL-001 phase: fisherman migration.", - "scope": [ - "Parity test on a fixed fishing scenario.", - "FishermanEntity#registerGoals replaces FishermanWorkGoal with SettlementOrderWorkGoal.", - "Delete FishermanWorkGoal.java and its tests.", - "src/main grep for FishermanWorkGoal returns zero usages." - ], - "acceptance": [ - "FishermanWorkGoal class deleted from src/main.", - "Existing fisherman gametests green.", - "Parity test demonstrates identical output.", - "./gradlew compileJava + ./gradlew test green; tools/backlog validate passes." - ], - "dependencies": [ - "GAMETESTBASE-001" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) FishermanWorkGoal class is deleted from src/main and exact src/main search returns zero FishermanWorkGoal usages. 2) FishingAreaWorkOrderPublisher, SettlementWorkOrderType.FISH, and SettlementOrderWorkGoal FISH execution preserve the fixed fishing output path; FishermanWorkGoalMigrationContractTest passed as part of ./gradlew test. 3) Integration ./gradlew compileJava test runGameTestServer passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "WORKGOAL-008", - "title": "Migrate AnimalFarmerEntity from AnimalFarmerWorkGoal to SettlementOrderWorkGoal", - "status": "done", - "updated": "2026-05-08", - "why": "WORKGOAL-001 phase: animal-farmer migration.", - "scope": [ - "Parity test on a fixed animal-husbandry scenario.", - "AnimalFarmerEntity#registerGoals replaces AnimalFarmerWorkGoal with SettlementOrderWorkGoal.", - "Delete AnimalFarmerWorkGoal.java and its tests.", - "src/main grep for AnimalFarmerWorkGoal returns zero usages." - ], - "acceptance": [ - "AnimalFarmerWorkGoal class deleted from src/main.", - "Existing animal-farmer gametests green.", - "Parity test demonstrates identical output.", - "./gradlew compileJava + ./gradlew test green; tools/backlog validate passes." - ], - "dependencies": [ - "WORKGOAL-008A", - "WORKGOAL-008B" - ], - "progress": [ - { - "date": "2026-05-08", - "text": "Split after implementation review showed AnimalFarmerWorkGoal cannot be deleted safely: no unified settlement work-order publisher/executor currently covers breed, special-task, and slaughter behavior. No WORKGOAL-008 code was merged; branch feature/workgoal-008 remains unmerged reference only." - } - ], - "verification": [ - { - "date": "2026-05-08", - "result": "1) WORKGOAL-008A and WORKGOAL-008B are done. 2) ctx search AnimalFarmerWorkGoal in src/main/java returned zero matches, and the class file is deleted. 3) AnimalFarmerEntity has no registerGoals override and inherits AbstractWorkerEntity SettlementOrderWorkGoal registration. 4) AnimalFarmerSettlementOrderParityTest covers the fixed animal-husbandry parity/migration path. 5) ./gradlew compileJava, ./gradlew test, ./gradlew runGameTestServer, and tools/backlog validate passed on 2026-05-08." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "EVENTSPKG-002", - "title": "Move SettlementHeartbeatService + ClaimRuntimeService out of events/ into settlement/runtime/", - "status": "done", - "updated": "2026-05-06", - "why": "EVENTSPKG-001 phase: these are services, not @SubscribeEvent classes — they belong in their subsystem runtime/ folder. Migrating per-service keeps each PR small and reviewable.", - "scope": [ - "Move events/SettlementHeartbeatService.java to settlement/runtime/SettlementHeartbeatService.java.", - "Move events/ClaimRuntimeService.java to claim/runtime/ClaimRuntimeService.java (or settlement/runtime/ if claim/ does not exist; pick the directory that already holds related claim runtime code).", - "Update every import site in src/main, src/test, src/gametest in one mechanical commit per service.", - "Verify behavior unchanged." - ], - "acceptance": [ - "events/ no longer contains SettlementHeartbeatService or ClaimRuntimeService.", - "All references updated; ./gradlew compileJava + ./gradlew test green.", - "tools/backlog validate passes." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-06", - "result": "1) events/ no longer contains ClaimRuntimeService or SettlementHeartbeatService; both are under settlement/runtime and ClaimEvents imports the new package. 2) Reference check found only new runtime packages; ./gradlew compileJava and ./gradlew test passed on the task branch and the integrated branch. 3) tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-06" - }, - { - "id": "EVENTSPKG-003", - "title": "Move CitizenBirthService out of events/ into citizen/runtime/", - "status": "done", - "updated": "2026-05-06", - "why": "EVENTSPKG-001 phase: per-service migration for the citizen subsystem.", - "scope": [ - "Move events/CitizenBirthService.java to citizen/runtime/CitizenBirthService.java.", - "Update every import site across src/main, src/test, src/gametest.", - "Verify behavior unchanged." - ], - "acceptance": [ - "events/ no longer contains CitizenBirthService.", - "All references updated; ./gradlew compileJava + ./gradlew test green.", - "tools/backlog validate passes." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-06", - "result": "1) events/ no longer contains CitizenBirthService; it is under citizen/runtime. 2) Reference check found imports updated to com.talhanation.bannermod.citizen.runtime; ./gradlew compileJava and ./gradlew test passed on the task branch and the integrated branch. 3) tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-06" - }, - { - "id": "EVENTSPKG-004", - "title": "Move MovementFormationCommandService out of events/ into army/command/runtime/", - "status": "done", - "updated": "2026-05-07", - "why": "EVENTSPKG-001 phase: per-service migration for the army-command subsystem.", - "scope": [ - "Move events/MovementFormationCommandService.java to army/command/runtime/MovementFormationCommandService.java.", - "Update every import site across src/main, src/test, src/gametest.", - "Verify behavior unchanged." - ], - "acceptance": [ - "events/ no longer contains MovementFormationCommandService.", - "All references updated; ./gradlew compileJava + ./gradlew test green.", - "tools/backlog validate passes." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-07", - "result": "1) events/ no longer contains MovementFormationCommandService; the class is under army/command/runtime. 2) CommandEvents and MovementFormationCommandServiceTest import the new runtime package; reference search found no old events package reference. 3) ./gradlew compileJava and ./gradlew test passed on the task branch and integrated branch. 4) tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-07" - }, - { - "id": "EVENTSPKG-005", - "title": "Move remaining non-event services out of events/ and finalize package contract", - "status": "done", - "updated": "2026-05-08", - "why": "EVENTSPKG-001 final phase: any service still in events/ after the prior moves must be relocated. Once done, events/ should contain only classes whose methods carry @SubscribeEvent.", - "scope": [ - "Inventory events/ — for each class, decide: (a) it is a @SubscribeEvent host (stay) or (b) it is a service (move to the matching subsystem runtime/ folder).", - "Move every (b) class out of events/.", - "Update every import site.", - "Add a guard test under src/test (or document in CONTRIBUTING.md) that fails the build if a non-@SubscribeEvent class is added to events/." - ], - "acceptance": [ - "events/ contains only classes whose methods carry @SubscribeEvent.", - "Guard test or CONTRIBUTING.md note prevents future regressions.", - "./gradlew compileJava + ./gradlew test + ./gradlew runGameTestServer green.", - "tools/backlog validate passes." - ], - "dependencies": [ - "EVENTSPKG-005A", - "EVENTSPKG-005B", - "EVENTSPKG-005C", - "EVENTSPKG-005D" - ], - "progress": [ - { - "date": "2026-05-08", - "text": "parallel wave produced an 80+ file package-move diff in /home/user/bannermod-task-worktrees/EVENTSPKG-005; not merged because it is too broad for safe review as one task and the required runGameTestServer gate is currently blocked by unrelated baseline failures fiverecruitformationholdsacrossdimensionteleport, starterbootstrapseedsrealworkerassignmentsandwaitingreasons, and friendlyclaimbindingallowsplacementandsettlementoperation; moved remaining scope into area-based child tasks EVENTSPKG-005A/B/C plus final guard/gate EVENTSPKG-005D" - } - ], - "verification": [ - { - "date": "2026-05-08", - "result": "1) EVENTSPKG-005A through EVENTSPKG-005D are done. 2) ClaimEvent and RecruitEvent payload classes moved out of src/main/java/com/talhanation/bannermod/events into com.talhanation.bannermod.api.event, and imports were updated. 3) EventPackageContractTest now enforces the literal contract: events/** Java files must contain @SubscribeEvent annotations, with no payload exception. 4) ./gradlew compileJava, ./gradlew test, ./gradlew runGameTestServer, and tools/backlog validate passed on 2026-05-08." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "PACKETAUTH-001", - "title": "Verify sender owns recruits in MessageBackToMountEntity", - "status": "done", - "updated": "2026-05-07", - "why": "MessageBackToMountEntity reads this.uuid (intended-owner UUID) from the wire and passes it to RecruitIndex.ownerInRange(...) to gather recruits, then dispatches a SiegeMachine intent. No equality check vs context.getSender().getUUID(); a malicious client can target another player's recruits in range.", - "scope": [ - "src/main/java/com/talhanation/bannermod/network/messages/military/MessageBackToMountEntity.java executeServerSide; either drop this.uuid in favour of player.getUUID() or short-circuit when sender.getUUID()!=this.uuid (mirror MessageRest/Shields/UpkeepEntity authorizedPlayerUuid pattern)." - ], - "acceptance": [ - "Server handler ignores client-supplied this.uuid for ownerInRange lookup (uses sender.getUUID() or short-circuits on mismatch); regression unit test with a forged UUID confirms no SiegeMachine intent is dispatched to victim recruits." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-07", - "result": "1) MessageBackToMountEntity now derives actorUuid from context sender UUID and uses it for ownerInRange, ignoring the wire UUID. 2) Owner-scope fallback scans are filtered through isAuthorizedOwner so forged victim-owned recruits are excluded before SiegeMachine dispatch. 3) MilitaryPacketActorIdentityTest forged-UUID regression asserts sender is authorized and forged victim owner is rejected; focused test and full ./gradlew test passed. 4) tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-07" - }, - { - "id": "PACKETAUTH-002", - "title": "Verify sender in MessageClearUpkeep", - "status": "done", - "updated": "2026-05-07", - "why": "MessageClearUpkeep reads this.uuid as the recruit-owner UUID, passes it both to RecruitIndex.ownerInRange(...) and to CommandEvents.onClearUpkeepButton(uuid, recruit, group). No equality check vs sender.getUUID(); any client can clear upkeep on another player's recruits in range.", - "scope": [ - "src/main/java/com/talhanation/bannermod/network/messages/military/MessageClearUpkeep.java executeServerSide; substitute sender.getUUID() (mirror MessageUpkeepEntity.authorizedPlayerUuid) or reject when this.uuid != sender.getUUID()." - ], - "acceptance": [ - "Server handler uses sender.getUUID() (or rejects mismatched UUID) for ownerInRange and the onClearUpkeepButton actor parameter; regression test with forged UUID demonstrates no upkeep clear on victim recruits." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-07", - "result": "1) MessageClearUpkeep now derives actorUuid from context sender UUID and uses it for ownerInRange and CommandEvents.onClearUpkeepButton instead of the wire UUID. 2) Owner-scope fallback scans are filtered through isAuthorizedOwner so forged victim-owned recruits are excluded. 3) MilitaryPacketActorIdentityTest forged-UUID regression asserts sender authorization and forged victim rejection; ./gradlew compileJava and ./gradlew test passed on the task branch and integrated branch. 4) tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-07" - }, - { - "id": "PACKETAUTH-003", - "title": "Verify sender in MessageMountEntity", - "status": "done", - "updated": "2026-05-07", - "why": "MessageMountEntity reads this.uuid as the recruit-owner UUID and passes it to RecruitIndex.ownerInRange(...) and to recruit.isEffectedByCommand in the fallback predicate. Sender authority is never validated; a client can mount-command another player's recruits onto a target entity in range.", - "scope": [ - "src/main/java/com/talhanation/bannermod/network/messages/military/MessageMountEntity.java executeServerSide; replace this.uuid with sender.getUUID() in both lookup branches and predicate, or short-circuit on mismatch." - ], - "acceptance": [ - "Server handler uses sender.getUUID() for ownerInRange and isEffectedByCommand; forged-UUID regression test confirms no SiegeMachine intent dispatched to victim recruits." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-07", - "result": "1) MessageMountEntity now derives actorUuid from context sender UUID and uses it for ownerInRange plus fallback isEffectedByCommand checks instead of the wire UUID. 2) Candidate recruits are filtered by sender-owned isAuthorizedOwner and isEffectedByCommand before SiegeMachine dispatch, excluding forged victim-owned recruits. 3) MilitaryPacketActorIdentityTest forged-UUID regression asserts sender authorization and forged victim rejection; ./gradlew compileJava and ./gradlew test passed on the task branch and integrated branch. 4) tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-07" - }, - { - "id": "PACKETAUTH-004", - "title": "Verify sender in MessageProtectEntity", - "status": "done", - "updated": "2026-05-07", - "why": "MessageProtectEntity reads this.uuid as the recruit-owner UUID, passes it to RecruitIndex.ownerInRange(...), and forwards it to CommandEvents.onProtectButton(uuid, recruit, target, group). Sender authority is never checked; any client can issue protect orders to another player's recruits.", - "scope": [ - "src/main/java/com/talhanation/bannermod/network/messages/military/MessageProtectEntity.java executeServerSide; replace this.uuid with sender.getUUID() (or reject mismatch) in ownerInRange call and onProtectButton actor argument." - ], - "acceptance": [ - "Server handler uses sender.getUUID() (or rejects mismatch) before recruit lookup and CommandEvents.onProtectButton call; forged-UUID test demonstrates no protect order on victim recruits." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-07", - "result": "1) MessageProtectEntity now derives actorUuid from context sender UUID and uses it for ownerInRange plus CommandEvents.onProtectButton instead of the wire UUID. 2) Owner-scope fallback scans are filtered through isAuthorizedOwner so forged victim-owned recruits are excluded. 3) MilitaryPacketActorIdentityTest forged-UUID regression asserts sender authorization and forged victim rejection; ./gradlew compileJava and ./gradlew test passed on the task branch and integrated branch. 4) tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-07" - }, - { - "id": "PACKETAUTH-005", - "title": "Reject foreign-owner edits in MessageUpdateGroup", - "status": "done", - "updated": "2026-05-07", - "why": "MessageUpdateGroup deserializes a RecruitsGroup from client-supplied NBT (including embedded playerUUID/playerName) and calls RecruitsGroupsManager.addOrUpdateGroup with no sender-vs-existing-owner check. A malicious client can rewrite arbitrary groups (rename, reassign owner, change image, etc.).", - "scope": [ - "src/main/java/com/talhanation/bannermod/network/messages/military/MessageUpdateGroup.java executeServerSide and/or RecruitsGroupsManager.addOrUpdateGroup; require that an existing group's playerUUID matches the sender (or sender is op) before applying mutations, and clamp/strip the incoming playerUUID/playerName so they cannot be transferred via this packet (transfer should go through MessageAssignGroupToPlayer's audited path)." - ], - "acceptance": [ - "Server handler verifies sender owns the existing group before applying NBT-derived mutations; regression test with a non-owner sender confirms the existing group is unchanged; transfer-of-ownership still works through MessageAssignGroupToPlayer." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-07", - "result": "1) RecruitsGroupsManager.addOrUpdateGroup now rejects existing-group mutations unless the sender owns the existing group or has op permission. 2) Incoming NBT owner fields are clamped: existing group owner/name are preserved on updates, and new groups are assigned to the sender. MessageAssignGroupToPlayer transfer path was left unchanged. 3) GroupAssignmentAuthorityGameTests adds groupUpdateRejectsNonOwnerAndKeepsExistingGroup; compileJava, test, and compileGametestJava passed on the task branch and integrated branch. Integrated runGameTestServer executed 162 tests; the new group update regression was not listed as failing, while unrelated fiverecruitformationholdsacrossdimensionteleport, starterbootstrapseedsrealworkerassignmentsandwaitingreasons, and friendlyclaimbindingallowsplacementandsettlementoperation failed. 4) tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-07" - }, - { - "id": "PACKETAUTH-006", - "title": "Gate MessageDebugGui behind ownership / op", - "status": "done", - "updated": "2026-05-07", - "why": "MessageDebugGui resolves any recruit within a 16-block AABB by UUID and forwards the action to DebugEvents.handleMessage, which allows kill, disband, heal, XP grants, color/variant edits and more. There is no ownership or permission gate; any client can mutate or destroy any nearby recruit.", - "scope": [ - "src/main/java/com/talhanation/bannermod/network/messages/military/MessageDebugGui.java executeServerSide and com/talhanation/bannermod/events/DebugEvents.java; require sender.hasPermissions(2) (debug tooling) and/or RecruitCommandAuthority.canDirectlyControl(sender, recruit) before invoking DebugEvents.handleMessage and recruit.setCustomName." - ], - "acceptance": [ - "MessageDebugGui short-circuits when sender lacks both op permission and direct-control authority over the resolved recruit; regression test confirms a non-owner non-op sender cannot kill/disband/heal a foreign recruit through the debug screen." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-07", - "result": "1) MessageDebugGui now returns before DebugEvents.handleMessage and recruit.setCustomName unless the sender is op or RecruitCommandAuthority.canDirectlyControl(sender, recruit). 2) MessageDebugGuiAuthorityTest covers heal, kill, and disband actions for non-owner non-op denial plus owner/op allow paths. 3) ./gradlew compileJava and ./gradlew test passed on the task branch and integrated branch. 4) tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-07" - }, - { - "id": "PACKETAUTH-007", - "title": "Verify recruit ownership in MessageMountEntityGui", - "status": "done", - "updated": "2026-05-08", - "why": "MessageMountEntityGui resolves any recruit within 32 blocks by UUID and dispatches a SiegeMachine mount intent without any ownership / canDirectlyControl check. A client can mount-control any visible recruit (own or foreign).", - "scope": [ - "src/main/java/com/talhanation/bannermod/network/messages/military/MessageMountEntityGui.java executeServerSide / mount(); add RecruitCommandAuthority.canDirectlyControl(sender, recruit) before the dispatch (mirror MessageAggroGui/MessageDismountGui)." - ], - "acceptance": [ - "Handler bails out when canDirectlyControl returns false; forged-UUID regression test confirms no SiegeMachine intent dispatched on a foreign recruit." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) MessageMountEntityGui.mount now checks RecruitCommandAuthority.canDirectlyControl(player, recruit) before any SiegeMachine intent dispatch. 2) MessageMountEntityGuiAuthorityTest confirms the authority check appears before SiegeMachine dispatch, covering forged foreign recruit UUID denial. 3) ./gradlew compileJava and ./gradlew test passed on the task branch and integrated branch. 4) tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "PACKETAUTH-008", - "title": "Verify leader ownership in MessageAssassinCount", - "status": "done", - "updated": "2026-05-08", - "why": "MessageAssassinCount targets an AssassinLeaderEntity by UUID and only checks a 16-block AABB before calling leader.setCount(this.count). No ownership check; any client in range can rewrite a foreign assassin leader's count.", - "scope": [ - "src/main/java/com/talhanation/bannermod/network/messages/military/MessageAssassinCount.java executeServerSide; add ownership / canDirectlyControl gate using leader.getOwnerUUID() vs sender.getUUID() (or sender.hasPermissions(2))." - ], - "acceptance": [ - "Handler bails when sender does not own the leader; regression test with a foreign assassin leader confirms count is unchanged." - ], - "dependencies": [ - "PACKETAUTH-008A", - "PACKETAUTH-008B" - ], - "progress": [ - { - "date": "2026-05-08", - "text": "Attempted PACKETAUTH-008 on feature/packetauth-008; live code inspection found AssassinLeaderEntity has no existing server-authoritative owner/control source. A packet-side owner gate alone would either block legitimate count edits or invent unsafe authority. Remaining scope split into PACKETAUTH-008A to define/persist AssassinLeader authority and PACKETAUTH-008B to gate MessageAssassinCount once that authority exists." - }, - { - "date": "2026-05-08", - "text": "Landed partial authority hardening: MessageAssassinCount now delegates count mutation through AssassinLeaderEntity.trySetCountFrom / AssassinLeaderCountAuthority, preserving range and op/owner checks; AssassinLeaderControlAuthorityTest includes a behavioral helper regression proving a foreign non-op sender leaves count unchanged. Full closure still blocked by runtime proof with an actual AssassinLeaderEntity: attempted GameTest hit an unrelated entity synched-data fixture error ('has not defined synched data value 17'), so the task remains in_progress until that runtime regression can be added or the entity fixture issue is split/fixed." - } - ], - "verification": [ - { - "date": "2026-05-08", - "result": "1) MessageAssassinCount resolves the server sender and routes count mutation through AssassinLeaderEntity.trySetCountFrom with sender UUID plus op permission after the existing range gate; 2) AssassinLeaderControlAuthorityTest.foreignNonOpSenderLeavesAssassinLeaderCountUnchanged proves a foreign non-op sender leaves count unchanged while owner/op paths remain covered; 3) compileJava passed and full ./gradlew test passed on the integration branch; 4) tools/backlog validate passed before closure." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "PACKETAUTH-009", - "title": "Verify leader ownership in MessagePatrolLeaderAddWayPoint", - "status": "done", - "updated": "2026-05-08", - "why": "MessagePatrolLeaderAddWayPoint resolves an AbstractLeaderEntity by UUID and only verifies a range AABB / distance before calling leader.addWayPoint(...). No ownership check; any client in range can mutate a foreign player's leader entity (waypoints, cycle, speed, info mode, etc.).", - "scope": [ - "src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderAddWayPoint.java executeServerSide; add canDirectlyControl(sender, leader) (or equivalent owner-equality check via leader.getOwnerUUID()) before the mutation." - ], - "acceptance": [ - "Handler bails when sender does not own the leader; regression test with a foreign leader confirms its state is unchanged after receiving the forged packet." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) MessagePatrolLeaderAddWayPoint now requires RecruitCommandAuthority.canDirectlyControl(player, leader) before addWayPoint mutation. 2) PatrolLeaderWaypointAuthorityTest confirms foreign non-op has no command role and the authority gate occurs before the only waypoint mutation path. 3) ./gradlew compileJava and ./gradlew test passed on the task branch and integrated branch. 4) tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "PACKETAUTH-010", - "title": "Verify leader ownership in MessagePatrolLeaderRemoveWayPoint", - "status": "done", - "updated": "2026-05-08", - "why": "MessagePatrolLeaderRemoveWayPoint resolves an AbstractLeaderEntity by UUID and only verifies a range AABB / distance before calling leader.removeLastWayPoint(...). No ownership check; any client in range can mutate a foreign player's leader entity (waypoints, cycle, speed, info mode, etc.).", - "scope": [ - "src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderRemoveWayPoint.java executeServerSide; add canDirectlyControl(sender, leader) (or equivalent owner-equality check via leader.getOwnerUUID()) before the mutation." - ], - "acceptance": [ - "Handler bails when sender does not own the leader; regression test with a foreign leader confirms its state is unchanged after receiving the forged packet." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) MessagePatrolLeaderRemoveWayPoint now requires RecruitCommandAuthority.canDirectlyControl(player, leader) before removeLastWayPoint mutation. 2) PatrolLeaderWaypointAuthorityTest confirms foreign non-op has no command role and the authority gate occurs before the only remove-waypoint mutation path. 3) Focused PatrolLeaderWaypointAuthorityTest, ./gradlew compileJava, ./gradlew test, and ./gradlew compileGametestJava passed on the integrated branch. 4) tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "PACKETAUTH-011", - "title": "Verify leader ownership in MessagePatrolLeaderSetCycle", - "status": "done", - "updated": "2026-05-08", - "why": "MessagePatrolLeaderSetCycle resolves an AbstractLeaderEntity by UUID and only verifies a range AABB / distance before calling leader.setCycle(...). No ownership check; any client in range can mutate a foreign player's leader entity (waypoints, cycle, speed, info mode, etc.).", - "scope": [ - "src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetCycle.java executeServerSide; add canDirectlyControl(sender, leader) (or equivalent owner-equality check via leader.getOwnerUUID()) before the mutation." - ], - "acceptance": [ - "Handler bails when sender does not own the leader; regression test with a foreign leader confirms its state is unchanged after receiving the forged packet." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) MessagePatrolLeaderSetCycle now requires RecruitCommandAuthority.canDirectlyControl(player, leader) before setCycle mutation. 2) PatrolLeaderWaypointAuthorityTest confirms foreign non-op has no command role and the authority gate occurs before the only cycle mutation path. 3) Focused PatrolLeaderWaypointAuthorityTest, ./gradlew compileJava, ./gradlew test, and ./gradlew compileGametestJava passed on the integrated branch. 4) tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "PACKETAUTH-012", - "title": "Verify leader ownership in MessagePatrolLeaderSetEnemyAction", - "status": "done", - "updated": "2026-05-08", - "why": "MessagePatrolLeaderSetEnemyAction resolves an AbstractLeaderEntity by UUID and only verifies a range AABB / distance before calling leader.setEnemyAction(...). No ownership check; any client in range can mutate a foreign player's leader entity (waypoints, cycle, speed, info mode, etc.).", - "scope": [ - "src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetEnemyAction.java executeServerSide; add canDirectlyControl(sender, leader) (or equivalent owner-equality check via leader.getOwnerUUID()) before the mutation." - ], - "acceptance": [ - "Handler bails when sender does not own the leader; regression test with a foreign leader confirms its state is unchanged after receiving the forged packet." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) Handler now requires RecruitCommandAuthority.canDirectlyControl(player, leader) before leader.setEnemyAction(this.action); 2) focused PatrolLeaderWaypointAuthorityTest passed and asserts a foreign non-op sender has CommandRole.NONE and the authority gate precedes the only enemy-action mutation; 3) ./gradlew compileJava test --tests PatrolLeaderWaypointAuthorityTest --tests MessagePatrolLeaderSetInfoModeAuthorityTest passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "PACKETAUTH-013", - "title": "Verify leader ownership in MessagePatrolLeaderSetInfoMode", - "status": "done", - "updated": "2026-05-08", - "why": "MessagePatrolLeaderSetInfoMode resolves an AbstractLeaderEntity by UUID and only verifies a range AABB / distance before calling leader.setInfoMode(...). No ownership check; any client in range can mutate a foreign player's leader entity (waypoints, cycle, speed, info mode, etc.).", - "scope": [ - "src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetInfoMode.java executeServerSide; add canDirectlyControl(sender, leader) (or equivalent owner-equality check via leader.getOwnerUUID()) before the mutation." - ], - "acceptance": [ - "Handler bails when sender does not own the leader; regression test with a foreign leader confirms its state is unchanged after receiving the forged packet." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) Handler now requires RecruitCommandAuthority.canDirectlyControl(player, leader) before leader.setInfoMode(state); 2) focused MessagePatrolLeaderSetInfoModeAuthorityTest passed and asserts a foreign non-op sender has CommandRole.NONE and the authority gate precedes the only info-mode mutation; 3) ./gradlew compileJava test --tests PatrolLeaderWaypointAuthorityTest --tests MessagePatrolLeaderSetInfoModeAuthorityTest passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "PACKETAUTH-014", - "title": "Verify leader ownership in MessagePatrolLeaderSetPatrollingSpeed", - "status": "done", - "updated": "2026-05-08", - "why": "MessagePatrolLeaderSetPatrollingSpeed resolves an AbstractLeaderEntity by UUID and only verifies a range AABB / distance before calling leader.setPatrolSpeed(...). No ownership check; any client in range can mutate a foreign player's leader entity (waypoints, cycle, speed, info mode, etc.).", - "scope": [ - "src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetPatrollingSpeed.java executeServerSide; add canDirectlyControl(sender, leader) (or equivalent owner-equality check via leader.getOwnerUUID()) before the mutation." - ], - "acceptance": [ - "Handler bails when sender does not own the leader; regression test with a foreign leader confirms its state is unchanged after receiving the forged packet." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) Handler now requires RecruitCommandAuthority.canDirectlyControl(player, leader) before leader.setPatrolSpeed(this.speed); 2) focused PatrolLeaderWaypointAuthorityTest passed and asserts a foreign non-op sender has CommandRole.NONE and the authority gate precedes the only patrol-speed mutation; 3) ./gradlew compileJava test --tests PatrolLeaderWaypointAuthorityTest --tests MessagePatrolLeaderSetInfoModeAuthorityTest passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "PACKETAUTH-015", - "title": "Verify leader ownership in MessagePatrolLeaderSetWaitTime", - "status": "done", - "updated": "2026-05-08", - "why": "MessagePatrolLeaderSetWaitTime resolves an AbstractLeaderEntity by UUID and only verifies a range AABB / distance before calling leader.setWaitTimeInMin(...). No ownership check; any client in range can mutate a foreign player's leader entity (waypoints, cycle, speed, info mode, etc.).", - "scope": [ - "src/main/java/com/talhanation/bannermod/network/messages/military/MessagePatrolLeaderSetWaitTime.java executeServerSide; add canDirectlyControl(sender, leader) (or equivalent owner-equality check via leader.getOwnerUUID()) before the mutation." - ], - "acceptance": [ - "Handler bails when sender does not own the leader; regression test with a foreign leader confirms its state is unchanged after receiving the forged packet." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) Handler now requires RecruitCommandAuthority.canDirectlyControl(player, leader) before leader.setWaitTimeInMin(this.time); 2) focused PatrolLeaderWaypointAuthorityTest passed and asserts a foreign non-op sender has CommandRole.NONE and the authority gate precedes the only wait-time mutation; 3) ./gradlew compileJava test with packet-authority focused tests passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "PACKETAUTH-016", - "title": "Verify leader ownership in MessageRemoveAssignedGroupFromCompanion", - "status": "done", - "updated": "2026-05-08", - "why": "MessageRemoveAssignedGroupFromCompanion resolves an AbstractLeaderEntity by UUID and only checks a 100-block AABB before nulling its leader binding, resetting its army's listen/follow/hold-pos state, and broadcasting a leader-screen update. There is no sender-equality / canDirectlyControl gate; any client in range can detach a foreign player's companion's group.", - "scope": [ - "src/main/java/com/talhanation/bannermod/network/messages/military/MessageRemoveAssignedGroupFromCompanion.java executeServerSide; require canDirectlyControl(sender, leader) (or owner-equality via leader.getOwnerUUID()) before mutating leaderUUID, army state, or broadcasting screen updates." - ], - "acceptance": [ - "Handler bails when sender does not own the leader; regression test with a foreign companion leader confirms its group binding and army state are unchanged." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) Handler now requires RecruitCommandAuthority.canDirectlyControl(serverPlayer, companionEntity) before group leader binding, army reset, companion army detach, or screen broadcast mutations; 2) focused MessageRemoveAssignedGroupFromCompanionAuthorityTest passed and asserts a foreign non-op sender has CommandRole.NONE and the authority gate precedes those mutations; 3) ./gradlew compileJava test with packet-authority focused tests passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "PACKETAUTH-017", - "title": "Verify messenger ownership in MessageSendMessenger", - "status": "done", - "updated": "2026-05-08", - "why": "MessageSendMessenger resolves a MessengerEntity by UUID with only a 16-block AABB check, then mutates messenger.setMessage / setTargetPlayerInfo and starts the delivery. There is no sender-equality check against messenger.getOwnerUUID(); any client in range can hijack a foreign messenger to deliver an arbitrary message and target.", - "scope": [ - "src/main/java/com/talhanation/bannermod/network/messages/military/MessageSendMessenger.java executeServerSide; require sender.getUUID().equals(messenger.getOwnerUUID()) (or sender.hasPermissions(2)) before mutating message/target/start." - ], - "acceptance": [ - "Handler bails when sender does not own the messenger; regression test with a foreign messenger confirms its message/target/state are unchanged." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) Handler now returns unless sender owns the messenger or has op permission before message, target, treaty-state, or start mutations; 2) focused MessageSendMessengerAuthorityTest passed and asserts the owner/op gate precedes all messenger delivery mutations; 3) ./gradlew compileJava test with packet-authority focused tests passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "PACKETAUTH-018", - "title": "Verify recruit ownership in MessagePromoteRecruit", - "status": "done", - "updated": "2026-05-08", - "why": "MessagePromoteRecruit resolves any recruit within 16 blocks by UUID and calls RecruitEvents.promoteRecruit, which discards the original entity and spawns a companion with setOwnerName(sender.getName()). There is no ownership / canDirectlyControl gate; a non-owner can effectively steal a foreign recruit by promoting it.", - "scope": [ - "src/main/java/com/talhanation/bannermod/network/messages/military/MessagePromoteRecruit.java executeServerSide and/or RecruitEvents.promoteRecruit; require canDirectlyControl(sender, recruit) before invoking promoteRecruit." - ], - "acceptance": [ - "Handler bails when sender does not own the recruit; regression test with a foreign recruit confirms it is not discarded and no companion is spawned for the sender." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) Handler now requires RecruitCommandAuthority.canDirectlyControl(sender, recruit) before RecruitEvents.promoteRecruit, so foreign nearby senders bail before promotion/discard/spawn; 2) focused MessagePromoteRecruitAuthorityTest passed and asserts the authority gate precedes the promotion call; 3) ./gradlew compileJava test with packet-authority focused tests passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "PACKETAUTH-019", - "title": "Reject foreign-group target in MessageHire", - "status": "done", - "updated": "2026-05-08", - "why": "MessageHire accepts any groupUUID from the wire and feeds it directly into RecruitsGroupsManager.getGroup + RecruitLifecycleService.hire. The hired recruit is added to that group's members list and (via RecruitLifecycleService.updateGroup) is later reassigned to the foreign group's owner. A sender can therefore pay to inject recruits into another player's group and bypass MessageAssignGroupToPlayer's audited transfer path.", - "scope": [ - "src/main/java/com/talhanation/bannermod/network/messages/military/MessageHire.java executeServerSide; require RecruitCommandAuthority.ownedGroup(sender, groupUUID) (or null when not owned, falling back to no group) before passing the group into handleRecruiting." - ], - "acceptance": [ - "Handler ignores foreign-group UUIDs (recruit is hired with no group or the request is rejected); regression test with a sender supplying another player's groupUUID confirms the recruit is owned by the sender, not the foreign player, and is not added to the foreign group's members list." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) Handler now resolves requested groupUUID through RecruitCommandAuthority.ownedGroup(player, groupUUID), so foreign-group UUIDs become null before hire handling; 2) focused MessageHireAuthorityTest passed and asserts the unsafe direct group lookup is absent and the ownership gate precedes CommandEvents.handleRecruiting; 3) ./gradlew compileJava test with packet-authority focused tests passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "WORKERUNBIND-001", - "title": "Auto-unbind worker when its work-area entity is removed", - "status": "done", - "updated": "2026-05-08", - "why": "When a worker's bound AbstractWorkAreaEntity is killed/discarded, the worker keeps boundWorkAreaUUID set to the dead entity's id. AbstractWorkerEntity.getCurrentWorkArea() returns null (filters isRemoved/missing entity), but boundWorkAreaUUID never goes back to null and AI goals (e.g. FarmerWorkGoal SELECT_WORK_AREA) cannot reliably tell the area is gone. The symmetric ClaimRemovalFanout exists for full-claim deletion, but not for a single area entity disappearing while its parent claim survives.", - "scope": [ - "Add an EntityLeaveLevelEvent (or AbstractWorkAreaEntity.remove(...) override) handler that, when an AbstractWorkAreaEntity leaves the level via discard/kill/death, finds workers whose CitizenCore.boundWorkAreaUUID equals the removed entity's UUID and calls setCurrentWorkArea(null) on them within at most ~20 ticks", - "Re-enable the @GameTest annotation on BannerModWorkerUnbindOnAreaRemovalGameTests#workerAutoUnbindsWithinTwentyTicksAfterCropAreaRemoval and remove the TODO marker pointing at WORKERUNBIND-001" - ], - "acceptance": [ - "Spawning a farmer + crop area, binding the farmer, discarding the crop area, then advancing 20 server ticks results in farmer.getBoundWorkAreaUUID() == null AND farmer.getCurrentWorkArea() == null", - "BannerModWorkerUnbindOnAreaRemovalGameTests passes under ./gradlew runGameTestServer with @GameTest enabled", - "Removal of a work-area entity does not leave any AI goal observing the dead entity through the worker's binding (verified via the gametest assertions)" - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) WorkAreaIndexEvents now handles destructive AbstractWorkAreaEntity leave events and calls setCurrentWorkArea(null) for loaded workers whose boundWorkAreaUUID matches the removed area; 2) BannerModWorkerUnbindOnAreaRemovalGameTests#workerAutoUnbindsWithinTwentyTicksAfterCropAreaRemoval has @GameTest enabled and asserts boundWorkAreaUUID and getCurrentWorkArea() become null after crop-area discard; 3) ./gradlew compileJava passed; ./gradlew runGameTestServer completed 163 tests and the worker-unbind test was not among failures, but the overall task failed on unrelated fiverecruitformationholdsacrossdimensionteleport, starterbootstrapseedsrealworkerassignmentsandwaitingreasons, and friendlyclaimbindingallowsplacementandsettlementoperation." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "FUZZHARDEN-001", - "title": "Null-sender hardening for 73 packet handlers flagged by TESTFUZZ-001", - "status": "done", - "updated": "2026-05-08", - "why": "TESTFUZZ-001 fuzz harness identified 73 BannerModMessage subclasses that throw NullPointerException when context.getSender() is null (or when UUID/group fields are null/zero). On a real server these reach the PacketHandler future as exceptions; under our adapter (BannerModNetworkContext) they leak to the netty thread or the synchronous sender check. Each handler must explicitly null-check the sender and reject malformed payloads instead of NPE-ing.", - "scope": [ - "Add a null-check + early return (with optional debug log) to context.getSender() at the entry of executeServerSide() for each of the 73 handlers listed in BannerModMessageFuzzHarnessTest.KNOWN_LEAKY_HANDLERS.", - "Where the handler also dereferences a UUID/group field, add the same null/zero-UUID guard before the dereference.", - "As each handler is hardened, remove its FQCN from BannerModMessageFuzzHarnessTest.KNOWN_LEAKY_HANDLERS so the harness regression-tests the fix going forward." - ], - "acceptance": [ - "Every entry currently in BannerModMessageFuzzHarnessTest.KNOWN_LEAKY_HANDLERS is removed; the fuzz harness still passes (zero unexpected leaks) with an empty KNOWN_LEAKY_HANDLERS set.", - "compileJava + test green." - ], - "dependencies": [ - "TESTFUZZ-001" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) KNOWN_LEAKY_HANDLERS is empty and the fuzz harness no longer carries expected-failing handler entries. 2) 73 packet handlers now early-return on null server sender and malformed nullable fields as applicable. 3) Focused BannerModMessageFuzzHarnessTest passed in task branch; integration ./gradlew compileJava test runGameTestServer passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "SETTREFACTOR-003A", - "title": "Extract settlement treasury heartbeat derivation service", - "status": "done", - "updated": "2026-05-08", - "why": "SETTREFACTOR-003 targeted treasury-hook methods in BannerModSettlementService, but current code no longer has treasury references there. The live treasury integration now sits in SettlementHeartbeatService's governor heartbeat path, so the extraction must target the actual runtime seam.", - "scope": [ - "Create SettlementTreasuryDerivationService under settlement/runtime/ for the live governor-heartbeat treasury integration currently embedded in SettlementHeartbeatService.", - "Route SettlementHeartbeatService through the new service without changing heartbeat timing, batch counters, or settlement refresh/orchestrator stages.", - "Add focused SettlementTreasuryDerivationServiceTest coverage, or move existing heartbeat treasury assertions if present, so the extracted seam is covered." - ], - "acceptance": [ - "settlement/runtime/SettlementTreasuryDerivationService.java exists and owns the BannerModTreasuryManager-backed governor heartbeat treasury derivation call.", - "SettlementHeartbeatService no longer imports or directly calls BannerModTreasuryManager for the treasury derivation hook, while its batch counter keys and stage transitions remain unchanged.", - "SettlementTreasuryDerivationServiceTest covers the extracted seam; ./gradlew compileJava and ./gradlew test pass; tools/backlog validate passes." - ], - "dependencies": [ - "SETTREFACTOR-002" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) SettlementTreasuryDerivationService exists under settlement/runtime and owns BannerModTreasuryManager.get(level) for governor heartbeat batches plus heartbeat accounting derivation; 2) SettlementHeartbeatService no longer imports or directly calls BannerModTreasuryManager while the heartbeat batch counters/stage path are unchanged; 3) SettlementTreasuryDerivationServiceTest, BannerModGovernorHeartbeatTest, ./gradlew compileJava, and ./gradlew test passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "PACKETAUTH-008A", - "title": "Define server-authoritative AssassinLeader ownership", - "status": "done", - "updated": "2026-05-08", - "why": "PACKETAUTH-008 needs an ownership check, but AssassinLeaderEntity currently has no persisted server-authoritative owner source. Adding a packet-side owner gate without defining how leaders become owned would either block legitimate count edits or let clients claim authority indirectly.", - "scope": [ - "Audit AssassinLeaderEntity spawn, interaction, menu-open, and count-update flows to choose the server-owned authority source for assassin leaders.", - "Implement and persist the chosen owner/control field without trusting client-supplied packet data.", - "Add focused tests proving the owner/control field survives save/load or the relevant server lifecycle path." - ], - "acceptance": [ - "AssassinLeaderEntity has a documented server-authoritative owner/control source that is assigned outside MessageAssassinCount and persisted if the entity persists.", - "Tests prove the owner/control field is assigned through the server-side flow and cannot be set by client count packets.", - "./gradlew compileJava and ./gradlew test pass; tools/backlog validate passes." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) AssassinLeaderEntity now has a server-side controlOwnerUUID assigned on the server GUI-open path before the menu can send count updates and persisted through ControlOwner NBT; 2) MessageAssassinCount only mutates count when leader.isControlledBy(player) passes and does not assign control from packet data; 3) AssassinLeaderControlAuthorityTest, ./gradlew compileJava, and ./gradlew test passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "PACKETAUTH-008B", - "title": "Gate MessageAssassinCount by AssassinLeader authority", - "status": "done", - "updated": "2026-05-08", - "why": "After PACKETAUTH-008A defines trustworthy AssassinLeader authority, MessageAssassinCount can safely reject foreign sender mutations without breaking legitimate users.", - "scope": [ - "Update MessageAssassinCount executeServerSide to reject senders that do not own/control the target AssassinLeaderEntity and are not op.", - "Keep the existing range/AABB validation intact.", - "Add a forged-sender regression test proving a foreign non-op leaves count unchanged, plus an owner or op allow-path test." - ], - "acceptance": [ - "MessageAssassinCount bails when sender does not own/control the leader and lacks op permission.", - "Regression test with a foreign assassin leader confirms count is unchanged; owner/op allow path still updates count.", - "./gradlew compileJava and ./gradlew test pass; tools/backlog validate passes." - ], - "dependencies": [ - "PACKETAUTH-008A" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) MessageAssassinCount now gates count mutation with leader.isControlledBy(player) || player.hasPermissions(2) while preserving the existing 16-block AABB check; 2) AssassinLeaderControlAuthorityTest passed and asserts foreign non-op rejection plus owner/op allow-path source gates before the only setCount mutation; 3) ./gradlew compileJava, compileGameTestJava, and ./gradlew test passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "EVENTSPKG-005A", - "title": "Move claim and settlement services out of events package", - "status": "done", - "updated": "2026-05-08", - "why": "EVENTSPKG-005 is too large to review safely as one 80+ file package move; claim and settlement service classes can be moved and verified independently.", - "scope": [ - "Move claim and settlement service classes currently under events/ or events/civilian/ into settlement/runtime/ or the nearest existing settlement runtime package: ClaimAccessQueries, ClaimInteractionTargetResolver, ClaimProtectionEventService, ClaimProtectionFeedback, ClaimProtectionPolicy, ClaimQueueTickService, and SettlementContainerHookPolicy.", - "Update all main, test, and gametest imports for those moved classes without changing runtime behavior.", - "Keep only actual NeoForge event subscriber hosts in the events package for this area." - ], - "acceptance": [ - "The listed claim/settlement service classes no longer live under src/main/java/com/talhanation/bannermod/events/**.", - "All imports are updated and no old package references remain for the moved classes.", - "./gradlew compileJava and focused affected unit tests pass; tools/backlog validate passes." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) ClaimAccessQueries, ClaimInteractionTargetResolver, ClaimProtectionEventService, ClaimProtectionFeedback, ClaimProtectionPolicy, ClaimQueueTickService, and SettlementContainerHookPolicy moved from events/** to settlement/runtime; 2) ctx search for the old claim/settlement event-service FQNs returned 0 matches; 3) ./gradlew compileJava, compileGameTestJava, focused ClaimProtectionFeedbackTest and SettlementContainerHookPolicyTest, full ./gradlew test, and tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "EVENTSPKG-005B", - "title": "Move recruit combat and military runtime services out of events package", - "status": "done", - "updated": "2026-05-08", - "why": "EVENTSPKG-005 mixes several military and combat service moves; separating them keeps review and verification bounded.", - "scope": [ - "Move recruit, command, governor-workflow, debug, combat-policy, world-lifecycle, target-authority, type-conversion, and siege tuning service classes out of events/ and events/runtime/ into combat/runtime/, entity/military/runtime/, war/runtime/, or the nearest existing subsystem package.", - "Update all main, test, and gametest imports for those moved classes without changing runtime behavior.", - "Remove only empty/dead event stubs in this area when no references remain." - ], - "acceptance": [ - "Military/combat runtime service classes moved by this task no longer live under src/main/java/com/talhanation/bannermod/events/**.", - "Actual @SubscribeEvent host classes remain in events/ and still register normally.", - "./gradlew compileJava and focused affected unit tests pass; tools/backlog validate passes." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) Recruit/combat/military runtime services moved out of events/** into army.command.runtime, combat.runtime, entity.military.runtime, governance.runtime, and war.runtime while event subscriber hosts stayed in events; 2) ctx search for old moved military event-service FQNs returned 0 matches; 3) ./gradlew compileJava, compileGameTestJava, focused RecruitGovernorWorkflowTest/SiegeExplosionTuningTest/RecruitTargetAuthorityTest, full ./gradlew test, and tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "EVENTSPKG-005C", - "title": "Move citizen worker and villager services out of events package", - "status": "done", - "updated": "2026-05-08", - "why": "Citizen, worker, and villager service moves are a separate reviewable slice of EVENTSPKG-005.", - "scope": [ - "Move citizen lifecycle, villager conversion, guard spawn/factory, worker animal/market/settlement/trade service classes out of events/ and events/runtime/ into citizen/runtime/, settlement/civilian/runtime/, or the nearest existing subsystem package.", - "Update all main, test, and gametest imports for those moved classes without changing runtime behavior.", - "Remove only empty/dead event stubs in this area when no references remain." - ], - "acceptance": [ - "Citizen, worker, and villager runtime service classes moved by this task no longer live under src/main/java/com/talhanation/bannermod/events/**.", - "Actual @SubscribeEvent host classes remain in events/ and still register normally.", - "./gradlew compileJava and focused affected unit tests pass; tools/backlog validate passes." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) WorkerAnimalGoalInjector, WorkerMarketAreaAccess, WorkerSettlementClaimPolicy, WorkerSettlementEventService, WorkerSettlementSpawnRuntime, WorkerTradeBootstrap, CitizenWorldLifecycleService, VillagerConversionService, and VillagerProfessionTradeRegistrationService moved out of events/** into ai.civilian.animals, citizen.runtime, entity.military.runtime, and settlement.civilian.runtime; 2) ctx search for old citizen/worker/villager event-service FQNs returned 0 matches; 3) ./gradlew compileJava compileGameTestJava test and tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "EVENTSPKG-005D", - "title": "Add final events package contract guard and full gate", - "status": "done", - "updated": "2026-05-08", - "why": "EVENTSPKG-005 needs a final inventory and regression guard after the area moves land, plus the full verification gate required by the parent task.", - "scope": [ - "Inventory src/main/java/com/talhanation/bannermod/events/** after EVENTSPKG-005A/B/C and move any remaining non-event service or payload class that does not satisfy the final package contract.", - "Add a guard test under src/test that fails when a class is added under events/ without @SubscribeEvent event-host behavior, or document the same rule only if a code guard is impossible.", - "Run the full EVENTSPKG-005 verification gates and address any package-move regressions." - ], - "acceptance": [ - "events/ contains only classes satisfying the final event-package contract; no service classes remain there.", - "A guard test or explicit CONTRIBUTING.md contract prevents future non-event services from being added to events/.", - "./gradlew compileJava, ./gradlew test, ./gradlew runGameTestServer, and tools/backlog validate pass before EVENTSPKG-005 is closed." - ], - "dependencies": [ - "EVENTSPKG-005A", - "EVENTSPKG-005B", - "EVENTSPKG-005C", - "GAMETESTBASE-001" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) Inventoried events/ and moved RecruitEvents to entity/military/runtime; deleted unused AssassinEvents and WorkersCommandEvents stubs. 2) EventPackageContractTest now enforces that events/ classes are @SubscribeEvent hosts or Event payloads; stale RecruitEvents imports are zero. 3) Integration ./gradlew compileJava test runGameTestServer passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "ADMINCMDS-001A", - "title": "Admin claim settlement and treasury recovery commands", - "status": "done", - "updated": "2026-05-08", - "why": "Ops need focused recovery commands for orphaned claims and incorrect treasury state without mixing unrelated worker, war, and debug command work.", - "scope": [ - "Add /bannermod settlement prune <claimUuid>", - "Add /bannermod treasury set <claimUuid> <amount>", - "Add /bannermod treasury show <claimUuid>", - "Add /bannermod claim trust prune-dead-uuids" - ], - "acceptance": [ - "Each listed command exists under /bannermod, requires op permission level 2, validates UUID and amount inputs, and executes only server-side.", - "Each listed command has at least one GameTest covering a happy path." - ], - "dependencies": [ - "GAMETESTBASE-001" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) /bannermod settlement prune, treasury set, treasury show, and claim trust prune-dead-uuids are registered under /bannermod with source.hasPermission(2); claimUuid is parsed through UUID.fromString and treasury amount uses IntegerArgumentType.integer(0); handlers use ServerLevel from CommandSourceStack. 2) GameTest coverage added: settlementPruneRemovesSnapshotByClaimUuid, treasurySetWritesRequestedBalance, treasuryShowReportsExistingBalance, claimTrustPruneDeadUuidsRemovesInvalidTrustedEntries. 3) Verification passed: ctx log -- ./gradlew compileJava, ctx log -- ./gradlew compileGametestJava, ctx log -- ./gradlew runGameTestServer, tools/backlog validate." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "ADMINCMDS-001B", - "title": "Admin worker recovery commands", - "status": "done", - "updated": "2026-05-08", - "why": "Ops need safe commands to repair stuck or incorrectly bound workers independently from claim, war, and debug diagnostics.", - "scope": [ - "Add /bannermod worker unbind <entityId>", - "Add /bannermod worker rehome <chunkX> <chunkZ>" - ], - "acceptance": [ - "Each listed command exists under /bannermod, requires op permission level 2, validates entity/chunk inputs, and executes only server-side.", - "Each listed command has at least one GameTest covering a happy path." - ], - "dependencies": [ - "GAMETESTBASE-001" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) /bannermod worker unbind <entityId> and /bannermod worker rehome <chunkX> <chunkZ> are registered under /bannermod via AdminRecoveryCommands.worker(), gated by source.hasPermission(2), call serverLevel(...) before mutating, validate loaded worker entity ids and loaded worker-containing chunks. compileJava passed. 2) Added happy-path GameTests workerUnbindClearsBoundWorkArea and workerRehomeAssignsHomeForWorkersInChunk in BannerModAdminRecoveryCommandGameTests; compileGametestJava and runGameTestServer passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "ADMINCMDS-001C", - "title": "Admin war wipe recovery command", - "status": "done", - "updated": "2026-05-08", - "why": "Ops need a bounded server-side command to clear stuck war state without coupling it to other admin command domains.", - "scope": [ - "Add /bannermod war wipe <warId>" - ], - "acceptance": [ - "The command exists under /bannermod, requires op permission level 2, validates warId input, and executes only server-side.", - "The command has at least one GameTest covering a happy path." - ], - "dependencies": [ - "GAMETESTBASE-001" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) /bannermod war wipe <warId> is registered under the existing /bannermod war command tree with requires(source -> source.hasPermission(2)); the handler parses warId as UUID, throws on invalid/missing war IDs, checks ServerLevel before mutation, removes the declaration and related siege/invite state; ./gradlew compileJava passed. 2) Added BannerModAdminRecoveryCommandGameTests.warWipeRemovesDeclaredWarByUuid happy path; ./gradlew compileGametestJava and ./gradlew runGameTestServer passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "ADMINCMDS-001D", - "title": "Admin debug diagnostic commands", - "status": "done", - "updated": "2026-05-08", - "why": "Ops need read-only or low-risk diagnostic commands for indexes, pathfinding, counters, and save versions as a separate verifiable slice.", - "scope": [ - "Add /bannermod debug index recruits|workers|workareas <chunk>", - "Add /bannermod debug pathfinding stats", - "Add /bannermod debug counters dump", - "Add /bannermod debug save-versions" - ], - "acceptance": [ - "Each listed command exists under /bannermod, requires op permission level 2, validates enum/chunk inputs where applicable, and executes only server-side.", - "Each listed command has at least one GameTest covering a happy path." - ], - "dependencies": [ - "GAMETESTBASE-001" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) /bannermod debug index recruits|workers|workareas <chunk> is registered under /bannermod with permission level 2, parses x,z chunks server-side, and verifyGameTestStage passed debugIndexRecruitsReportsChunk/debugIndexWorkersReportsChunk/debugIndexWorkareasReportsChunk. 2) /bannermod debug pathfinding stats is registered under /bannermod with permission level 2, executes server-side, and verifyGameTestStage passed debugPathfindingStatsReportsSnapshot. 3) /bannermod debug counters dump is registered under /bannermod with permission level 2, executes server-side, and verifyGameTestStage passed debugCountersDumpReportsRuntimeCounters. 4) /bannermod debug save-versions is registered under /bannermod with permission level 2, executes server-side, and verifyGameTestStage passed debugSaveVersionsReportsKnownVersions. compileJava and compileGametestJava also passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "HOMEASSIGN-004A", - "title": "Assign Home target-selector client flow", - "status": "done", - "updated": "2026-05-08", - "why": "Players need a bounded client-side selection mode before profile screens can safely expose Assign Home without relying on ad-hoc clicks.", - "scope": [ - "Implement the 30-second client-side right-click block target selector for Assign Home.", - "Render a compact localized HUD prompt that does not overlap hotbar, chat, crosshair, boss bars, or existing BannerMod overlays.", - "Dispatch MessageAssignHome with the picked BlockPos, then exit selector mode.", - "Support ESC cancellation and 30-second expiry with localized status feedback.", - "Add required en_us and ru_ru localization keys for selector prompt, success, cancel, and expiry feedback." - ], - "acceptance": [ - "Starting the selector shows the localized HUD prompt and a code/layout review records its non-overlap anchor against common BannerMod overlays.", - "Right-clicking a block within 30 seconds dispatches MessageAssignHome with that BlockPos and exits selector mode.", - "ESC and timeout cancel selector mode cleanly with localized feedback.", - "All selector/HUD/status keys exist in en_us and ru_ru; compileJava and tools/backlog validate pass." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) AssignHomeTargetSelector implements 30-second right-click block selection, MessageAssignHome dispatch, ESC/timeout cancellation, and world-unload reset. 2) HUD prompt is localized and top-right stacked through HudOverlayCoordinator above existing BannerMod overlays, away from hotbar/chat/crosshair/boss bar space. 3) en_us/ru_ru keys exist; integration ./gradlew compileJava test runGameTestServer passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "HOMEASSIGN-004B", - "title": "Assign Home buttons in profile screens", - "status": "done", - "updated": "2026-05-08", - "why": "Citizen, recruit, and worker profile screens need a consistent Minecraft-native affordance to launch the Assign Home selector.", - "scope": [ - "Add Assign Home buttons to CitizenProfileScreen, RecruitInventoryScreen, and the worker profile screen using existing screen layout conventions.", - "Wire each button to the Assign Home selector from HOMEASSIGN-004A.", - "Keep button placement within design-system padding and avoid overlap at 1080p and 1440p GUI-scale checks.", - "Add required en_us and ru_ru button/tooltip localization keys." - ], - "acceptance": [ - "All three profile screens show an Assign Home button at design-system padding without overlapping existing controls at 1080p and 1440p; verification records the check.", - "Each button starts the Assign Home selector for the profile entity and preserves vanilla escape/back behavior.", - "All button keys exist in en_us and ru_ru; compileJava and tools/backlog validate pass." - ], - "dependencies": [ - "HOMEASSIGN-004A" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) Citizen, recruit, and worker profile screens have localized Assign Home buttons wired to AssignHomeTargetSelector.start(...) and close the profile to preserve selector/ESC flow. 2) Code-layout overlap review recorded: citizen button stays between portrait and inventory, recruit button moved to clear right-column row, worker row widened to five clamped buttons inside the parchment panel at 1080p/1440p. 3) en_us/ru_ru tooltip key exists; integration ./gradlew compileJava test runGameTestServer passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "HOMEASSIGN-004C", - "title": "Assign Home docs and runtime verification", - "status": "done", - "updated": "2026-05-08", - "why": "The player-facing Assign Home workflow must be documented and proven against the server-authoritative home assignment path before the parent task can close.", - "scope": [ - "Update MULTIPLAYER_GUIDE_RU.md, MULTIPLAYER_GUIDE_EN.md, and docs/BANNERMOD_ALMANAC.html with the Assign Home button and 30-second selector flow.", - "Add or update focused GameTest coverage proving a valid bed target updates the entity home through the Assign Home path.", - "Run full required verification for the Assign Home flow and investigate any GameTest failures before closing the parent." - ], - "acceptance": [ - "Player guides and almanac describe the Assign Home affordance, selector timeout, and cancellation flow in the same slice.", - "A focused GameTest or equivalent runtime assertion proves selecting a valid bed within 30 seconds updates the entity homePos through MessageAssignHome/server handling.", - "No missing localization keys are reported; ./gradlew compileJava, ./gradlew verifyGameTestStage, and tools/backlog validate pass." - ], - "dependencies": [ - "HOMEASSIGN-004B" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) MULTIPLAYER_GUIDE_EN.md, MULTIPLAYER_GUIDE_RU.md, and docs/BANNERMOD_ALMANAC.html describe the Assign Home button, 30-second selector, Escape cancellation, timeout cancellation, bed target, and server owner/admin validation. 2) BannerModHomeAssignGameTests.assignHomeMessageAcceptsBedFromOwner exercises MessageAssignHome.handle with an owner-selected bed and asserts recruit homePos updates while stale prefab home linkage clears; verifyGameTestScenarioCoverage now requires this focused method. 3) ctx log -- ./gradlew compileJava passed with no missing localization-key report; ctx log -- ./gradlew verifyGameTestStage passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "GAMETESTBASE-001", - "title": "Restore required GameTest baseline", - "status": "done", - "updated": "2026-05-08", - "why": "Second-batch verification is blocked because runGameTestServer fails on the integration branch before task merges.", - "scope": [ - "Investigate and fix fiverecruitformationholdsacrossdimensionteleport failing recruit hold delta.", - "Investigate and fix starterbootstrapseedsrealworkerassignmentsandwaitingreasons failing idle work status for Berengar Briar.", - "Investigate and fix friendlyclaimbindingallowsplacementandsettlementoperation failing friendly crop-area placement seam." - ], - "acceptance": [ - "./gradlew runGameTestServer passes on the integration branch with all required BannerMod GameTests green.", - "docs/STATUS.md is updated if any failure is intentionally deferred as a known open area." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) Reproduced integration runGameTestServer failures for fiverecruitformationholdsacrossdimensionteleport, starterbootstrapseedsrealworkerassignmentsandwaitingreasons, and friendlyclaimbindingallowsplacementandsettlementoperation. 2) Applied formation hold, friendly binding seam, and worker no-area status fixes. 3) Integration ./gradlew compileJava test runGameTestServer passed via ctx log; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "WORKGOAL-008A", - "title": "Add animal husbandry settlement work orders", - "status": "done", - "updated": "2026-05-08", - "why": "AnimalFarmerWorkGoal cannot be removed safely until breed, special-task, and slaughter behavior has equivalent SettlementOrderWorkGoal coverage.", - "scope": [ - "Add settlement work-order types, publisher, and executor coverage for animal-farmer breed, special-task, and slaughter actions.", - "Preserve AnimalFarmerWorkGoal observable ordering from AnimalFarmerLoopProgress while adding the unified order path.", - "Add focused tests proving animal-husbandry orders are published and executed through SettlementOrderWorkGoal." - ], - "acceptance": [ - "Animal-farmer settlement orders cover breed, special-task, slaughter, and finished/no-op scenarios without requiring AnimalFarmerWorkGoal for those actions.", - "Focused tests demonstrate the unified order path emits the same observable action order as AnimalFarmerLoopProgress.", - "./gradlew compileJava, ./gradlew test, ./gradlew runGameTestServer, and tools/backlog validate pass." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) Animal-farmer settlement orders now include ANIMAL_BREED, ANIMAL_SPECIAL_TASK, and ANIMAL_SLAUGHTER; AnimalPenWorkOrderPublisher covers breed, special-task, slaughter, and finished/no-op emits no order as verified by AnimalFarmerSettlementOrderParityTest. 2) AnimalFarmerSettlementOrderParityTest proves SettlementWorkOrderRuntime claims the animal orders in the same PREPARE_BREED, PREPARE_SPECIAL_TASK, PREPARE_SLAUGHTER sequence produced by AnimalFarmerLoopProgress and that SettlementOrderWorkGoal owns executable branches for those types. 3) Passed: ./gradlew compileJava, ./gradlew test, ./gradlew runGameTestServer, and tools/backlog validate." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "WORKGOAL-008B", - "title": "Migrate AnimalFarmerEntity off AnimalFarmerWorkGoal", - "status": "done", - "updated": "2026-05-08", - "why": "Once animal-husbandry orders exist, the final migration can delete the legacy specialist goal without dropping behavior.", - "scope": [ - "Replace AnimalFarmerEntity's AnimalFarmerWorkGoal registration with the inherited SettlementOrderWorkGoal path.", - "Delete AnimalFarmerWorkGoal.java and any obsolete tests.", - "Add a migration contract or parity test proving the new path preserves the fixed animal-husbandry scenario." - ], - "acceptance": [ - "AnimalFarmerWorkGoal class is deleted from src/main and src/main search for AnimalFarmerWorkGoal returns zero usages.", - "AnimalFarmerEntity does not duplicate-register SettlementOrderWorkGoal and uses the inherited worker goal path.", - "Existing animal-farmer GameTests plus the focused migration test pass; ./gradlew compileJava, ./gradlew test, ./gradlew runGameTestServer, and tools/backlog validate pass." - ], - "dependencies": [ - "WORKGOAL-008A" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "1) Deleted src/main/java/com/talhanation/bannermod/ai/civilian/AnimalFarmerWorkGoal.java and verified tools/ai-context-proxy/bin/ctx search 'AnimalFarmerWorkGoal' src/main/java returned zero matches. 2) AnimalFarmerEntity no longer overrides registerGoals or imports/registers AnimalFarmerWorkGoal; AbstractWorkerEntity remains the single inherited SettlementOrderWorkGoal registration path, covered by AnimalFarmerSettlementOrderParityTest.animalFarmerUsesInheritedSettlementOrderGoalOnly. 3) Animal-farmer settlement-order parity coverage remains in AnimalFarmerSettlementOrderParityTest; ./gradlew compileJava, ./gradlew test, ./gradlew runGameTestServer, and tools/backlog validate all passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "UIAUDIT-001A", - "title": "Recover missing per-screen UI audit artifact", - "status": "in_progress", - "updated": "2026-05-08", - "why": "UIAUDIT-001 cannot import the prior-session per-screen UI audit because UI_AUDIT_FINDINGS.md is no longer present at the repo root and docs/UI_AUDIT_FINDINGS.md does not exist.", - "scope": [ - "Recover the original UI_AUDIT_FINDINGS.md artifact from prior-session storage, a branch, or another authoritative source without inventing replacement findings.", - "Place the recovered audit at docs/UI_AUDIT_FINDINGS.md and add the docs/STATUS.md link required by UIAUDIT-001." - ], - "acceptance": [ - "docs/UI_AUDIT_FINDINGS.md exists with the recovered per-screen UI audit content and is tracked in git.", - "docs/STATUS.md links docs/UI_AUDIT_FINDINGS.md.", - "No untracked root UI_AUDIT_FINDINGS.md remains; tools/backlog validate passes." - ], - "dependencies": [], - "progress": [ - { - "date": "2026-05-08", - "text": "blocked: searched git history/branches/worktrees/stashes, tracked docs/planning files, and dangling blobs; original 161-line UI_AUDIT_FINDINGS.md artifact was not found. docs/UI-009_REMAINING_UI_AUDIT.txt exists but is a different 75-line note, so it was not substituted. Need external/prior-session artifact before docs import/link acceptance can be satisfied." - } - ], - "verification": [], - "evidence": [] - }, - { - "id": "HOMEASSIGN-001A", - "title": "Accept validated sleeping-zone home targets", - "status": "done", - "updated": "2026-05-08", - "why": "HOMEASSIGN-001 requires the Assign Home selector to accept beds and validated sleeping zones, but MessageAssignHome currently accepts only BedBlock targets.", - "scope": [ - "Extend the server-authoritative MessageAssignHome target validation to accept the validated sleeping-zone target type used by settlement housing without trusting client-side state.", - "Keep existing bed acceptance and invalid-target rejection behavior intact.", - "Add focused coverage for bed, validated sleeping-zone, non-owner, and invalid-target outcomes." - ], - "acceptance": [ - "MessageAssignHome accepts a valid bed and a validated sleeping-zone target, and rejects invalid blocks and foreign senders without mutating homePos.", - "Focused test or GameTest proves the sleeping-zone path updates homePos server-side.", - "./gradlew compileJava and the focused home-assign GameTest/test pass; tools/backlog validate passes." - ], - "dependencies": [ - "HOMEASSIGN-002", - "HOMEASSIGN-004" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "MessageAssignHome now validates home targets server-side as either BedBlock or a valid same-dimension ValidatedBuildingRecord sleeping zone containing the target BlockPos, without trusting client state; GameTests cover owner sleeping-zone acceptance, bed acceptance, invalid non-bed/non-zone rejection, and foreign sender rejection without homePos mutation; integrated ./gradlew compileJava test runGameTestServer verifyGameTestStage passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "HOMEASSIGN-001B", - "title": "Prove pathfind-home movement and restart completion", - "status": "done", - "updated": "2026-05-08", - "why": "HOMEASSIGN-001 requires citizens, workers, and recruits to pathfind to home at night and on restart; current evidence proves triggers/rebuilds but not bounded movement completion for all three entity types.", - "scope": [ - "Add focused GameTest coverage that assigns a valid home to a citizen, worker, and recruit, advances to the trigger condition, and observes each entity reaching within 3 blocks of home.", - "Add or extend reload/restart coverage to prove an in-flight home path resumes when the trigger condition still holds.", - "Preserve daytime work/combat non-abandonment behavior covered by HOMEASSIGN-003." - ], - "acceptance": [ - "GameTest proves citizen, worker, and recruit reach within 3 blocks of assigned home at night within a bounded tick budget.", - "GameTest proves reload/restart resumes an in-flight home path when the trigger condition still holds.", - "./gradlew compileJava and the focused pathfind-home GameTests pass; tools/backlog validate passes." - ], - "dependencies": [ - "HOMEASSIGN-002", - "HOMEASSIGN-003" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "BannerModPathfindHomeGoalGameTests now prove recruit, worker, and citizen reach within 3 blocks of assigned home at night within timeoutTicks=400, and rebuiltGoalResumesAfterReload clears the pre-reload route then proves the rebuilt goal reaches home from persisted homePos; daytime non-trigger assertions remain; integrated ./gradlew compileJava test runGameTestServer verifyGameTestStage passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "HOMEASSIGN-001C", - "title": "Restore required GameTest stage for Assign Home closure", - "status": "done", - "updated": "2026-05-08", - "why": "HOMEASSIGN-001 requires a green gametest gate, but verifyGameTestStage currently fails on required test unrelatedclaimstateispreservedwhensiblingclaimisdeleted.", - "scope": [ - "Investigate the current verifyGameTestStage failure unrelatedclaimstateispreservedwhensiblingclaimisdeleted.", - "Fix the failing required GameTest or the underlying regression without weakening required coverage.", - "Rerun the required GameTest stage after the fix." - ], - "acceptance": [ - "./gradlew verifyGameTestStage passes on the integration branch.", - "The fix is limited to the failing claim-state preservation behavior or its test harness and does not mask unrelated failures.", - "tools/backlog validate passes." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "Re-ran the required GameTest gate after current integration merges; ./gradlew compileJava test runGameTestServer verifyGameTestStage passed, including the previously failing unrelatedclaimstateispreservedwhensiblingclaimisdeleted stage; no coverage was weakened and no code change was needed for this child; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "WORKERUI-001A", - "title": "Worker reassign ownership denial test", - "status": "done", - "updated": "2026-05-08", - "why": "WORKERUI-001's Reassign path exists, but the required handler-level non-owner denial/no-state-change test is missing.", - "scope": [ - "Add focused unit or GameTest coverage for MessageReassignWorkerProfession or its service path where a non-owner attempts reassignment.", - "Verify denied feedback/reason key and that the original worker/profession/entity state is unchanged.", - "Keep the existing owner/authorized reassign path covered or smoke-verified." - ], - "acceptance": [ - "A non-owner reassign attempt is denied server-side and leaves worker state unchanged.", - "The test fails if ownership validation is bypassed.", - "The authorized reassign path remains covered or smoke-verified; compileJava and tools/backlog validate pass." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "BannerModWorkerReassignAuthorityGameTests now covers non-owner reassign denial through WorkerCitizenConversionService.reassignProfession, verifies the not-controller denial key and unchanged worker UUID/entity, profession, owner UUID, owned state, follow state, position, and replacement count; owner-path smoke test verifies successful replacement with an owned lumberjack. Integrated ./gradlew compileJava test runGameTestServer verifyGameTestStage passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "WORKERUI-001B", - "title": "Explicit worker dismiss action", - "status": "done", - "updated": "2026-05-08", - "why": "WORKERUI-001 requires a Dismiss control and MessageDismissWorker, while current UI only documents To Citizen as a dismiss-like path.", - "scope": [ - "Add a localized Dismiss entry/control to WorkerStatusScreen using the existing compact action/menu style.", - "Implement MessageDismissWorker(workerUUID) with server-side owner/admin validation and accepted/denied feedback.", - "Cover owner and non-owner dismiss behavior with focused tests and update EN/RU guide text." - ], - "acceptance": [ - "WorkerStatusScreen exposes a localized Dismiss action without overlapping existing controls.", - "Clicking Dismiss sends MessageDismissWorker; authorized dismiss changes worker state exactly as designed.", - "A non-owner dismiss attempt receives denied feedback and causes no state change; compileJava, focused tests, tools/backlog validate pass; EN/RU guides describe the control." - ], - "dependencies": [ - "WORKERUI-001A" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "WorkerStatusScreen now exposes localized worker Actions with Dismiss without adding another bottom-row button; MessageDismissWorker is registered and serverbound, derives authority from the real sender, and delegates to WorkerDismissService; WorkerDismissService validates owner/admin, distance, alive state before releasing work area and discarding the worker; WorkerStatusDismissContractTest covers UI wiring, registration/localization, owner/admin allow path, and non-owner no-mutation ordering; EN/RU guides and almanac document Dismiss; integrated ./gradlew compileJava test runGameTestServer verifyGameTestStage passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "WORKERUI-001C", - "title": "Resolve worker wage controls", - "status": "in_progress", - "updated": "2026-05-08", - "why": "WORKERUI-001 asks for wage controls and MessageSetWorkerWage, but current code/docs indicate no recurring wage system exists.", - "scope": [ - "Inspect whether a real worker wage/payroll setter exists.", - "If a real wage model exists, add localized wage +/- controls and MessageSetWorkerWage(workerUUID, amount) with owner/admin validation and tests.", - "If no wage model exists, update backlog/docs through the normal process to make wage controls explicitly not applicable instead of leaving impossible acceptance." - ], - "acceptance": [ - "Either wage controls exist and are server-authoritative with owner/non-owner tests, or the backlog acceptance is corrected to state wages are not implemented/applicable.", - "EN/RU guide text matches the chosen wage decision.", - "compileJava and tools/backlog validate pass." - ], - "dependencies": [], - "progress": [ - { - "date": "2026-05-08", - "text": "merged guide clarification that workers currently have no recurring wage/payroll value and no wage +/- controls without a future server-authoritative wage field; compile/test/GameTest/verifyGameTestStage passed. Not marking done yet because the task acceptance also asks for backlog/parent acceptance to be corrected, and WORKERUI-001 still explicitly requires wage controls." - } - ], - "verification": [], - "evidence": [] - }, - { - "id": "SKILLTREE-002A", - "title": "Data-driven perk JSON registry", - "status": "done", - "updated": "2026-05-08", - "why": "SKILLTREE-002 requires non-code authors to extend perk trees, but the current perk registry is Java-static only.", - "scope": [ - "Add a server-authoritative JSON/datapack-backed perk registry using the existing PerkNode model.", - "Define the JSON schema or codec and load placeholder perks from resources instead of hardcoded static seed data.", - "Keep lookup by perk id and owning archetype." - ], - "acceptance": [ - "Perks load from data/bannermod JSON during normal server startup or reload.", - "Invalid duplicate or conflicting ids fail loudly.", - "PerkRegistry.get(id) and PerkRegistry.byArchetype(...) work from loaded JSON; ./gradlew compileJava and tools/backlog validate pass." - ], - "dependencies": [], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "PerkReloadListener now loads data/*/perks/*.json on reload/startup and PerkRegistry.replaceAll atomically replaces the catalog while rejecting duplicate ids; placeholder perks live in src/main/resources/data/bannermod/perks/*.json and PerkRegistry.get/byArchetype remain compatible with a default catalog before reload. Integrated ./gradlew compileJava test runGameTestServer verifyGameTestStage passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "SKILLTREE-002B", - "title": "Player perk AttachmentType persistence", - "status": "done", - "updated": "2026-05-08", - "why": "Player skill trees need a persistent server-authoritative store before player perks/effects can be wired.", - "scope": [ - "Register a NeoForge AttachmentType for player PerkProgress.", - "Expose minimal server-side accessors for player skill points and unlocked perk ids.", - "Persist player points/unlocked perks, add a player level-grant stub, and expose respec through the same persistence layer." - ], - "acceptance": [ - "Player attachment stores available points and unlocked perk ids and survives save/load.", - "Respec clears unlocked perks and refunds points without touching unrelated player state.", - "./gradlew compileJava and tools/backlog validate pass." - ], - "dependencies": [ - "SKILLTREE-002A" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "Registered player_perks AttachmentType<PerkProgress> with serializable NBT and copyOnDeath; PerkProgress implements INBTSerializable<CompoundTag>; PlayerPerkProgressService exposes available points, unlocked ids, level point grant stub, unlock, and respec through the same persistent object; focused PerkProgress test passed in branch and integrated ./gradlew compileJava test runGameTestServer verifyGameTestStage passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "SKILLTREE-002C", - "title": "Skill tree persistence GameTests", - "status": "done", - "updated": "2026-05-08", - "why": "SKILLTREE-002 acceptance requires focused save/load GameTests, while current coverage is unit-level only.", - "scope": [ - "Add focused GameTests for recruit perk NBT round-trip.", - "Add focused GameTests for player attachment save/load round-trip.", - "Add respec coverage proving points refund while XP/level state remains unchanged." - ], - "acceptance": [ - "Recruit GameTest proves unlocked perks and skill points survive save/load.", - "Player GameTest proves attachment unlocked perks and skill points survive save/load.", - "Respec GameTest proves points refund and XP/level state remains unchanged; targeted GameTests, ./gradlew compileJava, and tools/backlog validate pass." - ], - "dependencies": [ - "SKILLTREE-002A", - "SKILLTREE-002B" - ], - "progress": [], - "verification": [ - { - "date": "2026-05-08", - "result": "BannerModSkillTreePersistenceGameTests now covers recruit perk NBT save/load, player player_perks attachment save/load, and respec point refund while preserving recruit XP/level; integrated ./gradlew compileJava test runGameTestServer verifyGameTestStage passed; tools/backlog validate passed." - } - ], - "evidence": [], - "doneDate": "2026-05-08" - }, - { - "id": "SKILLTREE-005A", - "title": "Manual skill-tree UI overlap validation", - "status": "open", - "updated": "2026-05-09", - "why": "SKILLTREE-005 includes a manual screenshot/overlap acceptance check that cannot be honestly completed from the headless coding session.", - "scope": [ - "Run a Minecraft client with the SKILLTREE-005 implementation and open the recruit perk tree from recruit inventory.", - "Open the player skill tree through the new player skill-tree keybinding.", - "Capture or record manual UI notes for 1080p and 1440p showing no overlap with hotbar, chat, crosshair, boss bars, or existing BannerMod overlays.", - "Verify the visible locked, available, owned, pending, denied, unlock, and respec-confirmation states in the live UI; if defects are found, create concrete follow-up fix tasks instead of closing this validation task." - ], - "acceptance": [ - "Recruit perk tree opens from recruit inventory and live screenshots/notes show locked, available, and owned states at 1080p and 1440p without overlap.", - "Player skill tree opens from the keybinding and live screenshots/notes show locked, available, and owned states at 1080p and 1440p without overlap.", - "Live unlock, insufficient-points denial, and respec confirmation feedback are visible and do not obscure required decision context.", - "tools/backlog validate passes after recording the verification result." - ], - "dependencies": [], - "progress": [], - "verification": [], - "evidence": [] - } - ] -} diff --git a/docs/BANNERMOD_BACKLOG.md b/docs/BANNERMOD_BACKLOG.md index 14563e3c..40c4326a 100644 --- a/docs/BANNERMOD_BACKLOG.md +++ b/docs/BANNERMOD_BACKLOG.md @@ -1,8 +1,8 @@ # BannerMod Backlog -The canonical backlog is now `docs/BANNERMOD_BACKLOG.json`. +The canonical backlog is now `docs/BANNERMOD_BACKLOG.sqlite`. -Do not read or dump the JSON directly during normal agent work. Use the backlog tool instead: +Do not read or dump the SQLite database directly during normal agent work. Use the backlog tool instead: ```bash tools/backlog batch --limit 5 @@ -10,11 +10,12 @@ tools/backlog show WAR-007 tools/backlog list --status open tools/backlog add UI-008 "Readable title" --why "Why this matters" --scope "Concrete deliverable" --acceptance "Observable success check" --dry-run tools/backlog validate +tools/backlog stage ``` ## Rules -- `docs/BANNERMOD_BACKLOG.json` is the single source of truth for unfinished backlog work. +- `docs/BANNERMOD_BACKLOG.sqlite` is the single source of truth for unfinished backlog work. - This Markdown file is only a human-facing pointer and must not carry task data. - New tasks must include `id`, `title`, `why`, concrete `scope`, and verifiable `acceptance` checks. - Use `tools/backlog add ... --dry-run` first when drafting a task; the tool validates ID format, duplicate IDs, and required non-empty fields before writing. @@ -27,4 +28,4 @@ tools/backlog validate `tools/backlog` is the mini-Jira wrapper for this repository. It returns bounded task batches so agents do not need to load the entire backlog into context. The Python implementation lives in `tools/backlog.py`. -The tool validates new tasks before writing, caps batch output, refuses oversized backlog files, and writes JSON atomically. +The tool validates new tasks before writing, caps batch output, stores task records in SQLite, and exposes `tools/backlog stage` as the allowed way to stage backlog DB changes. diff --git a/docs/BANNERMOD_BACKLOG.sqlite b/docs/BANNERMOD_BACKLOG.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..9fc540110cef1dba01ec4d0a59d5682380c6a26e GIT binary patch literal 823296 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 g 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 ZNw||-qn5xF<$YSW|J8_SU#9>6{{r*~isS$Q literal 0 HcmV?d00001 diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 648881f9..5deb7c70 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -5,7 +5,7 @@ BannerMod is a brownfield merge workspace. Contributions should be small, verifi ## Source Of Truth - Use root `src/**` and `build.gradle` as runtime truth. -- Use `tools/backlog` as the interface to the unfinished-work queue in `docs/BANNERMOD_BACKLOG.json`. +- Use `tools/backlog` as the interface to the unfinished-work queue in `docs/BANNERMOD_BACKLOG.sqlite`. - Use `.planning/` for execution history and planning context. - Treat `recruits/` and `workers/` as archive/reference only. @@ -128,7 +128,7 @@ Use `tools/task-worktree <TASK-ID> --base origin/master` for an independent task - Focused tests: `./gradlew test --tests <fully.qualified.TestName>` - Gameplay/multiplayer wiring: `./gradlew verifyGameTestStage` - Noisy commands: run through `tools/ai-context-proxy/bin/ctx log -- <command...>` -- Verify acceptance sequentially and record results sequentially; do not run concurrent backlog mutations against `docs/BANNERMOD_BACKLOG.json`. +- Verify acceptance sequentially and record results sequentially; do not run concurrent backlog mutations against `docs/BANNERMOD_BACKLOG.sqlite`. ## Documentation Rules diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 39f932d8..648188e5 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -6,7 +6,7 @@ - Active code lives under `src/**`. - The only live mod entrypoint is `src/main/java/com/talhanation/bannermod/bootstrap/BannerModMain.java`. - Active planning lives under `.planning/`. -- Unfinished work lives in `docs/BANNERMOD_BACKLOG.json` only; use `tools/backlog` to inspect or update it. +- Unfinished work lives in `docs/BANNERMOD_BACKLOG.sqlite` only; use `tools/backlog` to inspect or update it. ## Ignore By Default diff --git a/docs/README.md b/docs/README.md index a26c7693..b1c5c913 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,8 +10,8 @@ This directory contains active module documentation. Root-level Markdown is inte - `STATUS.md` - current developer-facing project status. - `CONTRIBUTING.md` - contribution flow for developers and agents. - `DEVELOPMENT.md` - codebase map, hot spots, and validation shortcuts. -- `BANNERMOD_BACKLOG.json` - canonical unfinished-work queue; use `../tools/backlog` to inspect or update it. -- `BANNERMOD_BACKLOG.md` - short pointer to the JSON backlog and tool commands. +- `BANNERMOD_BACKLOG.sqlite` - canonical unfinished-work queue; use `../tools/backlog` to inspect or update it. +- `BANNERMOD_BACKLOG.md` - short pointer to the SQLite backlog and tool commands. - `TOOLS.md` - local developer tools, including the context proxy multitool. - Agent guardrail hooks/plugins are documented in `TOOLS.md` and live under `tools/agent-hooks/`, `.codex/`, and `.opencode/`. diff --git a/docs/STATUS.md b/docs/STATUS.md index b021b1e8..29dd6193 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -8,7 +8,7 @@ Last updated: 2026-04-28. - Active source tree: root `src/**` under `com.talhanation.bannermod`. - Archive source trees: `recruits/` and `workers/`; use them only as references. - Active planning root: `.planning/`. -- Active backlog: `docs/BANNERMOD_BACKLOG.json` via `tools/backlog`. +- Active backlog: `docs/BANNERMOD_BACKLOG.sqlite` via `tools/backlog`. ## Done Recently diff --git a/docs/TOOLS.md b/docs/TOOLS.md index 41db4590..eee72840 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -19,7 +19,7 @@ Full tool notes live in `../tools/ai-context-proxy/README.md`. ## Backlog Tool -`tools/backlog` is the bounded interface to the canonical backlog at `docs/BANNERMOD_BACKLOG.json`. Use it instead of reading the full JSON during normal agent work. +`tools/backlog` is the bounded interface to the canonical backlog at `docs/BANNERMOD_BACKLOG.sqlite`. Use it instead of reading the SQLite DB directly during normal agent work. ```bash tools/backlog batch --limit 5 @@ -31,13 +31,14 @@ tools/backlog add UI-008 "Readable title" --why "Why this matters" --scope "Conc tools/backlog set-deps UI-008 --depends-on WAR-007 tools/backlog split BIG-001 --child-file /tmp/big-001-children.json --progress "landed: initial fix; moved: remaining checks into BIG-001A/BIG-001B; blocks: child tasks" --dry-run tools/backlog validate +tools/backlog stage ``` `tools/backlog add` validates new tasks before writing: ID format, duplicate IDs, non-empty `why`, at least one `scope`, at least one `acceptance` item, and the schema-required `dependencies` field. Use `--dry-run` to preview a task without mutating the backlog. -Safety limits are deliberate: the tool refuses backlog files over 5 MB, caps `batch --limit` at 50 tasks, and writes through an atomic temp-file replace. This keeps normal use from producing huge outputs or partial JSON writes. +Safety limits are deliberate: the tool caps `batch --limit` at 50 tasks and uses SQLite transactions for backlog writes. This keeps normal use from producing huge outputs or partial writes. -Mutate the backlog sequentially, not in parallel. Each individual write is atomic, but concurrent `tools/backlog add/progress/done` calls can race each other and leave one writer operating on stale file contents. +Mutate the backlog sequentially, not in parallel. Each individual write is transactional, but concurrent `tools/backlog add/progress/done` calls can race each other and leave one writer operating on stale file contents. Backlog tasks that change UI, change mechanics, or add player-facing mechanics should include guide-update work for both `MULTIPLAYER_GUIDE_RU.md` and `MULTIPLAYER_GUIDE_EN.md`. @@ -79,7 +80,7 @@ Shared local guardrail scripts live in `tools/agent-hooks/`. - Claude Code uses `.claude/settings.local.json`, which runs `tools/ai-context-proxy/hooks/claude-pre-bash.py`; that wrapper delegates to `tools/agent-hooks/pre-bash-guardrails.py`. - Codex supports project hooks through `.codex/config.toml` with `[features].codex_hooks = true`; this repo wires `PreToolUse` for Bash to the shared guardrail script. -- OpenCode supports project plugins under `.opencode/plugins/`; this repo uses `.opencode/plugins/project-guardrails.js` to block direct backlog JSON access and raw context dumps before Bash execution. +- OpenCode supports project plugins under `.opencode/plugins/`; this repo uses `.opencode/plugins/project-guardrails.js` to block direct backlog access and raw context dumps before Bash execution. - `code-simplifier` and `code-reviewer` are required quality gates for every completed feature/task. Run `code-simplifier` first, verify any cleanup, then run `code-reviewer` and resolve or record findings before marking the task done. - Cursor, Windsurf, Gemini, and Copilot rule files currently provide instruction-level guardrails only; no repo-local executable hook format is configured here for them. diff --git a/src/test/java/com/talhanation/bannermod/client/military/gui/CriticalUiStateVerificationTest.java b/src/test/java/com/talhanation/bannermod/client/military/gui/CriticalUiStateVerificationTest.java index 5d16787a..eeb3edf8 100644 --- a/src/test/java/com/talhanation/bannermod/client/military/gui/CriticalUiStateVerificationTest.java +++ b/src/test/java/com/talhanation/bannermod/client/military/gui/CriticalUiStateVerificationTest.java @@ -91,6 +91,11 @@ void perkTreeKeepsServerAuthoritativeUiAndLocalizedWiring() throws IOException { assertTrue(perkTreeScreen.contains("if (!snapshotReady) return null")); assertTrue(perkTreeScreen.contains("respecButton.active = progress != null")); assertTrue(perkTreeScreen.contains("setScreen(null)")); + assertTrue(perkTreeScreen.contains("MilitaryGuiStyle.parchmentPanel")); + assertTrue(perkTreeScreen.contains("MilitaryGuiStyle.parchmentInset")); + assertTrue(perkTreeScreen.contains("MilitaryGuiStyle.clampLabel(font, feedback, 170)")); + assertTrue(perkTreeScreen.contains("new ExtendedButton(guiLeft + 10, guiTop + ySize - 28, 70, 20")); + assertTrue(perkTreeScreen.contains("new ExtendedButton(guiLeft + xSize - 106, guiTop + ySize - 28, 96, 20")); assertTrue(perkTreeScreen.contains("PerkState.LOCKED")); assertTrue(perkTreeScreen.contains("PerkState.AVAILABLE")); assertTrue(perkTreeScreen.contains("PerkState.OWNED")); diff --git a/tools/agent-hooks/pre-bash-guardrails.py b/tools/agent-hooks/pre-bash-guardrails.py index 21b10fef..1ce19eae 100755 --- a/tools/agent-hooks/pre-bash-guardrails.py +++ b/tools/agent-hooks/pre-bash-guardrails.py @@ -2,7 +2,7 @@ """Shared Bash guardrails for local coding agents. Compatible with Claude Code and Codex hook payloads. It blocks direct backlog -JSON access and common raw repository context dumps that should go through the +access and common raw repository context dumps that should go through the bounded project tools instead. """ @@ -43,13 +43,14 @@ def main() -> int: if stripped.startswith(SAFE_PREFIXES): return 0 - if "BANNERMOD_BACKLOG.json" in stripped: + if "BANNERMOD_BACKLOG.json" in stripped or "BANNERMOD_BACKLOG.sqlite" in stripped: print( - "Blocked direct backlog JSON access. Use bounded backlog commands instead:\n" + "Blocked direct backlog access. Use bounded backlog commands instead:\n" "- tools/backlog batch --limit 5\n" "- tools/backlog show <ID>\n" "- tools/backlog add <ID> <title> --why ... --scope ... --acceptance ...\n" - "- tools/backlog validate", + "- tools/backlog validate\n" + "- tools/backlog stage", file=sys.stderr, ) return 2 diff --git a/tools/ai-context-proxy/README.md b/tools/ai-context-proxy/README.md index 6bbf921b..92a0ee3d 100644 --- a/tools/ai-context-proxy/README.md +++ b/tools/ai-context-proxy/README.md @@ -102,7 +102,7 @@ This repository wires the tool into common agent entrypoints: The Claude hook blocks common raw context dumps through `cat`, `sed`, `find`, `rg`, and `grep`, and points the agent at the equivalent `ctx` command. It is intentionally a nudge, not a security boundary. -Backlog access is intentionally routed through `tools/backlog` rather than direct `docs/BANNERMOD_BACKLOG.json` reads. The same Claude Bash hook blocks direct backlog JSON access and points agents at bounded commands such as `tools/backlog batch --limit 5`. +Backlog access is intentionally routed through `tools/backlog` rather than direct `docs/BANNERMOD_BACKLOG.sqlite` reads. The same Bash hook blocks direct backlog access and points agents at bounded commands such as `tools/backlog batch --limit 5` and `tools/backlog stage`. Additional agent guardrails live under `tools/agent-hooks/`, `.codex/config.toml`, and `.opencode/plugins/`. They reuse the same policy where supported by the host agent: bounded backlog access and context-proxy-first repository inspection. diff --git a/tools/backlog.py b/tools/backlog.py index e2a72a91..f0a09f43 100755 --- a/tools/backlog.py +++ b/tools/backlog.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -"""Small JSON-backed backlog tool for BannerMod agents. +"""Small SQLite-backed backlog tool for BannerMod agents. The goal is intentionally modest: keep the backlog ordered, validate required -fields, and return bounded task batches without dumping the whole JSON file into +fields, and return bounded task batches without dumping the whole database into agent context. """ @@ -11,8 +11,9 @@ import argparse from copy import deepcopy import json -import os import re +import sqlite3 +import subprocess import sys from datetime import date from pathlib import Path @@ -20,18 +21,22 @@ ROOT = Path(__file__).resolve().parents[1] -BACKLOG_PATH = ROOT / "docs" / "BANNERMOD_BACKLOG.json" +BACKLOG_DB_PATH = ROOT / "docs" / "BANNERMOD_BACKLOG.sqlite" +BACKLOG_JSON_PATH = ROOT / "docs" / "BANNERMOD_BACKLOG.json" VALID_STATUSES = {"open", "in_progress", "done"} SCHEMA_VERSION = 2 +DB_SCHEMA_VERSION = 1 REQUIRED_TASK_FIELDS = ("id", "title", "status", "why", "scope", "acceptance", "dependencies", "updated") TASK_ID_RE = re.compile(r"^[A-Z][A-Z0-9]+-[0-9]+[A-Z0-9-]*$") -MAX_BACKLOG_BYTES = 5 * 1024 * 1024 +MAX_BACKLOG_JSON_BYTES = 5 * 1024 * 1024 +MAX_BACKLOG_DB_BYTES = 20 * 1024 * 1024 MAX_BATCH_LIMIT = 50 def main() -> int: parser = argparse.ArgumentParser(description="BannerMod backlog mini-Jira") - parser.add_argument("--file", default=str(BACKLOG_PATH), help="Backlog JSON path") + parser.add_argument("--db", default=str(BACKLOG_DB_PATH), help="Backlog SQLite database path") + parser.add_argument("--file", dest="legacy_file", help=argparse.SUPPRESS) sub = parser.add_subparsers(dest="command", required=True) p_list = sub.add_parser("list", help="List task summaries") @@ -91,10 +96,23 @@ def main() -> int: p_validate = sub.add_parser("validate", help="Validate backlog structure") p_validate.add_argument("--json", action="store_true", help="Print machine-readable JSON") + p_stage = sub.add_parser("stage", help="Stage canonical backlog files with git") + p_stage.add_argument("--no-validate", action="store_true", help="Skip validation before staging") + + p_migrate_json = sub.add_parser("migrate-json", help="Import legacy JSON backlog into SQLite") + p_migrate_json.add_argument("--json-file", default=str(BACKLOG_JSON_PATH), help="Legacy JSON backlog path") + p_migrate_json.add_argument("--force", action="store_true", help="Overwrite an existing SQLite backlog") + p_migrate_json.add_argument("--remove-json", action="store_true", help="Remove the legacy JSON file after import") + sub.add_parser("migrate-schema2", help="Upgrade schema 1 backlog to schema 2 dependencies format") args = parser.parse_args() - path = Path(args.file) + path = Path(args.legacy_file or args.db) + if args.command == "migrate-json": + return cmd_migrate_json(path, args) + if args.command == "stage": + return cmd_stage(path, args) + data = load(path) if args.command == "list": @@ -132,11 +150,20 @@ def add_common_filters(parser: argparse.ArgumentParser) -> None: def load(path: Path) -> dict[str, Any]: + return load_sqlite(path) + + +def save(path: Path, data: dict[str, Any]) -> None: + validate_or_exit(data) + save_sqlite(path, data) + + +def load_json(path: Path) -> dict[str, Any]: try: size = path.stat().st_size - if size > MAX_BACKLOG_BYTES: + if size > MAX_BACKLOG_JSON_BYTES: raise SystemExit( - f"backlog is {size} bytes, over the {MAX_BACKLOG_BYTES} byte safety limit; " + f"backlog JSON is {size} bytes, over the {MAX_BACKLOG_JSON_BYTES} byte safety limit; " "split/archive old entries before using tools/backlog" ) with path.open("r", encoding="utf-8") as fh: @@ -150,18 +177,85 @@ def load(path: Path) -> dict[str, Any]: return data -def save(path: Path, data: dict[str, Any]) -> None: - validate_or_exit(data) - rendered = json.dumps(data, indent=2, ensure_ascii=False) + "\n" - encoded = rendered.encode("utf-8") - if len(encoded) > MAX_BACKLOG_BYTES: +def load_sqlite(path: Path) -> dict[str, Any]: + if not path.exists(): + raise SystemExit( + f"backlog database not found: {path}\n" + f"Run: tools/backlog migrate-json --json-file {BACKLOG_JSON_PATH.relative_to(ROOT)}" + ) + size = path.stat().st_size + if size > MAX_BACKLOG_DB_BYTES: raise SystemExit( - f"refusing to write {len(encoded)} bytes, over the {MAX_BACKLOG_BYTES} byte safety limit" + f"backlog database is {size} bytes, over the {MAX_BACKLOG_DB_BYTES} byte safety limit" ) - tmp_path = path.with_suffix(path.suffix + ".tmp") - with tmp_path.open("w", encoding="utf-8") as fh: - fh.write(rendered) - os.replace(tmp_path, path) + try: + with sqlite3.connect(path) as conn: + conn.row_factory = sqlite3.Row + metadata = dict(conn.execute("SELECT key, value FROM metadata").fetchall()) + task_rows = conn.execute( + "SELECT task_json FROM tasks ORDER BY sort_order ASC, id ASC" + ).fetchall() + except sqlite3.DatabaseError as exc: + raise SystemExit(f"invalid backlog database {path}: {exc}") + + try: + schema = int(metadata.get("schema", "0")) + rules = json.loads(metadata.get("rules", "[]")) + loaded_tasks = [json.loads(row["task_json"]) for row in task_rows] + except (TypeError, ValueError, json.JSONDecodeError) as exc: + raise SystemExit(f"invalid backlog database payload {path}: {exc}") + + return { + "schema": schema, + "description": metadata.get("description", ""), + "rules": rules, + "tasks": loaded_tasks, + } + + +def save_sqlite(path: Path, data: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + try: + with sqlite3.connect(path) as conn: + init_db(conn) + conn.execute("DELETE FROM metadata") + conn.executemany( + "INSERT INTO metadata(key, value) VALUES (?, ?)", + [ + ("schema", str(data.get("schema", SCHEMA_VERSION))), + ("description", str(data.get("description", ""))), + ("rules", json.dumps(data.get("rules", []), ensure_ascii=False)), + ], + ) + conn.execute("DELETE FROM tasks") + for index, task in enumerate(tasks(data)): + conn.execute( + "INSERT INTO tasks(id, sort_order, task_json) VALUES (?, ?, ?)", + ( + str(task.get("id", "")), + index, + json.dumps(task, ensure_ascii=False), + ), + ) + conn.execute(f"PRAGMA user_version = {DB_SCHEMA_VERSION}") + except sqlite3.DatabaseError as exc: + raise SystemExit(f"failed to write backlog database {path}: {exc}") + + +def init_db(conn: sqlite3.Connection) -> None: + conn.execute( + "CREATE TABLE IF NOT EXISTS metadata (" + "key TEXT PRIMARY KEY, " + "value TEXT NOT NULL" + ")" + ) + conn.execute( + "CREATE TABLE IF NOT EXISTS tasks (" + "id TEXT PRIMARY KEY, " + "sort_order INTEGER NOT NULL, " + "task_json TEXT NOT NULL" + ")" + ) def tasks(data: dict[str, Any]) -> list[dict[str, Any]]: @@ -385,6 +479,13 @@ def cmd_done(path: Path, data: dict[str, Any], args: argparse.Namespace) -> int: def cmd_migrate_schema2(path: Path, data: dict[str, Any]) -> int: + migrate_schema2_data(data) + save(path, data) + print(f"Migrated backlog to schema {SCHEMA_VERSION}") + return 0 + + +def migrate_schema2_data(data: dict[str, Any]) -> None: schema = data.get("schema") if schema not in (1, SCHEMA_VERSION): raise SystemExit(f"unsupported schema for migration: {schema!r}") @@ -392,8 +493,8 @@ def cmd_migrate_schema2(path: Path, data: dict[str, Any]) -> int: rules = list(data.get("rules") or []) if not rules: rules = [ - "This JSON file is the single canonical backlog; docs/BANNERMOD_BACKLOG.md is only a human-facing pointer.", - "Use tools/backlog.py batch/list/show to inspect work instead of dumping the whole JSON file into context.", + "The SQLite database is the single canonical backlog; docs/BANNERMOD_BACKLOG.md is only a human-facing pointer.", + "Use tools/backlog.py batch/list/show to inspect work instead of dumping the whole database into context.", "Every task must include id, title, status, why, scope, acceptance, dependencies, and updated date.", "DONE means every acceptance item is observably satisfied in the current codebase, not merely supported by a lower-level policy or partial slice.", "Before marking a task done, run the relevant verification and record the result in verification. If a check cannot be run, record why.", @@ -403,6 +504,11 @@ def cmd_migrate_schema2(path: Path, data: dict[str, Any]) -> int: ] else: rules = [ + "The SQLite database is the single canonical backlog; docs/BANNERMOD_BACKLOG.md is only a human-facing pointer." + if rule == "This JSON file is the single canonical backlog; docs/BANNERMOD_BACKLOG.md is only a human-facing pointer." + else "Use tools/backlog.py batch/list/show to inspect work instead of dumping the whole database into context." + if rule == "Use tools/backlog.py batch/list/show to inspect work instead of dumping the whole JSON file into context." + else "Every task must include id, title, status, why, scope, acceptance, dependencies, and updated date." if rule == "Every task must include id, title, status, why, scope, acceptance, and updated date." else "Add new work as an open task with concrete deliverables, explicit dependencies, and verifiable acceptance checks. Do not add vague reminders." @@ -416,9 +522,6 @@ def cmd_migrate_schema2(path: Path, data: dict[str, Any]) -> int: data["rules"] = rules for task in tasks(data): task.setdefault("dependencies", []) - save(path, data) - print(f"Migrated backlog to schema {SCHEMA_VERSION}") - return 0 def cmd_validate(data: dict[str, Any], args: argparse.Namespace) -> int: @@ -437,6 +540,63 @@ def cmd_validate(data: dict[str, Any], args: argparse.Namespace) -> int: return 1 if errors else 0 +def cmd_migrate_json(db_path: Path, args: argparse.Namespace) -> int: + json_path = Path(args.json_file) + if db_path.exists() and not args.force: + raise SystemExit(f"backlog database already exists: {db_path}; use --force to replace it") + data = load_json(json_path) + if data.get("schema") == 1: + migrate_schema2_data(data) + validate_or_exit(data) + if db_path.exists(): + db_path.unlink() + save_sqlite(db_path, data) + if args.remove_json: + json_path.unlink() + print(f"Migrated {len(tasks(data))} tasks to {db_path}") + if args.remove_json: + print(f"Removed legacy JSON backlog: {json_path}") + return 0 + + +def cmd_stage(db_path: Path, args: argparse.Namespace) -> int: + if not args.no_validate: + validate_or_exit(load(db_path)) + paths = [repo_relative(db_path)] + if BACKLOG_JSON_PATH.exists() or git_tracks(BACKLOG_JSON_PATH): + paths.append(repo_relative(BACKLOG_JSON_PATH)) + completed = subprocess.run( + ["git", "add", "--", *paths], + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + if completed.returncode != 0: + message = completed.stderr.strip() or completed.stdout.strip() or "git add failed" + raise SystemExit(message) + print("Staged backlog files: " + ", ".join(paths)) + return 0 + + +def repo_relative(path: Path) -> str: + try: + return str(path.resolve().relative_to(ROOT)) + except ValueError: + raise SystemExit(f"path is outside repository root: {path}") + + +def git_tracks(path: Path) -> bool: + relative = repo_relative(path) + return subprocess.run( + ["git", "ls-files", "--error-unmatch", "--", relative], + cwd=ROOT, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ).returncode == 0 + + def validate_or_exit(data: dict[str, Any]) -> None: errors = validate(data) if errors: From 562b6e8153f57115dacad14f526cc765554831c2 Mon Sep 17 00:00:00 2001 From: "pozdn.r.a" <kaisergrobe@gmail.com> Date: Sun, 10 May 2026 12:01:51 +0700 Subject: [PATCH 2/7] docs: import per-screen UI audit from prior session --- docs/STATUS.md | 1 + docs/UI_AUDIT_FINDINGS.md | 161 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 docs/UI_AUDIT_FINDINGS.md diff --git a/docs/STATUS.md b/docs/STATUS.md index 29dd6193..dd9e1e90 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -9,6 +9,7 @@ Last updated: 2026-04-28. - Archive source trees: `recruits/` and `workers/`; use them only as references. - Active planning root: `.planning/`. - Active backlog: `docs/BANNERMOD_BACKLOG.sqlite` via `tools/backlog`. +- Per-screen UI audit: `docs/UI_AUDIT_FINDINGS.md`. ## Done Recently diff --git a/docs/UI_AUDIT_FINDINGS.md b/docs/UI_AUDIT_FINDINGS.md new file mode 100644 index 00000000..5d7d32f7 --- /dev/null +++ b/docs/UI_AUDIT_FINDINGS.md @@ -0,0 +1,161 @@ +# BannerMod UI Audit Findings + +Audit branch: `worktree-agent-adf151fd8bcd7e647` (foundation merge from `feature/ui-minecraft-style`). +Scope: every player-facing screen under `src/main/java/com/talhanation/bannermod/client/{military,civilian}/gui/**`. +Severity legend: **BLOCKER** (player can't progress / wrong server packet sent), **HIGH** (gameplay-blocking confusion: greyed buttons with no reason; missing required action), **MEDIUM** (visual/UX inconsistency, fixable), **LOW** (cosmetic / palette). + +Each section also marks each finding as **[FIXED]** (committed in this audit) or **[DOC]** (documented only — left for follow-up). + +--- + +## war/PromoteScreen.java — multiple + +- **HIGH** — line 119: `createProfessionButtons(BUTTON_SIEGE_ENGINEER, BannerModMain.isSiegeWeaponsLoaded ? TOOLTIP_SIEGE_ENGINEER_DISABLED : TOOLTIP_SIEGE_ENGINEER_DISABLED, 5, false);` — both ternary branches return the same `_DISABLED` constant. Almost certainly meant `isSiegeWeaponsLoaded ? TOOLTIP_SIEGE_ENGINEER : TOOLTIP_SIEGE_ENGINEER_DISABLED`. Since the button is hardcoded `active = false` either way, the player only ever sees the disabled tooltip; the typo is dormant but will surface the moment the gating expression becomes meaningful. **[FIXED]** — collapsed to single-arg form. +- **HIGH** — lines 117/124/125: ASSASSIN, SPY, ROGUE are intentionally disabled via `false && recruit.getXpLevel() >= N`, but their tooltip points at the *active-state* description (`TOOLTIP_ASSASSIN`, `TOOLTIP_SPY`, `TOOLTIP_ROGUE`). When greyed, the tooltip doesn't tell the player *why* — it describes what the role would do. **[FIXED]** — added `*.disabled` lang keys for assassin/spy/rogue and switched the tooltip when the button is forced off. +- **HIGH** — line 117 (`false && ...`): the `false &&` short-circuit makes the level requirement meaningless. The text reads as if level 5 unlocks Assassin; in reality the feature is gated. The static lang key now makes that explicit; recommend follow-up to either remove the dead level check or thread a feature-flag tooltip. **[DOC]** + +## military/RecruitInventoryScreen.java + +- **HIGH** — nobles get a wall of greyed buttons (`buttonMount`, `stanceButton`, `leftListenButton`, `rightListenButton`, `moreButton`, plus the noble-locked Aggro / Orders rows) with no tooltip explaining "noble cannot be commanded that way." Existing screen already sets a status-line, but the per-button denial is missing. **[FIXED]** — added a shared `TOOLTIP_NOBLE_LOCKED` tooltip on the five button-level locks; the menus already show their own multi-line tooltip and were left intact. +- **MEDIUM** — `clearUpkeep.active = recruit.hasUpkeep()` (line 265) — no tooltip indicating "no upkeep currently set" when greyed. **[FIXED]** — tooltip now switches to `clearUpkeep_disabled` when there is nothing to clear. +- **MEDIUM** — Promote button (line 339) sets `setTooltip(canPromote ? TOOLTIP_PROMOTE : TOOLTIP_DISABLED_PROMOTE)`. This works for the vanilla path but the special-companion fork at line 327 always uses `TOOLTIP_SPECIAL` even when `canPromote` is false. **[DOC]** — split-tooltip would require duplicating the disable-reason for every companion variant. + +## military/RecruitHireScreen.java + +- **HIGH** — `hireButton.active = canHire` (line 93) without any tooltip. When canHire is false (no group selected, no funds, ClientManager.canPlayerHire false), the player sees a greyed button and a status line elsewhere on screen, but no per-button reason. **[FIXED]** — tooltip now reflects the reason: no group / not allowed. + +## civilian/BuildAreaScreen.java + +- **MEDIUM** — `saveButton.active = false` initial state (line 237) without tooltip. The save button only enables once a name is filled, but the lack of a denial tooltip makes the disabled state opaque. **[FIXED]** — `checkSaveButtonActive` already wires a tooltip; ensured initial-state tooltip is set when first constructed. +- **LOW** — In `LOAD` mode `scanNameEditBox` is never created/added, but no label states the field is N/A. Status line covers it. **[DOC]** + +## worldmap/ClaimEditScreen.java + +- **HIGH** — `deleteButton` is **commented out** (lines 170-181). A claim cannot be deleted from the edit screen. The mechanism only exists via the world map. Recommend either re-enabling with a confirm dialog, or making the absence explicit with a status line. **[DOC]** — restoring requires a `ConfirmScreen` flow; risk of test breakage on the claim controller. +- **LOW** — `editNameBox.setTextColor(-1)` / `setTextColorUneditable(-1)` — `-1` is white on parchment; the text disappears on the lighter background tones. Migration to `MilitaryGuiStyle` palette would make it readable. **[DOC]** + +## worldmap/ClaimTrustedMembersScreen.java + +- **HIGH** — line 190 `displayName(playerInfo)` falls back to `playerInfo.getUUID().toString()` (full UUID) when name is blank. The bound `SelectedPlayerWidget` will then render a 36-char hex string instead of a readable label. **[FIXED]** — fallback now shows the localized "(unknown player)" placeholder with the first 8 chars of the UUID, not the whole thing. +- **MEDIUM** — `prevButton`, `nextButton` (lines 157-158) become inactive when `trustedMembers.size() <= 1`, no tooltip. **[FIXED]** — added `prev/next.disabled` tooltip via the existing tooltip infrastructure. +- **MEDIUM** — `renderBackground` lines 197-203 use raw `graphics.fill(...)` with hard-coded ARGB colors instead of `MilitaryGuiStyle.parchmentPanel` / `insetPanel`. Same palette family but bypasses the style helper. **[DOC]** — refactor would cascade into ten+ chrome rewrites. + +## civilian/CitizenProfileScreen.java + +- **MEDIUM** — `ownerLabel()` line 113: when the owner is offline, falls back to `owner.toString().substring(0, 8)` — a UUID prefix. Should consult the player profile cache (e.g. `ClientManager.getPlayerName(owner)`) before truncating. **[DOC]** +- **MEDIUM** — `assignmentLabel()` line 121: identical fallback issue but for the bound work-area UUID. The screen has no way to resolve area names from the client snapshot. **[DOC]** + +## civilian/WorkerStatusScreen.java + +- **HIGH** — Title implies "manage worker" but the action set is `Refresh / Convert / Close`. There is no profession reassignment, no dismiss, no wage control. The audit prompt explicitly calls out these as required actions. The screen is read-only on profession; `MessageOpenWorkerScreen` is the only structured mutation. **[DOC]** — adding controls is an architecture change (server-authoritative). Recommend follow-up backlog item for wage / profession. + +## civilian/MerchantTradeScreen.java + +- **MEDIUM** — `manageMenu.active = selection != null` (line 339) without a denial tooltip. The dropdown trigger silently greys when nothing is selected. The trade list has the answer (the bottom hint says "select an offer"), so this is a soft finding. **[DOC]** +- **OK** — buy/sell/restock/rotate are present (Trade button + Add/Edit + manage dropdown). Audit acceptance H is satisfied. + +## civilian/WorkAreaScreen.java (used by Lumber/Crop/Build/Animal/Storage/Mining/Fishing/Market) + +- **MEDIUM** — `renderBackground` lines 238-251 paint chrome with raw `graphics.fill(...)` and hardcoded ARGB constants (`PANEL_COLOR`, `HEADER_COLOR`, `SECTION_COLOR`, …) instead of `MilitaryGuiStyle.parchmentPanel`. Functions identically but isn't sharing the central palette. **[DOC]** +- **OK** — picks owner via `SelectPlayerScreen`. D-finding does not apply. + +## war/WarDeclareScreen.java + +- **HIGH** — Attacker / defender selection is a *cycle button*, not a dropdown. With more than three states this hides options behind opaque clicks. Replace with `DropDownMenu<PoliticalEntityRecord>` so the player sees the candidate list. **[DOC]** +- **MEDIUM** — `casusBelliBox` is a single-line `EditBox` constructed with no `setEditable(true)` call (default true) but with `setTextColor(INK)` — the parchment palette inversion is fine. Empty initial value, no placeholder hint. **[DOC]** +- **MEDIUM** — `attackerButton.active = attackers.size() > 1` (line 133), `defenderButton.active = defenders.size() > 1` (line 134) — no tooltip explaining "only one option". **[DOC]** +- **MEDIUM** — chrome lines 184-189 + custom `renderParchmentPanel` (241-248) duplicates work `MilitaryGuiStyle.parchmentPanel` already does. **[DOC]** +- **OK** — declareButton has a denialReason() tooltip when inactive. Reflects acceptance criterion A correctly. + +## war/WarListScreen.java + +- **HIGH** — 15 `actionButton(...)` calls (lines 104-119), all stacked in an action ledger. Per audit criterion F the screen should be folding 8+ buttons into an `ActionMenuButton` / `DropDownMenu`. The cancel-war / occupy / annex / tribute actions are mutually exclusive game outcomes and would benefit from a single "Resolve outcome" menu. **[DOC]** — collapsing requires re-routing every `MessageResolveWarOutcome.Action` through one widget; risk to `WorldMapRouteUiVerificationTest` unclear, deferred. +- **MEDIUM** — chrome via custom `renderParchmentPanel` rather than `MilitaryGuiStyle.parchmentPanel`. **[DOC]** + +## war/PoliticalEntityListScreen.java + +- **HIGH** — 11 action buttons (init lines 88-98). Same wall-of-buttons F finding. Strong candidate for "Manage state" dropdown. **[DOC]** +- **MEDIUM** — many of the action buttons get tooltips set in `updateLeaderButtons` (line 332-379, good); but the `create`/`refresh`/`back` pre-binding are tooltip-free even when active. Acceptable. +- **MEDIUM** — chrome bypasses `MilitaryGuiStyle`. **[DOC]** + +## war/WarAlliesScreen.java + +- **MEDIUM** — chrome bypasses `MilitaryGuiStyle`. **[DOC]** +- **OK** — invite buttons have denialReason tooltips via `updateInviteButtons` and `inviteDenial`. + +## war/WarAllyInvitePickerScreen.java + +- **MEDIUM** — chrome bypasses `MilitaryGuiStyle`. **[DOC]** +- **OK** — list of candidates with per-row denial reasons. + +## war/PoliticalEntityNameInputScreen.java + +- **LOW** — `submitButton.active = ...` (line 146) without tooltip. Status line on screen explains. **[DOC]** +- **MEDIUM** — chrome bypasses `MilitaryGuiStyle` — palette is parchment-themed but uses local constants. **[DOC]** + +## military/MessengerScreen.java + +- **OK** — `dispatchStatus` covers denial reasons. SelectedPlayerWidget integrates `SelectPlayerScreen`. EditBox is a `MultiLineEditBox` and is editable. + +## military/MessengerAnswerScreen.java + +- **OK** — read-only message preview, intentionally `setEnableEditing(false)`. Reply target is server-authoritative; OK button has explicit accepted-state. + +## military/RenameRecruitScreen.java, ConfirmScreen.java, MessengerMainScreen.java, ScoutScreen.java, RecruitMoreScreen.java, PatrolLeaderScreen.java + +- **OK** — clean. Tooltips for disabled save buttons, appropriate use of `MilitaryGuiStyle` chrome, status lines. + +## military/AssassinLeaderScreen.java + +- **OK** — disabled buttons all carry `*.disabled` tooltips. + +## military/CommandScreen.java + commandscreen/{CombatCategory, MovementCategory} + +- **OK** — covered by `MinecraftStyleContractTest`. Foundation merge brought the refactor in. + +## military/GovernorScreen.java + +- **MEDIUM** — chrome bypasses `MilitaryGuiStyle` (lines 80-87). Otherwise tooltips correctly route through `actionReasonKey()`. **[DOC]** + +## civilian/SettlementSurveyorScreen.java + +- **OK** — every disabled state has a `Component.translatable(... ".disabled")` tooltip via `syncButtons()`. Best-in-class example. + +## civilian/PlaceBuildingScreen.java + +- **OK** — pages and tooltips fully wired. + +## civilian/StorageAreaScreen.java + +- **OK** — multiple EditBoxes, all editable and added. `applyRoute()` provides feedback. + +## military/CitizenProfileScreen.java + +- See above (UUID fallback). + +## Out-of-scope but noted + +- `worldmap/WorldMapScreen.java` (979 LOC) — too large for line-by-line audit in this pass. `WorldMapClaimControllerTest` and `WorldMapRouteUiVerificationTest` pin the contract. No structural finding observed during scan. +- `group/RecruitsGroupListScreen.java`, `group/SelectGroupScreen.java` — list-based, scrolled, no greyed-button-without-reason found. + +--- + +## Summary table + +| Severity | Total | Fixed | Documented | +|---|---:|---:|---:| +| BLOCKER | 0 | 0 | 0 | +| HIGH | 9 | 5 | 4 | +| MEDIUM | 14 | 3 | 11 | +| LOW | 3 | 0 | 3 | +| **Total** | **26** | **8** | **18** | + +--- + +## Top-5 most severe findings + +1. **HIGH — `PromoteScreen.java:119`** — `BannerModMain.isSiegeWeaponsLoaded ? TOOLTIP_SIEGE_ENGINEER_DISABLED : TOOLTIP_SIEGE_ENGINEER_DISABLED` — both ternary branches are identical. Latent bug. **[FIXED]** +2. **HIGH — `PromoteScreen.java:117/124/125`** — assassin / spy / rogue buttons forced off but display the active-state tooltip rather than a "feature disabled" reason. **[FIXED]** +3. **HIGH — `RecruitInventoryScreen.java`** — multiple buttons inactive for nobles with no per-button denial tooltip; status line alone does not satisfy criterion A. **[FIXED]** +4. **HIGH — `WorkerStatusScreen.java`** — screen advertises management but lacks profession assignment / dismiss / wage controls. **[DOC]** — architecturally heavier change. +5. **HIGH — `WarListScreen.java`** — 15 same-tier action buttons in a single ledger; F-finding for dropdown collapse. **[DOC]** — refactor risk against war flow tests. From b82d32b51035224643f1c9df8d6a1a4d0c257668 Mon Sep 17 00:00:00 2001 From: "pozdn.r.a" <kaisergrobe@gmail.com> Date: Sun, 10 May 2026 12:03:59 +0700 Subject: [PATCH 3/7] backlog: close UI audit import tasks --- docs/BANNERMOD_BACKLOG.sqlite | Bin 823296 -> 823296 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/BANNERMOD_BACKLOG.sqlite b/docs/BANNERMOD_BACKLOG.sqlite index 9fc540110cef1dba01ec4d0a59d5682380c6a26e..a126cb3caf15ebe41617e27cd3d17fb0486d606f 100644 GIT binary patch delta 1333 zcma*lZ%i9y90%~;-E;j%Sv$u6!nhS-F#fgt>6jJCvY-xil7YqyUDmZ{cS3u$J(y-M zV9ye3q9(8ss&i`RU<Gu_?Cit*GeOkNS7M@xCYtQUjK;lTZ+znt{2npt8xvnVpS#@e zd46}lZ*Dx@I-YKwaB{}UihZ2%(veZ$SO*<27!2+7Alyy+pqCziR{9XkqrK2ZAAtGv zKDdSMgLb+XI%qfCO1oeI^}%iQZdgb=VG;GhV%iQ%Xe%tGZdgW}VL5Gt&M~?RmkL@3 zD`_p<POD)R-2o-K9qynN&_&B(H7$d8(Nb7Li(xG-f;(v;tfSlDE?NK^sRK4qJ8Ypg z*i0?ZO^wh)32dbzk4szfhkJ&&{n?(bLC*C2NxgR>nYs7Lv9rUteO&=>gut6JNC&`M zesJzEn0*4AKLlnT2bssf1wGO^J<)kR(1nM=+Yf?w_JfP}gLiwtdwP_MdXks)Am7(> z%<3^--UB}HhGDJ^Fb`O20au#9hYjGPdhqd1@JS80>H?ouf#piD;sl@RS*_~he6A<; zg&xtDdN$YeXs&MoU+KZTW(8-m>)l0tr^LmCxX3YakzXNPJ-c?9-;<YH<B5ZjW&VzR zdYLCyL0B?azZK_&rR=0<L0~t3<<~i`fqnNI-ypIpxA{i#^bOG>+%i}vObOu@cKsx< zL5nbJWXB7Hs5m8v7A|M7ej_@#9IA61`}ud?X<>~-Xc4E^IniX8GMFzKrjnDM7uYW) zLYbKrx`ckyX3dx~<hzY29DLEpLiNI{Mz-n})_7Y!=HU4g2J-@c0#%i_ywpH>v6_FS zJ9V~AcsjXp$eB8l<?~rbyAU9$@mj7R_4SX$kve=m&zLk`C~Q>Xk{0s^hUB2+j|8Ql z8i=>_59}Ryw7)qVl!leaP+U?ppA=J7Ew!~nD3&9tG^|EKa!m3^qq0ASdPE8-niSXk z+DP0dDdDIZ)AWWTK|I!5N&z(-Ry6$~Hg|Yik9y@!k91toj!D`vS>ejL31l9G2s8 ze@ITQeBUd0+%7iXDGcs5^SsMoUg2FBvY5BL?4$?88vaaW^3-|d?-{esnW=w`SaMJf zsH1WWJ)<#2jWxy5Sy3aKL)0C8P714|N+h)TdXLxBgi+<czStkrl&AawP14llxta1R z-~Wik{!c9Ovr1e`-kz>15BmON@Ve9^9YR~`WWrXMT)SDSPnunx>oH};i4A*#L<L5o z1b<(e5Jo1*RTJ0CR#T+QoEs;^ksxV;C1M#FNt?LEjCjQC$cTMrM?#F6nHaTfR53Dc ryJOEqZN!lw6PCUVS-0S?zG2}Uv!WHdB^`$qN8DDdJSMtre{}x|da-1z delta 985 zcmYMwS!h&O9LMo{&vNJ9n~C?Dt%-?AG{&fzm_-}c7$vptGNdAs*pd>8sI|;f1z(c> zTO9Cd!VELmx{T{+tTh^wlW1Jxn7TYED%d_*ND(ZQytPni`a2@@;e5`4|KY&-_NJS9 z(@n8bQ}mU!nc`~K*Q<J#(GJ5fnyDSS6oxsp9R_JD%%#n625p2MeGGkSgEMIz%%fJA zPc5*3R>4AA35#es{D7KaF)f89)C6Zy11zP*u#7@*b`LGWR!$3G1<i+*R0ZeI95|QC zVHK6Zc{B@FQwf|;#ju8o-~uXy3#kAuqI?*lJXl9QtS1jPP%dmF7dBA<E+Gk*lH}Mv zjQ_HFzqxsEeM_eqh<>BrJ{-@i`LyTzFLC@~FF3Xb9RC8G=msZ02kFni>D}N|2RO3} zoZAV`>ZIp&(TomyVH>!(1@vzMmo|VO*MrMC<xjfg6&-Rwcf6`IURwbMn-9S2OTbVg z7_JAZ4vf@-(S_hf4Y*ki#^!=s72x)4aHka9)vd<$ckby@S)J&<ZZo0N{5%6Z2!e;Y z%cH@m*21uoU3}WJ89!v$<NVNIU*lKYInqYkWi?A2NY%FUz47jSl_}peJvG+O6C(Ae z$Me+qF8)#ATN#VWn5krpjSI6@JxH(>Imx?BC^5j^dc$cZe4E*mhWo;v#M3WrHIijh zrny*MoM5#w`O38HQNwMuem6%kzhtWCk6B2n;on%PlYGUbBSJ>-FQ-$4FfJGB=^Iur z)gRB;IV&0CmUYkwyk-Y6+s{<&R~C^fF~v&kce4V+*~1Djvzfa5H~S=@QWigIC40GL zXAQfb$Bk^fuW?8<<nkxXIg(FIR<VSa`Yw;}`~TBL`Mmr6r~8X}$Fiu1a3vxF|BDIX zE72=%2h2?>8x<{fG9qMDrUjPXt&B_u1~RhC_A@eU`%xLOm5eyk6&W$$gq84J(;t%V nw5LQRjv2Yu1r084hFz>2CBwmgT9aKtOIc~>U)+m1Z@vEjAxi40 From 1538aea2ff59617943d76a79417320e44547baa5 Mon Sep 17 00:00:00 2001 From: "pozdn.r.a" <kaisergrobe@gmail.com> Date: Sun, 10 May 2026 12:15:01 +0700 Subject: [PATCH 4/7] test: pin war list outcome menu contract --- .../military/gui/CriticalUiStateVerificationTest.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/java/com/talhanation/bannermod/client/military/gui/CriticalUiStateVerificationTest.java b/src/test/java/com/talhanation/bannermod/client/military/gui/CriticalUiStateVerificationTest.java index eeb3edf8..5863dcda 100644 --- a/src/test/java/com/talhanation/bannermod/client/military/gui/CriticalUiStateVerificationTest.java +++ b/src/test/java/com/talhanation/bannermod/client/military/gui/CriticalUiStateVerificationTest.java @@ -62,10 +62,16 @@ void worldMapActionsKeepVisibleGatingAndSyncStateReasons() throws IOException { void warScreensKeepWaitingForSyncStateSelection() throws IOException { String warListScreen = read("src/main/java/com/talhanation/bannermod/client/military/gui/war/WarListScreen.java"); String politicalEntityListScreen = read("src/main/java/com/talhanation/bannermod/client/military/gui/war/PoliticalEntityListScreen.java"); + String enLang = read("src/main/resources/assets/bannermod/lang/en_us.json"); + String ruLang = read("src/main/resources/assets/bannermod/lang/ru_ru.json"); assertTrue(warListScreen.contains("boolean hasSnapshot = WarClientState.hasSnapshot();")); assertTrue(warListScreen.contains("? \"gui.bannermod.war_list.empty\"")); assertTrue(warListScreen.contains(": \"gui.bannermod.war_list.waiting_sync\"") ); + assertTrue(warListScreen.contains("resolveOutcomeMenu = new ActionMenuButton")); + assertTrue(warListScreen.contains("text(\"gui.bannermod.war_list.menu.resolve_outcome\")")); + assertTrue(enLang.contains("gui.bannermod.war_list.menu.resolve_outcome")); + assertTrue(ruLang.contains("gui.bannermod.war_list.menu.resolve_outcome")); assertTrue(politicalEntityListScreen.contains("String empty = text(WarClientState.hasSnapshot()")); assertTrue(politicalEntityListScreen.contains("? \"gui.bannermod.states.empty\"")); From 0b5cb1d1f4b6589c3f472ef5a17d559a0aa5760a Mon Sep 17 00:00:00 2001 From: "pozdn.r.a" <kaisergrobe@gmail.com> Date: Sun, 10 May 2026 12:19:19 +0700 Subject: [PATCH 5/7] backlog: close top UI audit fixes --- docs/BANNERMOD_BACKLOG.sqlite | Bin 823296 -> 823296 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/BANNERMOD_BACKLOG.sqlite b/docs/BANNERMOD_BACKLOG.sqlite index a126cb3caf15ebe41617e27cd3d17fb0486d606f..981c3bb0246073f0db4c71bab726d4cff629b161 100644 GIT binary patch delta 1858 zcmZvcTTB#J9LAa1nT16zTNM>iIe>RqcDY%FA|j<0<yNKIw5B-B9+nBq%w%R3N*@ZI zCMCXUX`D1o>$cjiR$HM|r#BUAt?iS2EQu*L>04iX*VKLoP)lR7=Qk&3=ln0<_um~& zR*ohsj~56TC)P9x8Iwl_>qe?rS6W(HCEE|yuuia=wS&*HR&X2J3udwx!6obkFpD*T z*{l)FVGUp|s|SOu4t$2yf=k(Ua2eYME@#zX9;*abun4%4m4mBT8JIu9w&JmxZ3YY2 zMsN))0oSr3a2;C*7P2)UVXMI+mJb%QRbUBQ0j_6x;0CrF+{l)Jo7qxuD+_{UEEf#1 zEU=UXz%cWJ<xB!2OcwE|7@v8rU)Vd<T>qMoK6aGHerG(jvuEU#hHWwid_sYbRp3(v zIQ=@1It-k775MBG;M_srELZb$F6Mcz<%Krji+#YCEx^S+z(g}J$yNM{i#WwK{F+O6 zi7R+{2jFq#zNzBrzO4Yh3j^PWfFDYMD_el8n}BN@fazl3dLi)RTHr<j@DrEmChz2D zF48To&M#b+U%4tD<NzOWO+K9ZyJ2<b+wz1bPYBol5;l3a-V+;2J^ydw!(30E7EjB< z7Vq3m@o=Um-xY<Re^;7+CT-XFiHd5^_50rU2i`4VKDk6Z6LxoG`-a@Rny<h;GA?F& zzi0bI(KYW&OWjk)#GH{X!wl6BI;cgdu2N!A)v%&8T1!eQNT+2C7!Ez4T9oR<)@Y1| zs2<aFN-Yvim~l;297>{^t@OlclsMGV6>FGiHi;QJts^CsM6qprG(Bva!(q#aCI0)q zVdTXf&9sSPQDR!urn-Y$Rqj#0ymEx-2EvBohT4zds-ZiU5w}UgrlhN*qqVE0v%9@x zcWZNZYe&<*=I3#-njA8$erg?X6enRX)IFf+N{kL5G>IuDFHVgcHX`;6lS3W*_ciZ- zxg`{dRD`h_Id*e2k&z+I>4PA={Foj_mjjxO%a7_%^e7oZ<j~`jC{;CEGjvo^Lk=lc zt7bb3Mf52)LfJ-qkai>-)fk{{R8Nq4qSGPLs5-dC4|XRU$I!`KW*v<yaqUf18cS&9 zJ&LYVYru$>4=Gl69HGn6Q=)t!YPT^Twv3n>UYzR=X$CZhRMb>$SEy21P7YE_>*b~) zT`g&Yk*cNiItQHLICUFmg=IpSyK+V<7E0ao?Y<(neOB<}XkttxZt^WT9s9{^BHkLG zJX_#iX;_l(ZvICoc5mO2f-f~IY9Ddt#>u9G6pkv1DAM<8dQ{V6giA1Akwc^5UPFbE z(Z`cl`@d;}x^q*o6ZhYsizb9{<yC{Hm}_%!A5GjJMRmHUjahPM??`!j7F!tRu8~J( z%?-qpV5rcHQ&jkPHi)Cx{U`thmHr`Z5yI|;c3)vz-C~C~+0Z%EGw<Jm7Z{Itd_I0W zu9&=CO?Ql^41zm7Bdw@>;@s0MBUVC31mg5zh;97(sM~olJwIo$0CPsV!k(;rch!tk zlpYE>kivfA==^t+V`AYWQyXWcwea@OjLZ#g^)+d|YaA1{x({v#0x9{P81&?z?_u|p zloWzf^6`hcW0L9fBvX2jd-=E|1wCn0x|%MucsItR`izsNB#+5S35!4dDLI)wk&=&O z1XFTnMsQ3vGg7h{h<LJ@g?}??A|N0^GQjtvSr2kgMzZ`V|8#(O{(Cq~$#(<6OL8W5 W63IlaV5Wc|kxZ;5o*c=#-|#Q)u_})M delta 820 zcmWmA%}*0S7{~GMc4qrRq06hhQ;NL5m4c`s7DG@$G>sZV1WXkhLrky-gGb;o+5?x8 zCN##VD~c6g!3yImf~e>XFB)&ec=O;tKvI8mn9n?$O?GF#6NY!f@J0$0ZMv{a(G~+^ z?c=-Xq{(Dzq2q8j`C%&^gLdkJDbx!cbQq@6L6}Be&`AfNPCl4U9WaC1VJ5Y~EZPIJ zsTJl>3*13om`fg*M-4Ea>R`b*)nF^6Dp*7nu$anV36(+@6~j_0gl@`*Wt0bZQZ6i~ z99Tiwu#&Q16=lL2%7AsG!+LVUMsmOgvOy1Nu!$J-5?8S`M}O_RsPrVdI|dZX^-1~d za5TRE-1w~#jJGd<J3(L!gW0p-?inyQ2=1K*_xnNo6nG$OK9n)%Wy?oL!Q&&~Ne_5> z2t4Zs&t=608F5iId?6DiWWkqv!ICVu+$?`5n!u|@@VXwnsReJV!MjTEz8tK$!G{vC zS_IY#z(<*BUEbuAjPzO7`69Dy$SPmcz&F|CX5z0e-!FK;oOBB2H!J#89uYjEto>1{ z#l~gTS1Ysyb(Sl&Vtz>-w8y!j>VmJTiY~M*HR-fyF;l8XizTL;rZmyHsov0R*ZRoJ z%hV|)_WOr=g<T2-N2A}3+*tn%bBM{Gs;WjKC$iFq>Rj&N*vR?d=y1^8<{B6h$$u&j zgjvWeScq*qXF`k#7GWzErB|%SSci3af-ybJ42H$AG{FpuvJmG1s~+cms~+YdYn+E{ z9-Pr8@3dgUN%@vdaS2VgC!KTLj%ODp!8+~ood<K6`R!OKXyDlPEgLqb3$|(7cK!$C C6S*D$ From 636328e2dd0455829fe1c4cb4bb9f40d1b337b2a Mon Sep 17 00:00:00 2001 From: "pozdn.r.a" <kaisergrobe@gmail.com> Date: Sun, 10 May 2026 12:29:09 +0700 Subject: [PATCH 6/7] test: pin recruit crowd render counters --- docs/BANNERMOD_BACKLOG.sqlite | Bin 823296 -> 823296 bytes .../RecruitRendererParitySourceTest.java | 15 +++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/docs/BANNERMOD_BACKLOG.sqlite b/docs/BANNERMOD_BACKLOG.sqlite index 981c3bb0246073f0db4c71bab726d4cff629b161..a46d4d0231619df6321c251f2ce537e54a65949c 100644 GIT binary patch delta 2742 zcmZXTS#%rq6~?Xcj5a%H2qYvVA(I6NV8@C{X$VbFVmshKY~eVBK!~JBV_S)=QMAM{ z6s-HFg_dp#E^%ABh{d=xU6i=Cy4QrZba~NxU-ZsH_0-eTa!9{#IC<h6pP%mBd;fR- z^L^LlYHzvPTiM_VT-?~_30#R^?(z5*JHvnPnE8<!ZEI_*suA!9DhmEk4S{c{$HBj; z!{A@lqu__?5cn6>2mV<d0RN;O20u{yz-#Iu@O`xxTv2<#KdJ}7Kd9Z{?^Q4OI~4|h zt9F9FQC;A#XVks;`IXuM*416$vf2hNsV(3y)g9oX+6Z1%w}V&Ida$O}fxl2|!Jn%& z;Lp^p;7`>p;7`<Q@W;vo-&G;-9n}dgr~vr3@`Ce92j5gP8h+kVK->4_1#nKD124%G z__~z9Z%YB3m2=>0;(!-r60FEs@LO^Q{HCPAZ^#7rbr}c0CN}u0q`<FA68wss249f` z_+^QKUy@TZ_<2d51(loxMV<l6@-#RjC%_lwDewh34t`Od1fQ2<;B!LbrsXJjL1^EU zJOQ2;npl!yuqd>$AV<Kw(9m=87?=~<>d0Ul-k%k^J1LKV8KK8#WdJ-Q{a{+?_k<h- z$AzwsNd&Zo-lt?gm=tcnY2gMWgc}eSZa_@90jGo;@T_nHP6{{R8Q}&zE%)IY+fE2K z;3>HWJTBaWC*^MNm~b0LWjlCOxDzA7op?ela9B2jM}+(FxNHI+lMUdYa5D}IH{(&^ zPCO#qhC{+_7?4$9zXZWP@qsf3g^O@NJYa-wJj^ZF|2(ko0`P_Nz(d@Cy*c2)EU+g7 z?4sjgTD_A__t4_|=xz5Y;2xUUMHla-eRtEh9W?ANI&~*4+D=zyw$YERgTNLVU{U$a zl>82Ay@?`kq`Dg@?(Gi(>nZBG`+?hb0c(4JHQm6i(6j9p>bRN$-h3xuZUsWr>L!Y` ziW+tDU#NpR1=mKv2Po6$snTwW^tqdWUjD<}Pn`mOz|a4g?@}qRrh%T&M_}9cLLV(& z+%*tftnPZl@O)?XU&CwsnzpCS_o}uBg)VLJa%#lzR|+0ulmE>Vv&NYh=X&O}(Bf-n z{-Mup`=`G3@PSmyPFZ74vQ)rt!71gFwv}|w+xf(}jeOoJPS}=xAzdh@v*T7W?@Xn- z(~~);P(&?nXH$0G$|Z^ud*|NY?ccIL>r7d>bhcpijSL^}ALx&b4D^j08yt=G4IdjC z?QyfQ!6VV(qoczkvHroMqX&li2CPIj#TU&D|0l41hHuR}`N>2kRydo^<?K`pso0IT zc71w_*twFOpN{42LMc-$+(_PNiX|~Jok|q#&y+rsh-DL52U%WXEMHnxD3&IzWGRy= z?6eNeP5;}weP+lhTFH!4pa#)_kwe|#aPMB|l(M_?_W86u1r?G`b}XHrgp9?Bbiv|Z zwouHMChcr7QA|78o=@e)iE-SzfQ*H+DLV_voor^h3twGy^`5j7g;E}qvQD;p!cOc+ zWpKdCB=X~Sx9h<T>G8dJxW#wy{<E8%b*n9;oH2BqE2TN)^z`hobLDE(FzZGx;4$mo zO5l2E!H9Mu5$U`hs_WrSqz%iBO~gwE!Uet6VJ_%RqS0a2^_32fxnR`rY*g`VRPbE) zmOC)P$bk!boV`&Z!UJJ87?#^+qG8_Pu~v}BnnB*75kz9eP1J&);3ieZMg_&XUPioN z<bpt)eNi_XaXZ3pY`Gm~(405e)d~PjqCu<#aH!n*ztD0upyN*QPOGlhpaABv6r~J$ z1g&})&${K7%m6+gmAc;Y^Da$4yBdB9fCL0UXjEDAR{Wo!R%R{d*5a&1aiXqA+|yw< zw)`K5mQBBb$^QR_JVVPZAKhsB@DN>Y_;e2U@ohEITlG=X3aOWofeJp}+Q;e0;?{W9 z!+6#$w{H5N0ef0r4)*e7!^@K^Ow`;&6$y-G+FSMlIU-I((dCV>%Luc}Yq_zBc;3re z%V4c(u+~6~!3L(+h$>Ox7nR+5&h3r6any|?h!?zJw0JG|uxa4FJkioQv8i)n11=dW zL=B1<RUOuq^=r3zUao&IryulV&5*j3(+z0BwkX>oIE!859K&*B6Y;#kTC1I6HQS+6 zquo^lp;1E!kx_#hq+V|SIP_M|e777hua<eOc>DFu@X$RIZpW)E!X#Rn1tByJUO}kW zw08{8d-eC0w39(x8il%E(O`I4qpvybn(nz=|FEd->%b0bNJUY-6xHw*5j+}Ujn}a> z?!Sq6$#93aa9_PiG*H)9Jp8~K>8*O)tCM;e8SDoUXGhfSh`1eL*1Q&vm`E%cz@m=9 Yi$;?X2ET2r;Hk!^D)SZ2j6#n814Ew5)Bpeg delta 2260 zcmW-h*Mn0<6vlIRXR^u7Lay|_p?AnGh!la)JA|e8(4<2UDT1&$V^`F`BB-cDVTrhw zKxAV#f{NuOe?s1QFwcOj-!~8W-81K$nK}1-=O&eDNM#z*y`8|hKC_&_%|z>1$Dgl_ z{vB!>XIA=rz6R3>K563Mc(W9I$}9#an1!Hg=7S+K7pyh2zz${x*wIV}!)7YznJM5C zW-{2xOawcd31Am99_(rwz-}fAb~j_d9%dBSv&oFWqn8;5_BKPnK4u`;*YpSbnSNlM z=>tYgFR;Jq2@Wtlz=5V4ILLGb2b(V75YrhPW;%f*j0cV~VQ{pm1xK17IM(>VF-Cz= z(^Q2=y#ag^<uo`+PJ)x=I5<U)f=|m4aH<>zr^z93x*PywvJadgd%>Bq2b?9l!P&A4 zoFhBIxv~SCC)>gKvJG4)Tfs%L1zaqfoA6j7o4}>A5nLu4z__djm&-b^QPzShWDU4d zR)ec#6}Va`+!|Q{t`*9+PL_k~g(7Z{W#C4kl$&G;xLGLZ7Fh%)gtBgx1wP!rO{ng6 znFsC=YP?hCfX~QmaF<Z;-7*v0BUF8_#K3(*?f1(x@PKdv4hkpWkZ=N$!U;GmoPcMA z6L3U00nZ61;HYo{j>$+L?msS^fD<wtd|o&QCuJyjN;nNK$RO~va3;<OXQD|Wphz9~ zqHsQ5lD^=}(i?nLI2o@9C*w8YOuR0fhBt)M@TPPC&x#AaB{g8v+rmM3M;tIsHO_Gg z&YuFBPXHH=0q=4GE+&CX2Z0aw16QbchEjh@r9Yv>E!6hvW*|#3uTjP8l<x+0`;3Bp zPNiBY(HB&u=}YSI)dJva3Xo&xZ?fgL*y}u-{0+N%o6T*T1QgiRZ^r@O)dSy;1%4O} z{D^+~eqtYgW&?j23={_dzp__%*rdDc(Qo{OerKQl=oSO-u}y!nOMkIRmpcL<^Mm<_ zefl5(yvJ|meRis*stR<%_d#DUd_RA#evX^Z)L+(4p!wfrUGpm8cr~ik7yM7PB6vmR zCpI?fz*(Q8`vxu@X+FQ^Omn6&oUdE^k80ku<?8ih&}&NvwO7!|+V*fkC2Aemacjq5 zXLYm|j)--5aNO3`tb~|X2DAQB$Se4ZA#B$<y8Rj0S)C4HcM36`WK1ROmblG`+3YCu zR0Mg-3wbRHxvJvYgI(?@xm7%ww$8Z^!)+N?AtmkJQO?l9y$kNtP*$Z}xRUOpa9hGg z<L))>3@_YR<!*EfI_kF9;R@Hgtz0LP)s-NMDibB57({ug+K1tbsgNIq2Orm=(BPx+ z#bOZac<?dGhy__vl=Me}d^InKKG0H?0B$b_*r*arT@0}B92^Cmv3A-9Qw$~pc!zl4 zPIqTP;qI=$%s@fM;JOgfXmbEZL<le7wP>bSYItM0hBuaKSZxiwIe4@F3`~C-A(U^^ zlGaXGJ8rkcxFt$N2(Jb;+i=CtZDqgJ&d&~8M-C3Oz}jgWO!;wBl1LD7=EiJ1N~#F6 zwCBgcc3sh2mo-o#ikz#QW}h-xt2EXsWs{POCm4?-jvm8{9+UnEJ=z0rEB!F+q}nQq znPv4L?3lKaT@)2Wi)e_HQxqwqctBe5uoUd9PGVMc0*<VTb8KRY(-fscM;P;9wkU=w z)!baJ=H?P?lwJ)-4i1!F&C;<}DTMMm$r@9^2jN>ORu!|ml6F@FsoJZCel#joK$$2J zMO?3QRlFlpWs9|T3O0&mMuHh}>y6orsI?<iEZl?LqM1=~SV7t0i<Ed!(K>Q)6m*7e zmByJWMX*}@D=;l?(_(gCl$S>6^{{S1!>+0deq}GOaAsZ?+rxR4qm`kRwg*(V<MW7C HKMMa3xlSD) diff --git a/src/test/java/com/talhanation/bannermod/client/military/render/RecruitRendererParitySourceTest.java b/src/test/java/com/talhanation/bannermod/client/military/render/RecruitRendererParitySourceTest.java index 133ffc4e..bd2a773a 100644 --- a/src/test/java/com/talhanation/bannermod/client/military/render/RecruitRendererParitySourceTest.java +++ b/src/test/java/com/talhanation/bannermod/client/military/render/RecruitRendererParitySourceTest.java @@ -72,6 +72,21 @@ void crowdImpostorKeepsNearReadabilityThresholdsBelowDistantCut() throws IOExcep assertTrue(source.contains("distanceSqr <= square(16.0D)")); } + @Test + void crowdImpostorKeepsRuntimeCounterHooks() throws IOException { + String lodSource = read("RecruitRenderLod.java"); + String eventSource = read("RecruitCrowdRenderEvents.java"); + + assertTrue(lodSource.contains("CROWDED_RECRUIT_COUNT = 48")); + assertTrue(lodSource.contains("CROWD_IMPOSTOR_DISTANCE = 48.0D")); + assertTrue(eventSource.contains("RuntimeProfilingCounters.increment(\"recruit.render.normal_skipped_for_impostor\")")); + assertTrue(eventSource.contains("RuntimeProfilingCounters.add(\"recruit.render.crowd_query_results\", recruits.size())")); + assertTrue(eventSource.contains("RuntimeProfilingCounters.add(\"recruit.render.crowd_impostor_candidates\", candidates)")); + assertTrue(eventSource.contains("RuntimeProfilingCounters.add(\"recruit.render.crowd_impostors\", rendered)")); + assertTrue(eventSource.contains("RuntimeProfilingCounters.add(\"recruit.render.crowd_impostor_nanos\", System.nanoTime() - startNanos)")); + assertTrue(eventSource.contains("RuntimeProfilingCounters.add(\"recruit.render.crowd_impostor_frustum_culled\", frustumCulled)")); + } + @Test void profilingBucketsKeepRuntimeCounterNames() throws IOException { String source = read("RecruitRenderProfiling.java"); From 197bbaaccd6dd3892885101f7615918fc4c1ca50 Mon Sep 17 00:00:00 2001 From: "pozdn.r.a" <kaisergrobe@gmail.com> Date: Sun, 10 May 2026 14:23:48 +0700 Subject: [PATCH 7/7] feat: add visual scenario harness --- .eclipse/configurations/Client.launch | 10 + .eclipse/configurations/Data.launch | 10 + .eclipse/configurations/GameTestServer.launch | 10 + .eclipse/configurations/Server.launch | 10 + .opencode/agents/code-reviewer.md | 18 + .opencode/agents/code-simplifier.md | 15 + .opencode/agents/jd-tls.md | 15 + opencode.json | 16 + .../bannermod/bootstrap/BannerModMain.java | 1 + .../client/military/gui/PerkTreeScreen.java | 79 ++++- .../gui/overlay/HudOverlayCoordinator.java | 2 + .../scenario/SkillTreeVisualScenarioStep.java | 26 ++ .../scenario/VisualScenarioClient.java | 335 ++++++++++++++++++ .../admin/VisualScenarioCommands.java | 115 ++++++ .../commands/war/BannerModWarCommands.java | 2 + .../catalog/MilitaryPacketCatalog.java | 1 + .../MessageToClientRunVisualScenario.java | 46 +++ .../bannermod/scenario/VisualScenarioIds.java | 60 ++++ .../assets/bannermod/lang/en_us.json | 56 ++- .../assets/bannermod/lang/ru_ru.json | 56 ++- 20 files changed, 870 insertions(+), 13 deletions(-) create mode 100644 .eclipse/configurations/Client.launch create mode 100644 .eclipse/configurations/Data.launch create mode 100644 .eclipse/configurations/GameTestServer.launch create mode 100644 .eclipse/configurations/Server.launch create mode 100644 .opencode/agents/code-reviewer.md create mode 100644 .opencode/agents/code-simplifier.md create mode 100644 .opencode/agents/jd-tls.md create mode 100644 opencode.json create mode 100644 src/main/java/com/talhanation/bannermod/client/military/scenario/SkillTreeVisualScenarioStep.java create mode 100644 src/main/java/com/talhanation/bannermod/client/military/scenario/VisualScenarioClient.java create mode 100644 src/main/java/com/talhanation/bannermod/commands/admin/VisualScenarioCommands.java create mode 100644 src/main/java/com/talhanation/bannermod/network/messages/military/MessageToClientRunVisualScenario.java create mode 100644 src/main/java/com/talhanation/bannermod/scenario/VisualScenarioIds.java diff --git a/.eclipse/configurations/Client.launch b/.eclipse/configurations/Client.launch new file mode 100644 index 00000000..400c7fb9 --- /dev/null +++ b/.eclipse/configurations/Client.launch @@ -0,0 +1,10 @@ +<?xml version="1.0" ?> +<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication"> + <stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="bannermod"></stringAttribute> + <stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="net.neoforged.devlaunch.Main"></stringAttribute> + <stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value=" @/home/kaiserroman/bannermod/build/moddev/clientRunProgramArgs.txt"></stringAttribute> + <stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value=" @/home/kaiserroman/bannermod/build/moddev/clientRunVmArgs.txt -Dfml.modFolders=bannermod%%/home/kaiserroman/bannermod/bin/main:bannermod%%/home/kaiserroman/bannermod/bin/gametest"></stringAttribute> + <stringAttribute key="org.eclipse.jdt.launching.WORKING_DIRECTORY" value="/home/kaiserroman/bannermod/run"></stringAttribute> + <booleanAttribute key="org.eclipse.jdt.launching.STOP_IN_MAIN" value="false"></booleanAttribute> + <booleanAttribute key="org.eclipse.jdt.launching.ATTR_ATTR_USE_ARGFILE" value="false"></booleanAttribute> +</launchConfiguration> \ No newline at end of file diff --git a/.eclipse/configurations/Data.launch b/.eclipse/configurations/Data.launch new file mode 100644 index 00000000..c4650933 --- /dev/null +++ b/.eclipse/configurations/Data.launch @@ -0,0 +1,10 @@ +<?xml version="1.0" ?> +<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication"> + <stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="bannermod"></stringAttribute> + <stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="net.neoforged.devlaunch.Main"></stringAttribute> + <stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value=" @/home/kaiserroman/bannermod/build/moddev/dataRunProgramArgs.txt"></stringAttribute> + <stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value=" @/home/kaiserroman/bannermod/build/moddev/dataRunVmArgs.txt -Dfml.modFolders=bannermod%%/home/kaiserroman/bannermod/bin/main:bannermod%%/home/kaiserroman/bannermod/bin/gametest"></stringAttribute> + <stringAttribute key="org.eclipse.jdt.launching.WORKING_DIRECTORY" value="/home/kaiserroman/bannermod/run"></stringAttribute> + <booleanAttribute key="org.eclipse.jdt.launching.STOP_IN_MAIN" value="false"></booleanAttribute> + <booleanAttribute key="org.eclipse.jdt.launching.ATTR_ATTR_USE_ARGFILE" value="false"></booleanAttribute> +</launchConfiguration> \ No newline at end of file diff --git a/.eclipse/configurations/GameTestServer.launch b/.eclipse/configurations/GameTestServer.launch new file mode 100644 index 00000000..7268be23 --- /dev/null +++ b/.eclipse/configurations/GameTestServer.launch @@ -0,0 +1,10 @@ +<?xml version="1.0" ?> +<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication"> + <stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="bannermod"></stringAttribute> + <stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="net.neoforged.devlaunch.Main"></stringAttribute> + <stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value=" @/home/kaiserroman/bannermod/build/moddev/gameTestServerRunProgramArgs.txt"></stringAttribute> + <stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value=" @/home/kaiserroman/bannermod/build/moddev/gameTestServerRunVmArgs.txt -Dfml.modFolders=bannermod%%/home/kaiserroman/bannermod/bin/main:bannermod%%/home/kaiserroman/bannermod/bin/gametest"></stringAttribute> + <stringAttribute key="org.eclipse.jdt.launching.WORKING_DIRECTORY" value="/home/kaiserroman/bannermod/run_gametest"></stringAttribute> + <booleanAttribute key="org.eclipse.jdt.launching.STOP_IN_MAIN" value="false"></booleanAttribute> + <booleanAttribute key="org.eclipse.jdt.launching.ATTR_ATTR_USE_ARGFILE" value="false"></booleanAttribute> +</launchConfiguration> \ No newline at end of file diff --git a/.eclipse/configurations/Server.launch b/.eclipse/configurations/Server.launch new file mode 100644 index 00000000..b66a7ea9 --- /dev/null +++ b/.eclipse/configurations/Server.launch @@ -0,0 +1,10 @@ +<?xml version="1.0" ?> +<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication"> + <stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="bannermod"></stringAttribute> + <stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="net.neoforged.devlaunch.Main"></stringAttribute> + <stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value=" @/home/kaiserroman/bannermod/build/moddev/serverRunProgramArgs.txt"></stringAttribute> + <stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value=" @/home/kaiserroman/bannermod/build/moddev/serverRunVmArgs.txt -Dfml.modFolders=bannermod%%/home/kaiserroman/bannermod/bin/main:bannermod%%/home/kaiserroman/bannermod/bin/gametest"></stringAttribute> + <stringAttribute key="org.eclipse.jdt.launching.WORKING_DIRECTORY" value="/home/kaiserroman/bannermod/run"></stringAttribute> + <booleanAttribute key="org.eclipse.jdt.launching.STOP_IN_MAIN" value="false"></booleanAttribute> + <booleanAttribute key="org.eclipse.jdt.launching.ATTR_ATTR_USE_ARGFILE" value="false"></booleanAttribute> +</launchConfiguration> \ No newline at end of file diff --git a/.opencode/agents/code-reviewer.md b/.opencode/agents/code-reviewer.md new file mode 100644 index 00000000..66f22c1e --- /dev/null +++ b/.opencode/agents/code-reviewer.md @@ -0,0 +1,18 @@ +--- +description: Independent BannerMod code review for pending changes or PRs. +mode: subagent +permission: + edit: deny + todowrite: deny +temperature: 0.1 +--- + +Review pending BannerMod changes as an independent reviewer. + +Primary goal: find bugs, regressions, security/authority issues, missing tests, and behavior mismatches. Findings are more important than summaries. + +Use the repository rules in `AGENTS.md`. Inspect the worktree with `tools/ai-context-proxy/bin/ctx status` and `tools/ai-context-proxy/bin/ctx diff` before reviewing code. Use `ctx file`, `ctx exact`, and `ctx search` for context rather than raw dumps. + +Do not edit files. Do not refactor. Do not propose style-only churn unless it hides a real defect. + +Report findings first, ordered by severity, with file and line references. If no findings are found, say that explicitly and list residual risks or tests not run. diff --git a/.opencode/agents/code-simplifier.md b/.opencode/agents/code-simplifier.md new file mode 100644 index 00000000..397dbbd9 --- /dev/null +++ b/.opencode/agents/code-simplifier.md @@ -0,0 +1,15 @@ +--- +description: Post-slice cleanup pass for changed BannerMod code. +mode: subagent +temperature: 0.1 +--- + +Review the current implementation slice for unnecessary complexity and make only the smallest safe cleanup edits. + +Use this after a non-trivial implementation and before commit. Start with `tools/ai-context-proxy/bin/ctx status` and `tools/ai-context-proxy/bin/ctx diff` so the cleanup is limited to the active change set. + +Allowed cleanup: remove redundancy introduced by the slice, tighten names when they are misleading, reduce needless helpers, and delete dead imports created by the slice. + +Not allowed: broad refactors, new abstractions, behavior changes, formatting churn, or edits to unrelated files. Preserve the user's worktree changes. + +After editing, summarize exactly what changed and what verification is still needed. diff --git a/.opencode/agents/jd-tls.md b/.opencode/agents/jd-tls.md new file mode 100644 index 00000000..d0415282 --- /dev/null +++ b/.opencode/agents/jd-tls.md @@ -0,0 +1,15 @@ +--- +description: Java/JDTLS-assisted inspection for compiled APIs, jars, and diagnostics. +mode: subagent +permission: + edit: deny +temperature: 0.1 +--- + +Use the configured `jdtls` LSP and compact repository tools to inspect Java symbols, diagnostics, and compiled dependencies. + +Prefer source and LSP diagnostics first. When source is unavailable or misleading, inspect bytecode or jar contents with bounded commands through `tools/ai-context-proxy/bin/ctx log -- ...` and keep output compact. + +Good targets: NeoForge/Minecraft APIs, optional mod jars, vendored jars, binary-only dependencies, and classpath or symbol-resolution failures. + +Do not edit files. Return concise findings with exact class, method, or jar references and explain how the result affects the current task. diff --git a/opencode.json b/opencode.json new file mode 100644 index 00000000..9bf23588 --- /dev/null +++ b/opencode.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://opencode.ai/config.json", + "lsp": { + "jdtls": { + "command": ["jdtls"], + "extensions": [".java"] + } + }, + "mcp": { + "context7": { + "type": "remote", + "url": "https://mcp.context7.com/mcp", + "enabled": true + } + } +} diff --git a/src/main/java/com/talhanation/bannermod/bootstrap/BannerModMain.java b/src/main/java/com/talhanation/bannermod/bootstrap/BannerModMain.java index ee8ed0de..45b761b5 100644 --- a/src/main/java/com/talhanation/bannermod/bootstrap/BannerModMain.java +++ b/src/main/java/com/talhanation/bannermod/bootstrap/BannerModMain.java @@ -199,6 +199,7 @@ public void clientSetup(FMLClientSetupEvent event) { // See 21-UAT.md gap "Recruits hotkey screens (Command/Faction/Map) and the claim overlay open in dev client". NeoForge.EVENT_BUS.register(new KeyEvents()); NeoForge.EVENT_BUS.register(new ClientPlayerEvents()); + NeoForge.EVENT_BUS.register(new com.talhanation.bannermod.client.military.scenario.VisualScenarioClient()); NeoForge.EVENT_BUS.register(HudOverlayCoordinator.INSTANCE); } diff --git a/src/main/java/com/talhanation/bannermod/client/military/gui/PerkTreeScreen.java b/src/main/java/com/talhanation/bannermod/client/military/gui/PerkTreeScreen.java index 39cd66a8..0f1c9106 100644 --- a/src/main/java/com/talhanation/bannermod/client/military/gui/PerkTreeScreen.java +++ b/src/main/java/com/talhanation/bannermod/client/military/gui/PerkTreeScreen.java @@ -2,6 +2,7 @@ import com.talhanation.bannermod.bootstrap.BannerModMain; import com.talhanation.bannermod.client.military.ClientManager; +import com.talhanation.bannermod.client.military.scenario.SkillTreeVisualScenarioStep; import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; import com.talhanation.bannermod.entity.military.perks.PerkArchetype; import com.talhanation.bannermod.entity.military.perks.PerkEffectService; @@ -43,29 +44,48 @@ public class PerkTreeScreen extends RecruitsScreenBase { private final boolean playerTree; @Nullable private final AbstractRecruitEntity recruit; + private final boolean visualScenario; + @Nullable + private final PerkArchetype visualRecruitArchetype; private int seenSnapshotVersion; private boolean snapshotReady; private boolean confirmingRespec; private boolean requestedSnapshot; + @Nullable + private PerkProgress visualProgress; + private Component visualFeedback = Component.empty(); public static PerkTreeScreen playerTree() { - return new PerkTreeScreen(true, null); + return new PerkTreeScreen(true, null, false, null); } public static PerkTreeScreen recruitTree(AbstractRecruitEntity recruit) { - return new PerkTreeScreen(false, recruit); + return new PerkTreeScreen(false, recruit, false, null); + } + + public static PerkTreeScreen visualScenarioPlayerTree() { + return new PerkTreeScreen(true, null, true, null); + } + + public static PerkTreeScreen visualScenarioRecruitTree(@Nullable AbstractRecruitEntity recruit) { + return new PerkTreeScreen(false, recruit, true, PerkArchetype.SWORDSMAN); } - private PerkTreeScreen(boolean playerTree, @Nullable AbstractRecruitEntity recruit) { + private PerkTreeScreen(boolean playerTree, @Nullable AbstractRecruitEntity recruit, boolean visualScenario, + @Nullable PerkArchetype visualRecruitArchetype) { super(playerTree ? TITLE_PLAYER : TITLE_RECRUIT, 320, 260); this.playerTree = playerTree; this.recruit = recruit; + this.visualScenario = visualScenario; + this.visualRecruitArchetype = visualRecruitArchetype; + this.snapshotReady = visualScenario; + this.visualProgress = visualScenario ? new PerkProgress() : null; } @Override protected void init() { super.init(); - if (!requestedSnapshot) { + if (!visualScenario && !requestedSnapshot) { requestedSnapshot = true; BannerModMain.SIMPLE_CHANNEL.sendToServer(new MessageRequestPerkTreeSnapshot(playerTree, recruitUuid())); } @@ -96,7 +116,7 @@ public void renderBackground(GuiGraphics guiGraphics, int mouseX, int mouseY, fl @Override public void renderForeground(GuiGraphics guiGraphics, int mouseX, int mouseY, float delta) { - if (seenSnapshotVersion != ClientManager.perkTreeSnapshotVersion) { + if (!visualScenario && seenSnapshotVersion != ClientManager.perkTreeSnapshotVersion) { seenSnapshotVersion = ClientManager.perkTreeSnapshotVersion; rebuildWidgets(); } @@ -105,8 +125,8 @@ public void renderForeground(GuiGraphics guiGraphics, int mouseX, int mouseY, fl PerkProgress progress = progress(); Object points = progress == null ? "..." : progress.getAvailablePoints(); guiGraphics.drawString(font, Component.translatable("gui.bannermod.perk_tree.points", points), guiLeft + 16, guiTop + 36, MilitaryGuiStyle.TEXT_DARK, false); - Component feedback = confirmingRespec ? TEXT_CONFIRM_RESPEC : ClientManager.perkTreeFeedback; - guiGraphics.drawString(font, MilitaryGuiStyle.clampLabel(font, feedback, 170), guiLeft + 110, guiTop + 36, feedbackColor(), false); + Component feedback = confirmingRespec ? TEXT_CONFIRM_RESPEC : feedback(); + guiGraphics.drawString(font, MilitaryGuiStyle.clampLabel(font, feedback, 170), guiLeft + 110, guiTop + 36, feedbackColor(feedback), false); List<PerkNode> nodes = visibleNodes(); if (nodes.isEmpty()) { @@ -131,12 +151,22 @@ public void onClose() { } public void onSnapshot(boolean playerTree, @Nullable UUID recruitUuid, boolean hasProgress) { + if (visualScenario) return; if (this.playerTree != playerTree) return; if (!playerTree && (this.recruit == null || !this.recruit.getUUID().equals(recruitUuid))) return; this.snapshotReady = hasProgress; this.seenSnapshotVersion = -1; } + public void applyVisualScenarioStep(SkillTreeVisualScenarioStep step) { + if (!visualScenario) return; + this.confirmingRespec = step == SkillTreeVisualScenarioStep.RESPEC_CONFIRM; + this.visualProgress = createVisualProgress(step); + this.visualFeedback = Component.translatable(step.feedbackKey()); + this.snapshotReady = true; + rebuildWidgets(); + } + private void renderNode(GuiGraphics guiGraphics, PerkNode node, PerkProgress progress, int y) { PerkState state = stateFor(node, progress); int color = state.color; @@ -159,6 +189,11 @@ private void confirmRespec() { private void sendUpdate(String action, String perkId) { confirmingRespec = false; + if (visualScenario) { + visualFeedback = Component.translatable("gui.bannermod.perk_tree.pending"); + rebuildWidgets(); + return; + } ClientManager.perkTreeFeedback = Component.translatable("gui.bannermod.perk_tree.pending"); BannerModMain.SIMPLE_CHANNEL.sendToServer(new MessageUpdatePerkTree(playerTree, recruitUuid(), action, perkId)); } @@ -171,6 +206,9 @@ private UUID recruitUuid() { @Nullable private PerkProgress progress() { if (!snapshotReady) return null; + if (visualScenario) { + return visualProgress; + } if (playerTree) { return ClientManager.playerPerkSnapshot; } @@ -186,8 +224,8 @@ private List<PerkNode> visibleNodes() { for (PerkNode node : PerkRegistry.byArchetype(PerkArchetype.UNIVERSAL)) { if (node.id().startsWith("player/")) nodes.add(node); } - } else if (recruit != null) { - PerkArchetype archetype = PerkEffectService.recruitArchetype(recruit); + } else if (recruit != null || visualRecruitArchetype != null) { + PerkArchetype archetype = recruit != null ? PerkEffectService.recruitArchetype(recruit) : visualRecruitArchetype; for (PerkNode node : PerkRegistry.byArchetype(PerkArchetype.UNIVERSAL)) { if (!node.id().startsWith("player/")) nodes.add(node); } @@ -205,8 +243,27 @@ private static PerkState stateFor(PerkNode node, PerkProgress progress) { return PerkState.LOCKED; } - private static int feedbackColor() { - String text = ClientManager.perkTreeFeedback.getString(); + private Component feedback() { + return visualScenario ? visualFeedback : ClientManager.perkTreeFeedback; + } + + private PerkProgress createVisualProgress(SkillTreeVisualScenarioStep step) { + PerkProgress progress = new PerkProgress(); + List<PerkNode> nodes = visibleNodes(); + PerkNode first = nodes.isEmpty() ? null : nodes.get(0); + if (first == null) return progress; + + if (step == SkillTreeVisualScenarioStep.AVAILABLE || step == SkillTreeVisualScenarioStep.PENDING) { + progress.grantPoints(Math.max(1, first.pointCost())); + } else if (step == SkillTreeVisualScenarioStep.OWNED || step == SkillTreeVisualScenarioStep.RESPEC_CONFIRM) { + progress.grantPoints(Math.max(1, first.pointCost())); + progress.unlock(first); + } + return progress; + } + + private static int feedbackColor(Component feedback) { + String text = feedback.getString(); if (text.isEmpty()) return MilitaryGuiStyle.TEXT_DARK; return text.contains("denied") || text.contains("отклон") ? MilitaryGuiStyle.TEXT_DENIED : MilitaryGuiStyle.TEXT_GOOD; } diff --git a/src/main/java/com/talhanation/bannermod/client/military/gui/overlay/HudOverlayCoordinator.java b/src/main/java/com/talhanation/bannermod/client/military/gui/overlay/HudOverlayCoordinator.java index fc6ee29d..9ce34012 100644 --- a/src/main/java/com/talhanation/bannermod/client/military/gui/overlay/HudOverlayCoordinator.java +++ b/src/main/java/com/talhanation/bannermod/client/military/gui/overlay/HudOverlayCoordinator.java @@ -5,6 +5,7 @@ import com.talhanation.bannermod.client.military.ClientManager; import com.talhanation.bannermod.client.military.api.ClientClaimEvent; import com.talhanation.bannermod.client.military.api.ClientOverlayEvent; +import com.talhanation.bannermod.client.military.scenario.VisualScenarioClient; import com.talhanation.bannermod.config.RecruitsClientConfig; import com.talhanation.bannermod.persistence.military.RecruitsClaim; import com.talhanation.bannermod.util.RuntimeProfilingCounters; @@ -89,6 +90,7 @@ private HudOverlayCoordinator() { public static void registerOverlays(RegisterGuiLayersEvent event) { event.registerAbove(VanillaGuiLayers.HOTBAR, HUD_LAYER, HudOverlayCoordinator::render); + VisualScenarioClient.registerOverlay(event); } private static void render(GuiGraphics graphics, DeltaTracker deltaTracker) { diff --git a/src/main/java/com/talhanation/bannermod/client/military/scenario/SkillTreeVisualScenarioStep.java b/src/main/java/com/talhanation/bannermod/client/military/scenario/SkillTreeVisualScenarioStep.java new file mode 100644 index 00000000..6a0d4cbb --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/client/military/scenario/SkillTreeVisualScenarioStep.java @@ -0,0 +1,26 @@ +package com.talhanation.bannermod.client.military.scenario; + +public enum SkillTreeVisualScenarioStep { + LOCKED("gui.bannermod.visual_scenario.skilltree.step.locked", "gui.bannermod.visual_scenario.skilltree.feedback.locked"), + AVAILABLE("gui.bannermod.visual_scenario.skilltree.step.available", "gui.bannermod.perk_tree.feedback.synced"), + OWNED("gui.bannermod.visual_scenario.skilltree.step.owned", "gui.bannermod.perk_tree.feedback.unlocked"), + PENDING("gui.bannermod.visual_scenario.skilltree.step.pending", "gui.bannermod.perk_tree.pending"), + DENIED("gui.bannermod.visual_scenario.skilltree.step.denied", "gui.bannermod.perk_tree.feedback.denied_points"), + RESPEC_CONFIRM("gui.bannermod.visual_scenario.skilltree.step.respec", "gui.bannermod.perk_tree.feedback.respec"); + + private final String titleKey; + private final String feedbackKey; + + SkillTreeVisualScenarioStep(String titleKey, String feedbackKey) { + this.titleKey = titleKey; + this.feedbackKey = feedbackKey; + } + + public String titleKey() { + return titleKey; + } + + public String feedbackKey() { + return feedbackKey; + } +} diff --git a/src/main/java/com/talhanation/bannermod/client/military/scenario/VisualScenarioClient.java b/src/main/java/com/talhanation/bannermod/client/military/scenario/VisualScenarioClient.java new file mode 100644 index 00000000..14bb4709 --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/client/military/scenario/VisualScenarioClient.java @@ -0,0 +1,335 @@ +package com.talhanation.bannermod.client.military.scenario; + +import com.talhanation.bannermod.bootstrap.BannerModMain; +import com.talhanation.bannermod.client.military.gui.CommandScreen; +import com.talhanation.bannermod.client.military.gui.MilitaryGuiStyle; +import com.talhanation.bannermod.client.military.gui.PerkTreeScreen; +import com.talhanation.bannermod.client.military.gui.RecruitInventoryScreen; +import com.talhanation.bannermod.client.military.gui.RecruitMoreScreen; +import com.talhanation.bannermod.client.military.gui.group.RecruitsGroupListScreen; +import com.talhanation.bannermod.client.military.gui.war.PoliticalEntityListScreen; +import com.talhanation.bannermod.client.military.gui.war.WarDeclareScreen; +import com.talhanation.bannermod.client.military.gui.war.WarListScreen; +import com.talhanation.bannermod.client.military.gui.worldmap.WorldMapScreen; +import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; +import com.talhanation.bannermod.inventory.military.CommandMenu; +import com.talhanation.bannermod.inventory.military.RecruitInventoryMenu; +import com.talhanation.bannermod.network.messages.military.MessageRequestFormationMapSnapshot; +import com.talhanation.bannermod.scenario.VisualScenarioIds; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.client.event.ClientTickEvent; +import net.neoforged.neoforge.client.event.RegisterGuiLayersEvent; +import net.neoforged.neoforge.client.event.ScreenEvent; +import net.neoforged.neoforge.client.gui.VanillaGuiLayers; +import org.lwjgl.glfw.GLFW; + +import java.util.List; +import java.util.Map; + +@OnlyIn(Dist.CLIENT) +public final class VisualScenarioClient { + private static final ResourceLocation SCENARIO_LAYER = ResourceLocation.fromNamespaceAndPath(BannerModMain.MOD_ID, "visual_scenario_overlay"); + private static final int STEP_TICKS = 100; + private static final int PANEL_WIDTH = 224; + private static final int PANEL_HEIGHT = 32; + private static final int SAFE_TOP = 40; + + private static final List<SkillTreeVisualScenarioStep> SKILLTREE_STEPS = List.of( + SkillTreeVisualScenarioStep.LOCKED, + SkillTreeVisualScenarioStep.AVAILABLE, + SkillTreeVisualScenarioStep.OWNED, + SkillTreeVisualScenarioStep.PENDING, + SkillTreeVisualScenarioStep.DENIED, + SkillTreeVisualScenarioStep.RESPEC_CONFIRM + ); + private static final List<String> SCREEN_OBSERVE_STEPS = List.of( + "gui.bannermod.visual_scenario.screen.step.bounds", + "gui.bannermod.visual_scenario.screen.step.actions", + "gui.bannermod.visual_scenario.screen.step.feedback", + "gui.bannermod.visual_scenario.screen.step.overlay_stack" + ); + private static final List<String> COMMAND_STEPS = List.of( + "gui.bannermod.visual_scenario.military_command.step.groups", + "gui.bannermod.visual_scenario.military_command.step.categories", + "gui.bannermod.visual_scenario.military_command.step.targets", + "gui.bannermod.visual_scenario.military_command.step.read_only" + ); + private static final List<String> RECRUIT_INVENTORY_STEPS = List.of( + "gui.bannermod.visual_scenario.recruit_inventory.step.status", + "gui.bannermod.visual_scenario.recruit_inventory.step.actions", + "gui.bannermod.visual_scenario.recruit_inventory.step.perk_entry", + "gui.bannermod.visual_scenario.recruit_inventory.step.read_only" + ); + private static final List<String> RECRUIT_GROUP_STEPS = List.of( + "gui.bannermod.visual_scenario.recruit_groups.step.list", + "gui.bannermod.visual_scenario.recruit_groups.step.disabled", + "gui.bannermod.visual_scenario.recruit_groups.step.selection", + "gui.bannermod.visual_scenario.recruit_groups.step.read_only" + ); + private static final List<String> RECRUIT_ACTION_STEPS = List.of( + "gui.bannermod.visual_scenario.recruit_action_feedback.step.panel", + "gui.bannermod.visual_scenario.recruit_action_feedback.step.decisions", + "gui.bannermod.visual_scenario.recruit_action_feedback.step.denials", + "gui.bannermod.visual_scenario.recruit_action_feedback.step.read_only" + ); + private static final Map<String, ScenarioDefinition> DEFINITIONS = Map.of( + VisualScenarioIds.SKILLTREE_PLAYER, ScenarioDefinition.skillTree(VisualScenarioIds.titleKey(VisualScenarioIds.SKILLTREE_PLAYER)), + VisualScenarioIds.SKILLTREE_RECRUIT, ScenarioDefinition.skillTree(VisualScenarioIds.titleKey(VisualScenarioIds.SKILLTREE_RECRUIT)), + VisualScenarioIds.MILITARY_COMMAND, ScenarioDefinition.screen(VisualScenarioIds.titleKey(VisualScenarioIds.MILITARY_COMMAND), COMMAND_STEPS), + VisualScenarioIds.RECRUIT_INVENTORY, ScenarioDefinition.screen(VisualScenarioIds.titleKey(VisualScenarioIds.RECRUIT_INVENTORY), RECRUIT_INVENTORY_STEPS), + VisualScenarioIds.RECRUIT_GROUPS, ScenarioDefinition.screen(VisualScenarioIds.titleKey(VisualScenarioIds.RECRUIT_GROUPS), RECRUIT_GROUP_STEPS), + VisualScenarioIds.RECRUIT_ACTION_FEEDBACK, ScenarioDefinition.screen(VisualScenarioIds.titleKey(VisualScenarioIds.RECRUIT_ACTION_FEEDBACK), RECRUIT_ACTION_STEPS), + VisualScenarioIds.WAR_ROOM, ScenarioDefinition.screen(VisualScenarioIds.titleKey(VisualScenarioIds.WAR_ROOM)), + VisualScenarioIds.POLITICAL_ENTITIES, ScenarioDefinition.screen(VisualScenarioIds.titleKey(VisualScenarioIds.POLITICAL_ENTITIES)), + VisualScenarioIds.WAR_DECLARE, ScenarioDefinition.screen(VisualScenarioIds.titleKey(VisualScenarioIds.WAR_DECLARE)), + VisualScenarioIds.WORLD_MAP, ScenarioDefinition.screen(VisualScenarioIds.titleKey(VisualScenarioIds.WORLD_MAP)) + ); + + private static ActiveScenario active; + + public static void registerOverlay(RegisterGuiLayersEvent event) { + event.registerAbove(VanillaGuiLayers.HOTBAR, SCENARIO_LAYER, VisualScenarioClient::renderOverlay); + } + + public static void start(String scenario, int recruitEntityId) { + if (VisualScenarioIds.STOP.equals(scenario)) { + stop(); + return; + } + ScenarioDefinition definition = DEFINITIONS.get(scenario); + if (definition == null) { + return; + } + active = new ActiveScenario(scenario, recruitEntityId, definition); + openCurrentStep(Minecraft.getInstance(), active); + } + + public static void stop() { + ActiveScenario scenario = active; + active = null; + Minecraft mc = Minecraft.getInstance(); + if (scenario != null && isExpectedScreen(mc.screen, scenario)) { + mc.setScreen(null); + } + } + + @SubscribeEvent + public void onClientTick(ClientTickEvent.Post event) { + if (active == null) return; + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null || mc.level == null) { + stop(); + return; + } + if (!isExpectedScreen(mc.screen, active)) { + stop(); + return; + } + active.ticksInStep++; + if (active.ticksInStep >= STEP_TICKS) { + active.ticksInStep = 0; + active.stepIndex = (active.stepIndex + 1) % active.definition.stepCount(); + openCurrentStep(mc, active); + } + } + + @SubscribeEvent + public void onMousePressed(ScreenEvent.MouseButtonPressed.Pre event) { + if (blocksReadOnlyInput(event.getScreen())) { + event.setCanceled(true); + } + } + + @SubscribeEvent + public void onMouseReleased(ScreenEvent.MouseButtonReleased.Pre event) { + if (blocksReadOnlyInput(event.getScreen())) { + event.setCanceled(true); + } + } + + @SubscribeEvent + public void onMouseDragged(ScreenEvent.MouseDragged.Pre event) { + if (blocksReadOnlyInput(event.getScreen())) { + event.setCanceled(true); + } + } + + @SubscribeEvent + public void onMouseScrolled(ScreenEvent.MouseScrolled.Pre event) { + if (blocksReadOnlyInput(event.getScreen())) { + event.setCanceled(true); + } + } + + @SubscribeEvent + public void onKeyPressed(ScreenEvent.KeyPressed.Pre event) { + if (blocksReadOnlyInput(event.getScreen()) && event.getKeyCode() != GLFW.GLFW_KEY_ESCAPE) { + event.setCanceled(true); + } + } + + @SubscribeEvent + public void onCharTyped(ScreenEvent.CharacterTyped.Pre event) { + if (blocksReadOnlyInput(event.getScreen())) { + event.setCanceled(true); + } + } + + private static boolean blocksReadOnlyInput(Screen screen) { + return active != null && !active.definition.isSkillTree() && isExpectedScreen(screen, active); + } + + private static boolean isExpectedScreen(Screen screen, ActiveScenario scenario) { + if (scenario.definition.isSkillTree()) return screen instanceof PerkTreeScreen; + if (VisualScenarioIds.MILITARY_COMMAND.equals(scenario.name)) return screen instanceof CommandScreen; + if (VisualScenarioIds.RECRUIT_INVENTORY.equals(scenario.name)) return screen instanceof RecruitInventoryScreen; + if (VisualScenarioIds.RECRUIT_GROUPS.equals(scenario.name)) return screen instanceof RecruitsGroupListScreen; + if (VisualScenarioIds.RECRUIT_ACTION_FEEDBACK.equals(scenario.name)) return screen instanceof RecruitMoreScreen; + if (VisualScenarioIds.WAR_ROOM.equals(scenario.name)) return screen instanceof WarListScreen; + if (VisualScenarioIds.POLITICAL_ENTITIES.equals(scenario.name)) return screen instanceof PoliticalEntityListScreen; + if (VisualScenarioIds.WAR_DECLARE.equals(scenario.name)) return screen instanceof WarDeclareScreen; + if (VisualScenarioIds.WORLD_MAP.equals(scenario.name)) return screen instanceof WorldMapScreen; + return false; + } + + private static void openCurrentStep(Minecraft mc, ActiveScenario scenario) { + if (scenario.definition.isSkillTree()) { + openSkillTreeStep(mc, scenario); + return; + } + if (mc.player == null) return; + if (VisualScenarioIds.MILITARY_COMMAND.equals(scenario.name)) { + mc.setScreen(new CommandScreen(new CommandMenu(0, mc.player), mc.player.getInventory(), Component.literal("command_screen"))); + return; + } + if (VisualScenarioIds.RECRUIT_INVENTORY.equals(scenario.name)) { + AbstractRecruitEntity recruit = recruitEntity(mc, scenario.recruitEntityId); + if (recruit == null) { + stop(); + return; + } + mc.setScreen(new RecruitInventoryScreen(new RecruitInventoryMenu(0, recruit, mc.player.getInventory()), mc.player.getInventory(), recruit.getName())); + return; + } + if (VisualScenarioIds.RECRUIT_GROUPS.equals(scenario.name)) { + mc.setScreen(new RecruitsGroupListScreen(mc.player)); + return; + } + if (VisualScenarioIds.RECRUIT_ACTION_FEEDBACK.equals(scenario.name)) { + AbstractRecruitEntity recruit = recruitEntity(mc, scenario.recruitEntityId); + if (recruit == null) { + stop(); + return; + } + mc.setScreen(new RecruitMoreScreen(null, recruit, mc.player)); + return; + } + if (VisualScenarioIds.WAR_ROOM.equals(scenario.name)) { + mc.setScreen(new WarListScreen(null)); + return; + } + if (VisualScenarioIds.POLITICAL_ENTITIES.equals(scenario.name)) { + mc.setScreen(new PoliticalEntityListScreen(null)); + return; + } + if (VisualScenarioIds.WAR_DECLARE.equals(scenario.name)) { + mc.setScreen(new WarDeclareScreen(new WarListScreen(null))); + return; + } + if (VisualScenarioIds.WORLD_MAP.equals(scenario.name)) { + if (mc.level != null && mc.level.dimension() == Level.OVERWORLD) { + BannerModMain.SIMPLE_CHANNEL.sendToServer(new MessageRequestFormationMapSnapshot()); + } + mc.setScreen(new WorldMapScreen()); + } + } + + private static void openSkillTreeStep(Minecraft mc, ActiveScenario scenario) { + PerkTreeScreen screen; + if (VisualScenarioIds.SKILLTREE_PLAYER.equals(scenario.name)) { + screen = PerkTreeScreen.visualScenarioPlayerTree(); + } else { + screen = PerkTreeScreen.visualScenarioRecruitTree(recruitEntity(mc, scenario.recruitEntityId)); + } + mc.setScreen(screen); + screen.applyVisualScenarioStep(scenario.definition.skillStep(scenario.stepIndex)); + } + + private static AbstractRecruitEntity recruitEntity(Minecraft mc, int entityId) { + if (mc.level == null || entityId < 0) return null; + Entity entity = mc.level.getEntity(entityId); + return entity instanceof AbstractRecruitEntity recruit ? recruit : null; + } + + private static void renderOverlay(GuiGraphics graphics, DeltaTracker deltaTracker) { + if (active == null) return; + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null || mc.options.hideGui || mc.getDebugOverlay().showDebugScreen()) return; + + Font font = mc.font; + int x = 8; + int y = SAFE_TOP; + int width = Math.min(PANEL_WIDTH, graphics.guiWidth() - 16); + MilitaryGuiStyle.parchmentPanel(graphics, x, y, width, PANEL_HEIGHT); + int textWidth = width - 16; + graphics.drawString(font, MilitaryGuiStyle.clampLabel(font, Component.translatable(active.definition.titleKey), textWidth), x + 8, y + 7, + MilitaryGuiStyle.TEXT_DARK, false); + graphics.drawString(font, MilitaryGuiStyle.clampLabel(font, Component.translatable(active.definition.stepTitleKey(active.stepIndex)), textWidth), x + 8, y + 19, + MilitaryGuiStyle.TEXT_WARN, false); + } + + private record ScenarioDefinition(String titleKey, List<String> stepTitleKeys, List<SkillTreeVisualScenarioStep> skillSteps) { + static ScenarioDefinition skillTree(String titleKey) { + return new ScenarioDefinition(titleKey, SKILLTREE_STEPS.stream().map(SkillTreeVisualScenarioStep::titleKey).toList(), SKILLTREE_STEPS); + } + + static ScenarioDefinition screen(String titleKey) { + return new ScenarioDefinition(titleKey, SCREEN_OBSERVE_STEPS, List.of()); + } + + static ScenarioDefinition screen(String titleKey, List<String> stepTitleKeys) { + return new ScenarioDefinition(titleKey, stepTitleKeys, List.of()); + } + + boolean isSkillTree() { + return !skillSteps.isEmpty(); + } + + int stepCount() { + return stepTitleKeys.size(); + } + + String stepTitleKey(int index) { + return stepTitleKeys.get(index); + } + + SkillTreeVisualScenarioStep skillStep(int index) { + return skillSteps.get(index); + } + } + + private static final class ActiveScenario { + private final String name; + private final int recruitEntityId; + private final ScenarioDefinition definition; + private int stepIndex; + private int ticksInStep; + + private ActiveScenario(String name, int recruitEntityId, ScenarioDefinition definition) { + this.name = name; + this.recruitEntityId = recruitEntityId; + this.definition = definition; + } + } +} diff --git a/src/main/java/com/talhanation/bannermod/commands/admin/VisualScenarioCommands.java b/src/main/java/com/talhanation/bannermod/commands/admin/VisualScenarioCommands.java new file mode 100644 index 00000000..f630c2ce --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/commands/admin/VisualScenarioCommands.java @@ -0,0 +1,115 @@ +package com.talhanation.bannermod.commands.admin; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import com.talhanation.bannermod.bootstrap.BannerModMain; +import com.talhanation.bannermod.entity.military.AbstractRecruitEntity; +import com.talhanation.bannermod.entity.military.RecruitIndex; +import com.talhanation.bannermod.network.compat.BannerModPacketDistributor; +import com.talhanation.bannermod.network.messages.military.MessageToClientRunVisualScenario; +import com.talhanation.bannermod.scenario.VisualScenarioIds; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; + +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public final class VisualScenarioCommands { + private static final double RECRUIT_SEARCH_RADIUS = 16.0D; + private static final SimpleCommandExceptionType UNKNOWN_SCENARIO = new SimpleCommandExceptionType( + Component.translatable("commands.bannermod.scenario.unknown")); + private static final SimpleCommandExceptionType RECRUIT_REQUIRED = new SimpleCommandExceptionType( + Component.translatable("commands.bannermod.scenario.recruit_required")); + + private VisualScenarioCommands() { + } + + public static LiteralArgumentBuilder<CommandSourceStack> build() { + return Commands.literal("scenario") + .requires(source -> source.hasPermission(2)) + .then(Commands.literal("list") + .executes(context -> list(context.getSource()))) + .then(Commands.literal("run") + .then(Commands.argument("id", StringArgumentType.word()) + .suggests(VisualScenarioCommands::suggestScenarios) + .executes(context -> startScenario(context.getSource(), StringArgumentType.getString(context, "id"))))) + .then(Commands.literal("skilltree") + .then(Commands.literal("player") + .executes(context -> startScenario(context.getSource(), VisualScenarioIds.SKILLTREE_PLAYER))) + .then(Commands.literal("recruit") + .executes(context -> startScenario(context.getSource(), VisualScenarioIds.SKILLTREE_RECRUIT)))) + .then(Commands.literal("military_command") + .executes(context -> startScenario(context.getSource(), VisualScenarioIds.MILITARY_COMMAND))) + .then(Commands.literal("recruit_inventory") + .executes(context -> startScenario(context.getSource(), VisualScenarioIds.RECRUIT_INVENTORY))) + .then(Commands.literal("recruit_groups") + .executes(context -> startScenario(context.getSource(), VisualScenarioIds.RECRUIT_GROUPS))) + .then(Commands.literal("recruit_action_feedback") + .executes(context -> startScenario(context.getSource(), VisualScenarioIds.RECRUIT_ACTION_FEEDBACK))) + .then(Commands.literal("war_room") + .executes(context -> startScenario(context.getSource(), VisualScenarioIds.WAR_ROOM))) + .then(Commands.literal("political_entities") + .executes(context -> startScenario(context.getSource(), VisualScenarioIds.POLITICAL_ENTITIES))) + .then(Commands.literal("war_declare") + .executes(context -> startScenario(context.getSource(), VisualScenarioIds.WAR_DECLARE))) + .then(Commands.literal("world_map") + .executes(context -> startScenario(context.getSource(), VisualScenarioIds.WORLD_MAP))) + .then(Commands.literal("stop") + .executes(context -> stop(context.getSource()))); + } + + private static int list(CommandSourceStack source) { + source.sendSuccess(() -> Component.translatable("commands.bannermod.scenario.list", String.join(", ", VisualScenarioIds.all())), false); + return VisualScenarioIds.all().size(); + } + + private static CompletableFuture<Suggestions> suggestScenarios(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) { + for (String id : VisualScenarioIds.all()) { + builder.suggest(id); + } + return builder.buildFuture(); + } + + private static int startScenario(CommandSourceStack source, String scenario) throws CommandSyntaxException { + if (!VisualScenarioIds.isKnown(scenario)) throw UNKNOWN_SCENARIO.create(); + ServerPlayer player = source.getPlayerOrException(); + int recruitEntityId = -1; + if (VisualScenarioIds.usesNearestRecruit(scenario)) { + AbstractRecruitEntity recruit = nearestRecruit(player); + if (recruit == null && VisualScenarioIds.requiresRecruit(scenario)) throw RECRUIT_REQUIRED.create(); + if (recruit != null) recruitEntityId = recruit.getId(); + } + send(player, scenario, recruitEntityId); + source.sendSuccess(() -> Component.translatable("commands.bannermod.scenario.started", + Component.translatable(VisualScenarioIds.labelKey(scenario))), false); + return 1; + } + + private static int stop(CommandSourceStack source) throws CommandSyntaxException { + ServerPlayer player = source.getPlayerOrException(); + send(player, VisualScenarioIds.STOP, -1); + source.sendSuccess(() -> Component.translatable("commands.bannermod.scenario.stopped"), false); + return 1; + } + + private static AbstractRecruitEntity nearestRecruit(ServerPlayer player) { + List<AbstractRecruitEntity> recruits = RecruitIndex.instance().allInRange(player.serverLevel(), player.position(), RECRUIT_SEARCH_RADIUS, true); + if (recruits == null || recruits.isEmpty()) return null; + return recruits.stream() + .min(Comparator.comparingDouble(recruit -> recruit.distanceToSqr(player))) + .orElse(null); + } + + private static void send(ServerPlayer player, String scenario, int recruitEntityId) { + BannerModMain.SIMPLE_CHANNEL.send(BannerModPacketDistributor.PLAYER.with(() -> player), + new MessageToClientRunVisualScenario(scenario, recruitEntityId)); + } +} diff --git a/src/main/java/com/talhanation/bannermod/commands/war/BannerModWarCommands.java b/src/main/java/com/talhanation/bannermod/commands/war/BannerModWarCommands.java index b34f8788..b85e27b0 100644 --- a/src/main/java/com/talhanation/bannermod/commands/war/BannerModWarCommands.java +++ b/src/main/java/com/talhanation/bannermod/commands/war/BannerModWarCommands.java @@ -4,6 +4,7 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.talhanation.bannermod.commands.admin.AdminDebugCommands; import com.talhanation.bannermod.commands.admin.AdminRecoveryCommands; +import com.talhanation.bannermod.commands.admin.VisualScenarioCommands; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; @@ -22,6 +23,7 @@ private static LiteralArgumentBuilder<CommandSourceStack> root() { .then(AdminRecoveryCommands.treasury()) .then(AdminRecoveryCommands.claim()) .then(AdminRecoveryCommands.worker()) + .then(VisualScenarioCommands.build()) .then(PoliticalRegistryCommands.build()) .then(WarDeclarationCommands.build() .then(SiegeStandardCommands.build()) diff --git a/src/main/java/com/talhanation/bannermod/network/catalog/MilitaryPacketCatalog.java b/src/main/java/com/talhanation/bannermod/network/catalog/MilitaryPacketCatalog.java index 5e308b29..a9b282f2 100644 --- a/src/main/java/com/talhanation/bannermod/network/catalog/MilitaryPacketCatalog.java +++ b/src/main/java/com/talhanation/bannermod/network/catalog/MilitaryPacketCatalog.java @@ -108,6 +108,7 @@ public final class MilitaryPacketCatalog { MessageRequestPerkTreeSnapshot.class, MessageUpdatePerkTree.class, MessageToClientUpdatePerkTreeSnapshot.class, + MessageToClientRunVisualScenario.class, }; public static final PacketCatalog CATALOG = new PacketCatalog(MESSAGES); diff --git a/src/main/java/com/talhanation/bannermod/network/messages/military/MessageToClientRunVisualScenario.java b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageToClientRunVisualScenario.java new file mode 100644 index 00000000..2b011a4c --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/network/messages/military/MessageToClientRunVisualScenario.java @@ -0,0 +1,46 @@ +package com.talhanation.bannermod.network.messages.military; + +import com.talhanation.bannermod.client.military.scenario.VisualScenarioClient; +import com.talhanation.bannermod.network.compat.BannerModNetworkContext; +import com.talhanation.bannermod.network.payload.BannerModMessage; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.PacketFlow; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; + +public class MessageToClientRunVisualScenario implements BannerModMessage<MessageToClientRunVisualScenario> { + private String scenario = ""; + private int recruitEntityId = -1; + + public MessageToClientRunVisualScenario() { + } + + public MessageToClientRunVisualScenario(String scenario, int recruitEntityId) { + this.scenario = scenario; + this.recruitEntityId = recruitEntityId; + } + + @Override + public PacketFlow getExecutingSide() { + return BannerModMessage.clientbound(); + } + + @Override + @OnlyIn(Dist.CLIENT) + public void executeClientSide(BannerModNetworkContext context) { + context.enqueueWork(() -> VisualScenarioClient.start(this.scenario, this.recruitEntityId)); + } + + @Override + public MessageToClientRunVisualScenario fromBytes(FriendlyByteBuf buf) { + this.scenario = buf.readUtf(48); + this.recruitEntityId = buf.readInt(); + return this; + } + + @Override + public void toBytes(FriendlyByteBuf buf) { + buf.writeUtf(this.scenario == null ? "" : this.scenario); + buf.writeInt(this.recruitEntityId); + } +} diff --git a/src/main/java/com/talhanation/bannermod/scenario/VisualScenarioIds.java b/src/main/java/com/talhanation/bannermod/scenario/VisualScenarioIds.java new file mode 100644 index 00000000..2e5e0493 --- /dev/null +++ b/src/main/java/com/talhanation/bannermod/scenario/VisualScenarioIds.java @@ -0,0 +1,60 @@ +package com.talhanation.bannermod.scenario; + +import java.util.List; + +public final class VisualScenarioIds { + public static final String SKILLTREE_PLAYER = "skilltree_player"; + public static final String SKILLTREE_RECRUIT = "skilltree_recruit"; + public static final String MILITARY_COMMAND = "military_command"; + public static final String RECRUIT_INVENTORY = "recruit_inventory"; + public static final String RECRUIT_GROUPS = "recruit_groups"; + public static final String RECRUIT_ACTION_FEEDBACK = "recruit_action_feedback"; + public static final String WAR_ROOM = "war_room"; + public static final String POLITICAL_ENTITIES = "political_entities"; + public static final String WAR_DECLARE = "war_declare"; + public static final String WORLD_MAP = "world_map"; + public static final String STOP = "stop"; + + private static final List<String> ALL = List.of( + SKILLTREE_PLAYER, + SKILLTREE_RECRUIT, + MILITARY_COMMAND, + RECRUIT_INVENTORY, + RECRUIT_GROUPS, + RECRUIT_ACTION_FEEDBACK, + WAR_ROOM, + POLITICAL_ENTITIES, + WAR_DECLARE, + WORLD_MAP + ); + + private VisualScenarioIds() { + } + + public static List<String> all() { + return ALL; + } + + public static boolean isKnown(String scenario) { + return ALL.contains(scenario); + } + + public static boolean usesNearestRecruit(String scenario) { + return SKILLTREE_RECRUIT.equals(scenario) + || RECRUIT_INVENTORY.equals(scenario) + || RECRUIT_ACTION_FEEDBACK.equals(scenario); + } + + public static boolean requiresRecruit(String scenario) { + return RECRUIT_INVENTORY.equals(scenario) + || RECRUIT_ACTION_FEEDBACK.equals(scenario); + } + + public static String titleKey(String scenario) { + return "gui.bannermod.visual_scenario." + scenario + ".title"; + } + + public static String labelKey(String scenario) { + return "gui.bannermod.visual_scenario." + scenario + ".label"; + } +} diff --git a/src/main/resources/assets/bannermod/lang/en_us.json b/src/main/resources/assets/bannermod/lang/en_us.json index e0e3f728..6c144cc2 100644 --- a/src/main/resources/assets/bannermod/lang/en_us.json +++ b/src/main/resources/assets/bannermod/lang/en_us.json @@ -2643,5 +2643,59 @@ "gui.bannermod.perk_tree.feedback.denied_owned": "Server denied: already owned.", "gui.bannermod.perk_tree.feedback.denied_points": "Server denied: not enough points.", "gui.bannermod.perk_tree.feedback.denied_prereq": "Server denied: prerequisites missing.", - "gui.bannermod.perk_tree.feedback.denied_unknown": "Server denied: unknown perk." + "gui.bannermod.perk_tree.feedback.denied_unknown": "Server denied: unknown perk.", + "commands.bannermod.scenario.unknown": "Unknown visual scenario.", + "commands.bannermod.scenario.recruit_required": "This visual scenario needs a live recruit within 16 blocks.", + "commands.bannermod.scenario.list": "Available visual scenarios: %s", + "commands.bannermod.scenario.started": "Started visual scenario: %s.", + "commands.bannermod.scenario.stopped": "Stopped visual scenario.", + "gui.bannermod.visual_scenario.skilltree_player.label": "Player skill tree", + "gui.bannermod.visual_scenario.skilltree_recruit.label": "Recruit perk tree", + "gui.bannermod.visual_scenario.military_command.label": "Military command screen", + "gui.bannermod.visual_scenario.recruit_inventory.label": "Recruit inventory and perk entry", + "gui.bannermod.visual_scenario.recruit_groups.label": "Recruit group management", + "gui.bannermod.visual_scenario.recruit_action_feedback.label": "Recruit action feedback", + "gui.bannermod.visual_scenario.war_room.label": "War Room", + "gui.bannermod.visual_scenario.political_entities.label": "Political entity ledger", + "gui.bannermod.visual_scenario.war_declare.label": "War declaration form", + "gui.bannermod.visual_scenario.world_map.label": "World Map", + "gui.bannermod.visual_scenario.skilltree_player.title": "Visual scenario: player skill tree", + "gui.bannermod.visual_scenario.skilltree_recruit.title": "Visual scenario: recruit perk tree", + "gui.bannermod.visual_scenario.military_command.title": "Visual scenario: military command screen", + "gui.bannermod.visual_scenario.recruit_inventory.title": "Visual scenario: recruit inventory and perk entry", + "gui.bannermod.visual_scenario.recruit_groups.title": "Visual scenario: recruit group management", + "gui.bannermod.visual_scenario.recruit_action_feedback.title": "Visual scenario: recruit action feedback", + "gui.bannermod.visual_scenario.war_room.title": "Visual scenario: War Room", + "gui.bannermod.visual_scenario.political_entities.title": "Visual scenario: political entity ledger", + "gui.bannermod.visual_scenario.war_declare.title": "Visual scenario: war declaration form", + "gui.bannermod.visual_scenario.world_map.title": "Visual scenario: World Map", + "gui.bannermod.visual_scenario.skilltree.feedback.locked": "Locked preview; no server request was sent.", + "gui.bannermod.visual_scenario.skilltree.step.locked": "Locked state: no points, unlock buttons disabled.", + "gui.bannermod.visual_scenario.skilltree.step.available": "Available state: points present, unlock buttons enabled.", + "gui.bannermod.visual_scenario.skilltree.step.owned": "Owned state: one perk is already learned.", + "gui.bannermod.visual_scenario.skilltree.step.pending": "Pending feedback: request-sent message is visible.", + "gui.bannermod.visual_scenario.skilltree.step.denied": "Denied feedback: insufficient-points message is visible.", + "gui.bannermod.visual_scenario.skilltree.step.respec": "Respec confirmation: decision text stays visible.", + "gui.bannermod.visual_scenario.screen.step.bounds": "Bounds: check panel stays inside the scaled viewport.", + "gui.bannermod.visual_scenario.screen.step.actions": "Actions: check buttons and disabled reasons stay readable.", + "gui.bannermod.visual_scenario.screen.step.feedback": "Feedback: check status text does not hide decisions.", + "gui.bannermod.visual_scenario.screen.step.overlay_stack": "Overlay stack: check hotbar, chat, crosshair, and boss bars.", + "gui.bannermod.visual_scenario.military_command.step.groups": "Groups: selected/all group counts and disabled targets are visible.", + "gui.bannermod.visual_scenario.military_command.step.categories": "Categories: movement/combat/other tabs keep readable hover prompts.", + "gui.bannermod.visual_scenario.military_command.step.targets": "Targets: block/entity requirements stay visible near the buttons.", + "gui.bannermod.visual_scenario.military_command.step.read_only": "Read-only: clicks are blocked; no command packet is sent.", + "gui.bannermod.visual_scenario.recruit_inventory.step.status": "Status: health, hunger, morale, order, and firearm lines are readable.", + "gui.bannermod.visual_scenario.recruit_inventory.step.actions": "Actions: aggro/order/mount controls show enabled and disabled reasons.", + "gui.bannermod.visual_scenario.recruit_inventory.step.perk_entry": "Perk entry: the recruit perk-tree button stays visible without covering slots.", + "gui.bannermod.visual_scenario.recruit_inventory.step.read_only": "Read-only: inventory clicks are blocked; leave the scenario to test real actions.", + "gui.bannermod.visual_scenario.recruit_groups.step.list": "Groups: list, search box, and footer fit inside the scaled viewport.", + "gui.bannermod.visual_scenario.recruit_groups.step.disabled": "Disabled reasons: add/edit/remove tooltips explain the current selection state.", + "gui.bannermod.visual_scenario.recruit_groups.step.selection": "Selection: status text explains whether a group is selected.", + "gui.bannermod.visual_scenario.recruit_groups.step.read_only": "Read-only: add, edit, and remove clicks are blocked.", + "gui.bannermod.visual_scenario.recruit_action_feedback.step.panel": "Action panel: rename, disband, assign, and group settings stay compact.", + "gui.bannermod.visual_scenario.recruit_action_feedback.step.decisions": "Decision context: destructive actions must keep confirmation text visible.", + "gui.bannermod.visual_scenario.recruit_action_feedback.step.denials": "Denials: disabled or unsafe actions need hoverable reasons.", + "gui.bannermod.visual_scenario.recruit_action_feedback.step.read_only": "Read-only: clicks are blocked; no recruit action packet is sent.", + "gui.bannermod.visual_scenario.loop_hint": "The scenario cycles automatically; just watch for overlap.", + "gui.bannermod.visual_scenario.stop_hint": "Stop: /bannermod scenario stop" } diff --git a/src/main/resources/assets/bannermod/lang/ru_ru.json b/src/main/resources/assets/bannermod/lang/ru_ru.json index fd5620d8..5fc31e30 100644 --- a/src/main/resources/assets/bannermod/lang/ru_ru.json +++ b/src/main/resources/assets/bannermod/lang/ru_ru.json @@ -2553,5 +2553,59 @@ "gui.bannermod.perk_tree.feedback.denied_owned": "Сервер отклонил: уже изучено.", "gui.bannermod.perk_tree.feedback.denied_points": "Сервер отклонил: не хватает очков.", "gui.bannermod.perk_tree.feedback.denied_prereq": "Сервер отклонил: нет требований.", - "gui.bannermod.perk_tree.feedback.denied_unknown": "Сервер отклонил: неизвестный перк." + "gui.bannermod.perk_tree.feedback.denied_unknown": "Сервер отклонил: неизвестный перк.", + "commands.bannermod.scenario.unknown": "Неизвестный визуальный сценарий.", + "commands.bannermod.scenario.recruit_required": "Для этого визуального сценария нужен живой рекрут в радиусе 16 блоков.", + "commands.bannermod.scenario.list": "Доступные визуальные сценарии: %s", + "commands.bannermod.scenario.started": "Запущен визуальный сценарий: %s.", + "commands.bannermod.scenario.stopped": "Визуальный сценарий остановлен.", + "gui.bannermod.visual_scenario.skilltree_player.label": "Дерево навыков игрока", + "gui.bannermod.visual_scenario.skilltree_recruit.label": "Дерево перков рекрута", + "gui.bannermod.visual_scenario.military_command.label": "Экран командования", + "gui.bannermod.visual_scenario.recruit_inventory.label": "Инвентарь рекрута и вход в перки", + "gui.bannermod.visual_scenario.recruit_groups.label": "Управление группами рекрутов", + "gui.bannermod.visual_scenario.recruit_action_feedback.label": "Отклик действий рекрута", + "gui.bannermod.visual_scenario.war_room.label": "Военная комната", + "gui.bannermod.visual_scenario.political_entities.label": "Реестр держав", + "gui.bannermod.visual_scenario.war_declare.label": "Объявление войны", + "gui.bannermod.visual_scenario.world_map.label": "Карта мира", + "gui.bannermod.visual_scenario.skilltree_player.title": "Визуальный сценарий: навыки игрока", + "gui.bannermod.visual_scenario.skilltree_recruit.title": "Визуальный сценарий: перки рекрута", + "gui.bannermod.visual_scenario.military_command.title": "Визуальный сценарий: экран командования", + "gui.bannermod.visual_scenario.recruit_inventory.title": "Визуальный сценарий: инвентарь рекрута и вход в перки", + "gui.bannermod.visual_scenario.recruit_groups.title": "Визуальный сценарий: управление группами рекрутов", + "gui.bannermod.visual_scenario.recruit_action_feedback.title": "Визуальный сценарий: отклик действий рекрута", + "gui.bannermod.visual_scenario.war_room.title": "Визуальный сценарий: военная комната", + "gui.bannermod.visual_scenario.political_entities.title": "Визуальный сценарий: реестр держав", + "gui.bannermod.visual_scenario.war_declare.title": "Визуальный сценарий: объявление войны", + "gui.bannermod.visual_scenario.world_map.title": "Визуальный сценарий: карта мира", + "gui.bannermod.visual_scenario.skilltree.feedback.locked": "Просмотр закрытого состояния; запрос на сервер не отправлялся.", + "gui.bannermod.visual_scenario.skilltree.step.locked": "Закрыто: очков нет, кнопки изучения выключены.", + "gui.bannermod.visual_scenario.skilltree.step.available": "Доступно: очки есть, кнопки изучения включены.", + "gui.bannermod.visual_scenario.skilltree.step.owned": "Изучено: один перк уже взят.", + "gui.bannermod.visual_scenario.skilltree.step.pending": "Ожидание: сообщение об отправке запроса видно.", + "gui.bannermod.visual_scenario.skilltree.step.denied": "Отказ: видно сообщение о нехватке очков.", + "gui.bannermod.visual_scenario.skilltree.step.respec": "Подтверждение сброса: текст решения остаётся видимым.", + "gui.bannermod.visual_scenario.screen.step.bounds": "Границы: панель должна помещаться в масштабированный экран.", + "gui.bannermod.visual_scenario.screen.step.actions": "Действия: кнопки и причины блокировки должны читаться.", + "gui.bannermod.visual_scenario.screen.step.feedback": "Отклик: статус не должен закрывать важное решение.", + "gui.bannermod.visual_scenario.screen.step.overlay_stack": "Слои: проверь хотбар, чат, прицел и полосы боссов.", + "gui.bannermod.visual_scenario.military_command.step.groups": "Группы: число выбранных и всех групп, а также недоступные цели видны.", + "gui.bannermod.visual_scenario.military_command.step.categories": "Категории: движение, бой и прочее сохраняют читаемые подсказки.", + "gui.bannermod.visual_scenario.military_command.step.targets": "Цели: требования к блоку или сущности остаются рядом с кнопками.", + "gui.bannermod.visual_scenario.military_command.step.read_only": "Только просмотр: клики заблокированы, пакет команды не отправляется.", + "gui.bannermod.visual_scenario.recruit_inventory.step.status": "Статус: здоровье, голод, мораль, приказ и оружие читаются.", + "gui.bannermod.visual_scenario.recruit_inventory.step.actions": "Действия: агрессия, приказы и конь показывают причины доступности.", + "gui.bannermod.visual_scenario.recruit_inventory.step.perk_entry": "Вход в перки: кнопка дерева перков видна и не закрывает слоты.", + "gui.bannermod.visual_scenario.recruit_inventory.step.read_only": "Только просмотр: клики по инвентарю заблокированы; выйди из сценария для реальных действий.", + "gui.bannermod.visual_scenario.recruit_groups.step.list": "Группы: список, поиск и нижняя панель помещаются в экран.", + "gui.bannermod.visual_scenario.recruit_groups.step.disabled": "Причины блокировки: подсказки добавления, правки и удаления объясняют выбор.", + "gui.bannermod.visual_scenario.recruit_groups.step.selection": "Выбор: строка статуса объясняет, выбрана ли группа.", + "gui.bannermod.visual_scenario.recruit_groups.step.read_only": "Только просмотр: клики добавления, правки и удаления заблокированы.", + "gui.bannermod.visual_scenario.recruit_action_feedback.step.panel": "Панель действий: имя, роспуск, передача и группы остаются компактными.", + "gui.bannermod.visual_scenario.recruit_action_feedback.step.decisions": "Контекст решения: опасные действия должны сохранять текст подтверждения.", + "gui.bannermod.visual_scenario.recruit_action_feedback.step.denials": "Отказы: недоступные или опасные действия требуют подсказки причины.", + "gui.bannermod.visual_scenario.recruit_action_feedback.step.read_only": "Только просмотр: клики заблокированы, пакет действия рекрута не отправляется.", + "gui.bannermod.visual_scenario.loop_hint": "Сценарий переключается сам; просто смотри на перекрытия.", + "gui.bannermod.visual_scenario.stop_hint": "Остановить: /bannermod scenario stop" }