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

8.7 KiB
Raw Blame History

NS-15 — Implementation plan

Story reference

Field Value
Key NS-15
Title E1.M1: Authoritative PositionState API (in-memory)
Jira NS-15
Parent context NS-10 — E1.M1 InputAndMovementRuntime

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).

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.
  1. 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.
  1. 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().
  1. 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.
  1. 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.
  1. 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/PositionStateApiTests.cs (or similar) 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 (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), paste the following so AC is satisfied without hunting the README.

Endpoint: GET /game/players/{id}/position200 when the player exists, 404 when unknown.

Example response body (v1, formatted for readability):

{
  "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) Ordering hint for future sync; v1 is 0.

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.