8.8 KiB
8.8 KiB
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}/positionreturning 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
GETreturns 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 testand/or a curl snippet inserver/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
- 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:1playerId: string (echo path id for sanity)position: object withx,y,zas 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.
- In-memory authority
- Implement
IPositionStateStore(and concrete in-memory type) backed byConcurrentDictionary<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 anIServiceCollectionextension (e.g.PositionStateServiceCollectionExtensions.AddPositionStateStore) that binds options and registers the singleton.
- HTTP API (dedicated class)
- Do not define routes inline in
Program.cs. Add a separate class (e.g.PositionStateApiorPositionStateEndpoints) whose responsibility is mapping this feature’s routes—typically a static class with an extension method onIEndpointRouteBuilderorWebApplication, e.g.MapPositionStateApi(), containing theMapGet("/game/players/{id}/position", …)and handler logic (or thin delegates to a small handler type). Handlers resolveIPositionStateStorefrom DI, returnResults.Json(...)orResults.NotFound().
- Configuration
- Add section in
appsettings.json/appsettings.Development.json(e.g.Game:DevPlayerId, optionalGame:DefaultPositionor nested object). Bind withIOptionsor read once at startup when seeding the store.
- Tests
- Add
NeonSprawl.Server.Tests(xUnit), referenceMicrosoft.AspNetCore.Mvc.Testing, and addpublic partial class Program { }to the server assembly (empty partial in a small file—e.g.Program.Waf.csor the tail ofProgram.cs—only for the test host, not for endpoint definitions) soWebApplicationFactory<Program>works with top-level statements. - One integration test:
GETfor the configured dev player returns 200, body parses,schemaVersionpresent,position.x,position.y,position.zpresent. - Optional second test: unknown id returns 404.
- Documentation
- Extend
server/README.mdwith “Position state (NS-15)”: endpoint path, examplecurl, 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>hittingGET /game/players/{id}/position— satisfies AC and testing expectations (first testable server module gets a test project). - Manual:
curlsnippet in README againstdotnet run(default port fromlaunchSettings.json, typicallyhttp://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}/position — 200 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) | Increments on each successful move apply (NS-16); 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
facingis included, confirm units (radians vs degrees) with NS-10 / future sync design; if unclear, omit v1facingand document “reserved for v2” or addschemaVersionbump 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 testyet, localdotnet teststill satisfies AC; consider a follow-up chore to wire CI when available.