neon-sprawl/docs/plans/NS-15-implementation-plan.md

127 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# NS-15 — Implementation plan
## Story reference
| Field | Value |
| ------------------ | --------------------------------------------------------------------------------------- |
| **Key** | NS-15 |
| **Title** | E1.M1: Authoritative PositionState API (in-memory) |
| **Jira** | [NS-15](https://neon-sprawl.atlassian.net/browse/NS-15) |
| **Parent context** | [NS-10 — E1.M1 InputAndMovementRuntime](https://neon-sprawl.atlassian.net/browse/NS-10) |
## Goal, scope, and out-of-scope
**Goal:** The game server is the source of truth for a single development **PositionState**, exposed read-only over HTTP as JSON (spike alignment with [tech stack](../../docs/architecture/tech_stack.md)).
**In scope**
- ASP.NET Core endpoint: `GET /game/players/{id}/position` returning JSON for the agreed **PositionState** shape (`x`, `y`, `z`, plus optional **facing** and/or **sequence** as needed for future sync).
- Seed one logical player id for local development (configuration entry or documented constant; prefer **config** so environments stay obvious).
- **In-memory** store only (dictionary or small service holding positions); no PostgreSQL.
**Out of scope (per Jira)**
- Godot or other client integration, move submission, database persistence.
## Acceptance criteria checklist
- [ ] `GET` returns stable JSON matching the agreed **PositionState** shape; field names documented in PR description and/or XML doc comments on the response/DTO types.
- [ ] Verification via **`dotnet test`** and/or a **curl** snippet in `server/README.md` (story allows either; plan: **both** — minimal integration test + README curl for ad-hoc checks).
- [ ] Payload is **versionable**: top-level **`schemaVersion`** (integer) or an explicit wrapper DTO so v2 can extend without breaking consumers that check version first.
## Technical approach
1. **Response contract**
- Define a small set of types (e.g. record/DTO) for the HTTP JSON body.
- **Suggested v1 shape** (adjust names in implementation if parent epic standardizes differently):
- `schemaVersion`: `1`
- `playerId`: string (echo path id for sanity)
- `position`: object with `x`, `y`, `z` as doubles (or floats serialized as JSON numbers)
- Optional: `facing` (e.g. yaw radians or degrees — pick one and document), `sequence` (ulong/int for future tick ordering)
- XML doc comments on public API types document each property; PR description repeats the sample JSON.
2. **In-memory authority**
- Implement `IPositionStateStore` (and concrete in-memory type) backed by `ConcurrentDictionary<string, …>` or immutable snapshots.
- On startup, ensure the **dev player id** from config exists with a default position (e.g. origin); other ids may return **404** until a later story seeds them.
- **Do not** register the store in `Program.cs`. Use an `IServiceCollection` extension (e.g. `PositionStateServiceCollectionExtensions.AddPositionStateStore`) that binds options and registers the singleton.
3. **HTTP API (dedicated class)**
- **Do not** define routes inline in `Program.cs`. Add a **separate class** (e.g. `PositionStateApi` or `PositionStateEndpoints`) whose responsibility is mapping this features routes—typically a static class with an extension method on `IEndpointRouteBuilder` or `WebApplication`, e.g. `MapPositionStateApi()`, containing the `MapGet("/game/players/{id}/position", …)` and handler logic (or thin delegates to a small handler type). Handlers resolve `IPositionStateStore` from DI, return `Results.Json(...)` or `Results.NotFound()`.
4. **Configuration**
- Add section in `appsettings.json` / `appsettings.Development.json` (e.g. `Game:DevPlayerId`, optional `Game:DefaultPosition` or nested object). Bind with `IOptions` or read once at startup when seeding the store.
5. **Tests**
- Add **`NeonSprawl.Server.Tests`** (xUnit), reference **`Microsoft.AspNetCore.Mvc.Testing`**, and add **`public partial class Program { }`** to the server assembly (empty partial in a **small** file—e.g. `Program.Waf.cs` or the tail of `Program.cs`—only for the test host, not for endpoint definitions) so **`WebApplicationFactory<Program>`** works with top-level statements.
- One integration test: `GET` for the configured dev player returns **200**, body parses, **`schemaVersion`** present, **`position.x`**, **`position.y`**, **`position.z`** present.
- Optional second test: unknown id returns **404**.
6. **Documentation**
- Extend `server/README.md` with “Position state (NS-15)”: endpoint path, example `curl`, note that state is in-memory and authoritative only at the HTTP spike layer.
## Files to add
| Path | Purpose |
|------|--------|
| `server/NeonSprawl.Server.Tests/NeonSprawl.Server.Tests.csproj` | xUnit + `Microsoft.AspNetCore.Mvc.Testing` test project. |
| `server/NeonSprawl.Server.Tests/Game/PositionState/PositionStateApiTests.cs` | Integration test(s) for `GET /game/players/{id}/position`. |
| `server/NeonSprawl.Server/...` DTOs / contracts | e.g. `PositionStateResponse.cs` (or folder `Game/PositionState/`) — versioned JSON shape. |
| `server/NeonSprawl.Server/...` `IPositionStateStore` + in-memory implementation | Authority + seeding from options. |
| `server/NeonSprawl.Server/...PositionStateServiceCollectionExtensions.cs` (or `Game/DependencyInjection/…`) | `AddPositionStateStore(IServiceCollection, IConfiguration)` (or options binding); **all** registration for this feature, not `Program.cs`. |
| `server/NeonSprawl.Server/...PositionStateApi.cs` (or `PositionStateEndpoints.cs`) | **Own class** mapping `GET /game/players/{id}/position` (extension on `WebApplication` / `IEndpointRouteBuilder`); no route definitions in `Program.cs`. |
| Optional: `server/NeonSprawl.Server/Program.Waf.cs` | Empty `public partial class Program { }` for `WebApplicationFactory` only. |
## Files to modify
| Path | Rationale |
|------|-----------|
| `server/NeonSprawl.Server/Program.cs` | Host bootstrap only: `WebApplication.CreateBuilder`, call service + endpoint extension methods (one line each or similarly minimal), `app.Run()`. **No** store registration body and **no** `MapGet` for position state here. |
| `server/NeonSprawl.Server/appsettings.json` and/or `appsettings.Development.json` | Dev player id and default position seed. |
| `server/README.md` | curl example + short contract note. |
| `NeonSprawl.sln` | Include test project in solution. |
## Tests
- **Automated:** Integration test(s) via `WebApplicationFactory<Program>` hitting `GET /game/players/{id}/position` — satisfies AC and [testing expectations](../../.cursor/rules/testing-expectations.md) (first testable server module gets a test project).
- **Manual:** `curl` snippet in README against `dotnet run` (default port from `launchSettings.json`, typically `http://localhost:5253`).
## Pull request description (Jira AC)
Jira NS-15 asks for stable JSON **field names** documented for reviewers outside the repo. **When you open the GitHub PR** (and optionally comment on [NS-15](https://neon-sprawl.atlassian.net/browse/NS-15)), paste the following so AC is satisfied without hunting the README.
**Endpoint:** `GET /game/players/{id}/position`**200** when the player exists, **404** when unknown.
**Example response body (v1, formatted for readability):**
```json
{
"schemaVersion": 1,
"playerId": "dev-local-1",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"sequence": 0
}
```
| JSON property | Type (v1) | Meaning |
|---------------|-----------|---------|
| `schemaVersion` | number (integer) | Contract version; increment when the payload shape or semantics change. |
| `playerId` | string | Echo of the `{id}` path segment from the request (casing preserved). |
| `position` | object | Authoritative world position. |
| `position.x` | number | X coordinate. |
| `position.y` | number | Y coordinate. |
| `position.z` | number | Z coordinate. |
| `sequence` | number (integer) | Increments on each successful move apply ([NS-16](NS-16-implementation-plan.md)); `0` until first move for the seeded dev player. |
**v1 omissions:** `facing` is not included (see Open questions). JSON uses **camelCase** property names (ASP.NET Core default serialization).
## Open questions / risks
- **Facing convention:** If `facing` is included, confirm units (radians vs degrees) with NS-10 / future sync design; if unclear, omit v1 `facing` and document “reserved for v2” or add `schemaVersion` bump when added.
- **Player id format:** Use a simple string (e.g. `dev-local-1`); path segment must be URL-safe.
- **CI:** If no workflow runs `dotnet test` yet, local `dotnet test` still satisfies AC; consider a follow-up chore to wire CI when available.