# NEO-29 — Implementation plan ## Story reference | Field | Value | |--------|--------| | **Key** | NEO-29 | | **Title** | E1M4-01: HotbarLoadout v1 contract + baseline persistence path | | **Linear** | [NEO-29](https://linear.app/neon-sprawl/issue/NEO-29/e1m4-01-hotbarloadout-v1-contract-baseline-persistence-path) | | **Slug** | E1M4-01 | | **Git branch** | `NEO-29-hotbarloadout-v1-contract-baseline-persistence-path` | | **Parent context** | [Epic 1 — Core Player Runtime](https://linear.app/neon-sprawl/project/epic-1-core-player-runtime-client-controls-character-loop-66bd590cd016) · [E1M4 prototype backlog](E1M4-prototype-backlog.md) | | **Decomposition** | [E1.M4 — AbilityInputScaffold](../decomposition/modules/E1_M4_AbilityInputScaffold.md), [E1.M3 — InteractionAndTargetingLayer](../decomposition/modules/E1_M3_InteractionAndTargetingLayer.md), [E5.M1 — CombatRulesEngine](../decomposition/modules/E5_M1_CombatRulesEngine.md) | ## Kickoff clarifications - **Q1 (blocking):** What should prototype `HotbarLoadout` scope be keyed to (player, character, or session)? - **Answer:** **Per player id** for the prototype. - **Q2 (blocking):** What baseline persistence backend should NEO-29 use? - **Answer:** Mirror position persistence pattern: use **Postgres when `ConnectionStrings:NeonSprawl` exists**, otherwise **in-memory fallback**. - **Q3 (blocking):** What fixed slot count should v1 expose? - **Answer:** **8 slots**. ## Goal, scope, and out-of-scope **Goal:** Define and ship a prototype `HotbarLoadout` v1 read/write contract with server-owned validation and persistence, then hydrate client hotbar state from that server contract on boot/reconnect. **In scope** - New server API surface for loadout fetch + update with explicit `slotIndex` and `abilityId`. - Validation denies for out-of-bounds slots and unknown ability ids with stable machine-readable `reasonCode`. - Prototype persistence policy documentation and implementation: per-player loadout, Postgres when configured, in-memory fallback otherwise. - Client bootstrap path to fetch and apply hotbar loadout state. **Out of scope** - Final account-vs-character policy hardening beyond this prototype decision. - Cast execution, cooldown timing, and full combat authorization flow (follow-up stories in E1.M4 / E5.M1). ## Acceptance criteria checklist - [x] A player can bind at least two slots and retrieve the same bindings after reconnect/reload. - [x] Invalid slot/ability input is rejected with a stable machine-readable reason code. - [x] Module docs and this plan describe the chosen prototype persistence policy. ## Technical approach 1. Add a dedicated `HotbarLoadout` v1 API to the server with `GET` (current loadout) and `POST` (replace/update slots) routes under `/game/players/{id}/hotbar-loadout`. 2. Keep the response shape versioned (`schemaVersion`) and explicit per-slot entries (`slotIndex`, `abilityId`) so E1.M4 follow-up stories can extend without breaking the v1 envelope. 3. Validate slot index range against fixed v1 slot count (**8**) and validate `abilityId` against a prototype server-side registry/list; return deterministic `reasonCode` values on deny (for example `slot_out_of_bounds`, `unknown_ability`). 4. Mirror `PositionState` persistence wiring: add an abstraction with in-memory and Postgres implementations selected from configuration, so reconnect/reload behavior is stable in both local-no-db and db-backed runs. 5. Hydrate the client hotbar model on startup/reconnect by requesting the loadout from server, then applying slot->ability bindings to local state without introducing cast execution behavior in this story. 6. Update decomposition/module docs and server/client README notes so the prototype persistence choice (per-player + Postgres-or-memory) is explicit and discoverable. ## Files to add | Path | Rationale | |------|-----------| | `server/NeonSprawl.Server/Game/AbilityInput/HotbarLoadoutDtos.cs` | New v1 DTO contracts for loadout read/write API payloads. | | `server/NeonSprawl.Server/Game/AbilityInput/HotbarLoadoutApi.cs` | New route mapping and validation/deny response logic for loadout endpoints. | | `server/NeonSprawl.Server/Game/AbilityInput/IPlayerHotbarLoadoutStore.cs` | Persistence abstraction to mirror existing position store pattern. | | `server/NeonSprawl.Server/Game/AbilityInput/InMemoryPlayerHotbarLoadoutStore.cs` | In-memory fallback implementation for prototype/dev without DB. | | `server/NeonSprawl.Server/Game/AbilityInput/PostgresPlayerHotbarLoadoutStore.cs` | Postgres-backed persistence path when connection string is configured. | | `server/NeonSprawl.Server/Game/AbilityInput/PostgresHotbarLoadoutBootstrap.cs` | One-time migration bootstrap for hotbar table DDL. | | `server/NeonSprawl.Server/Game/AbilityInput/HotbarLoadoutServiceCollectionExtensions.cs` | DI registration and backend selection wiring. | | `server/NeonSprawl.Server/Game/AbilityInput/PrototypeAbilityRegistry.cs` | Canonical prototype ability-id allowlist for validation. | | `server/db/migrations/V002__player_hotbar_loadout.sql` | Postgres schema for per-player slot bindings persistence. | | `server/NeonSprawl.Server.Tests/Game/AbilityInput/HotbarLoadoutApiTests.cs` | Server contract + validation tests for loadout endpoints. | | `server/NeonSprawl.Server.Tests/Game/AbilityInput/HotbarLoadoutPersistenceIntegrationTests.cs` | Postgres persistence survives process restart for bound slots. | | `client/scripts/hotbar_loadout_client.gd` | Client HTTP wrapper for fetch/update loadout API. | | `client/scripts/hotbar_state.gd` | Client-side hotbar state holder that applies hydrated slot bindings. | | `client/test/hotbar_loadout_client_test.gd` | Client tests for hydration/apply and update request behavior. | | `docs/manual-qa/NEO-29.md` | Story-specific manual checklist for bind/reconnect/deny validation. | ## Files to modify | Path | Rationale | |------|-----------| | `server/NeonSprawl.Server/Program.cs` | Register hotbar loadout store/services and map new API routes. | | `server/NeonSprawl.Server.Tests/InMemoryWebApplicationFactory.cs` | Keep in-memory test harness forcing both position and hotbar stores in CI/local. | | `server/README.md` | Document new loadout endpoints and prototype persistence behavior. | | `client/scripts/main.gd` | Wire initial hotbar hydration call on boot/reconnect path. | | `client/README.md` | Document hotbar loadout hydration/update flow and debug expectations. | | `docs/decomposition/modules/E1_M4_AbilityInputScaffold.md` | Record NEO-29 implementation snapshot + persistence policy decision. | | `docs/plans/E1M4-prototype-backlog.md` | Mark NEO-29 implementation notes/decision alignment if scope wording changes. | | `docs/plans/NEO-29-implementation-plan.md` | Keep decisions and file/test plan aligned as implementation choices evolve. | ## Tests | File | Coverage | |------|----------| | `server/NeonSprawl.Server.Tests/Game/AbilityInput/HotbarLoadoutApiTests.cs` | `GET`/`POST` contract shape, slot bounds validation, unknown ability validation, stable deny `reasonCode`, and successful bind persistence round-trip. | | `server/NeonSprawl.Server.Tests/Game/AbilityInput/HotbarLoadoutPersistenceIntegrationTests.cs` | Postgres-enabled persistence survives host restart/reconnect; in-memory fallback behavior remains consistent when DB is absent. | | `client/test/hotbar_loadout_client_test.gd` | Client hydration applies server slot bindings, preserves empty slots, and surfaces server deny reason codes from failed updates. | | `docs/manual-qa/NEO-29.md` | Manual checklist for two-slot bind + reconnect/reload verification and deny-path UX/log visibility. | **Executed during implementation** - `dotnet test NeonSprawl.sln --filter "FullyQualifiedName~HotbarLoadoutApiTests"` (passed) - `dotnet test NeonSprawl.sln --filter "FullyQualifiedName~HotbarLoadoutPersistenceIntegrationTests"` (passed) - `godot --headless --path . -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd --ignoreHeadlessMode -a res://test/hotbar_loadout_client_test.gd` (passed) ## Open questions / risks - **Risk:** Ability-id allowlist source may drift before full E5.M1 cast contract lands; keep a single prototype registry and document ownership to avoid client/server mismatch. - **Risk:** First cut may choose full-replace update semantics for simplicity; if partial patch semantics are needed for E1M4-02+, call that out as a follow-up to avoid contract churn. ## Decisions (implementation updates) - **Update semantics:** NEO-29 uses **slot upsert semantics** (specified slots are updated; unspecified slots remain unchanged) to keep v1 payload compact while preserving server ownership. - **Deny shape:** Validation denials return HTTP 200 with `updated=false`, stable `reasonCode`, and authoritative `loadout` snapshot so clients can reconcile immediately. ## Decisions | Topic | Decision | |-------|----------| | Persistence scope | Per-player loadout for prototype v1. | | Persistence backend | Postgres when `ConnectionStrings:NeonSprawl` is configured; otherwise in-memory fallback. | | Slot count (v1) | Fixed 8-slot hotbar contract. |