neon-sprawl/server
VinPropane e2d64a7a57 NS-17: PostgreSQL position persistence and integration tests
Wire IPositionStateStore to Npgsql when ConnectionStrings:NeonSprawl is set,
with versioned DDL under server/db/migrations and a shared PostgresPositionBootstrap
for schema plus dev-player seeding at host startup. In-memory tests override Postgres
registrations when CI sets the connection string globally.

Add GitHub Actions Postgres service for dotnet test, server README updates,
decomposition and implementation-plan docs, code review write-up, and C# brace
conventions in csharp-style rule.
2026-03-30 22:55:17 -04:00
..
NeonSprawl.Server NS-17: PostgreSQL position persistence and integration tests 2026-03-30 22:55:17 -04:00
NeonSprawl.Server.Tests NS-17: PostgreSQL position persistence and integration tests 2026-03-30 22:55:17 -04:00
db/migrations NS-17: PostgreSQL position persistence and integration tests 2026-03-30 22:55:17 -04:00
README.md NS-17: PostgreSQL position persistence and integration tests 2026-03-30 22:55:17 -04:00

README.md

Game server (NeonSprawl.Server)

ASP.NET Core .NET 10 host for authoritative simulation (prototype: HTTP + health + position state via in-memory or PostgreSQL).

Prerequisites

  • .NET 10 SDK (matches TargetFramework in NeonSprawl.Server.csproj). Check with dotnet --list-sdks.

Run

cd server/NeonSprawl.Server
dotnet run
  • Default URL is printed by Kestrel (see Properties/launchSettings.json, typically http://localhost:5253).
  • Check GET /health for a JSON heartbeat.

Position persistence (NS-17)

When ConnectionStrings:NeonSprawl is set to a valid PostgreSQL connection string, the server uses the player_position table (relational columns pos_xsequence, not JSONB). DDL lives in server/db/migrations/V001__player_position.sql; the app applies that script on startup and on first store use (idempotent CREATE TABLE IF NOT EXISTS). The configured dev player (Game:DevPlayerId / Game:DefaultPosition) is seeded once at host startup, matching the in-memory stores constructor seed (not on every HTTP request).

If ConnectionStrings:NeonSprawl is missing or blank, behavior matches NS-15/NS-16 (in-memory only). This keeps dotnet test without Postgres viable for fast checks; override any process-level env in tests when you need in-memory (see InMemoryWebApplicationFactory in the test project).

Environment variables (typical — same mapping as other ASP.NET Core config):

Mechanism Example
Shell / CI export ConnectionStrings__NeonSprawl='Host=localhost;Port=5432;Database=neon_sprawl;Username=neon_sprawl;Password=neon_sprawl_dev'
User secrets dotnet user-secrets set ConnectionStrings:NeonSprawl "<connection string>" (run from server/NeonSprawl.Server)

Match credentials with root docker-compose.yml for local development.

Restart check (Jira AC): start Postgres (docker compose up -d from repo root), set the connection string, dotnet run, POST …/move, stop the server, dotnet run again, GET …/position should return the last committed state.

Postgres integration tests require ConnectionStrings__NeonSprawl (set automatically in .github/workflows/dotnet.yml). Locally: export the same value while Postgres is listening, then dotnet test NeonSprawl.sln.

Position state (NS-15)

Authoritative player position is served over HTTP whether the backing store is in memory or PostgreSQL (NS-17). The HTTP JSON shape is versioned with top-level schemaVersion (see XML docs on PositionStateResponse in the server project). Path {id} is trimmed and matched case-insensitively against stored players; playerId in the JSON body echoes the path segment from the request. Stored player ids use invariant lowercase + trim in Postgres for lookup parity.

Example (dev player id defaults to dev-local-1 in appsettings.json):

curl -s http://localhost:5253/game/players/dev-local-1/position

Sample response:

{"schemaVersion":1,"playerId":"dev-local-1","position":{"x":0,"y":0,"z":0},"sequence":0}

Unknown player ids return 404. Full zone sync / replication is still out of scope for these spikes.

Move command (NS-16)

POST /game/players/{id}/move with JSON body applies a v1 MoveCommand: authoritative position snaps to target immediately; sequence in responses increases by one per successful apply.

Request body (example):

curl -s -X POST http://localhost:5253/game/players/dev-local-1/move \
  -H "Content-Type: application/json" \
  -d '{"schemaVersion":1,"target":{"x":1.5,"y":0,"z":-2}}'
  • 200 — body matches PositionStateResponse (same shape as GET/position).
  • 400 — missing/invalid body or schemaVersion1.
  • 404 — unknown player id.

See XML on MoveCommandRequest and PositionStateResponse in the server project.

For a PR/Jira-ready contract blurb (formatted JSON + field table), see the NS-15 implementation plan — Pull request description (GET shape) and NS-16 implementation plan (MoveCommand + sequence semantics).

Solution

From repo root: dotnet build NeonSprawl.sln and dotnet test NeonSprawl.sln.

CI

Pull requests targeting main run build and tests via .github/workflows/dotnet.yml on GitHub Actions (ubuntu-latest runner, Release configuration).

Linux + Rider: “only 8.x in /usr/share/dotnet”

If the debugger prints .NET location: /usr/share/dotnet and cannot find Microsoft.NETCore.App 10.x while you have .NET 10 under ~/.dotnet:

  • launchSettings.json environmentVariables (e.g. DOTNET_ROOT) often do not apply to the native apphost Rider launches first; the host probes /usr/share/dotnet before your app starts.
  • This repo sets <UseAppHost>false</UseAppHost> for Debug builds so the IDE runs dotnet …/NeonSprawl.Server.dll using the .NET CLI you configured in the toolchain (/home/don/.dotnet/dotnet), which then loads the 10.x shared runtime from the same install.
  • Alternatives: install .NET 10 into the default location so /usr/share/dotnet has 10.x, or add DOTNET_ROOT=/home/don/.dotnet in Run → Edit Configurations → Environment variables (IDE-level, not only launchSettings.json).