using Microsoft.Extensions.Options;
namespace NeonSprawl.Server.Game.PositionState;
/// Maps HTTP routes for authoritative position read and move (NS-15 / NS-16 / NS-19).
public static class PositionStateApi
{
public static WebApplication MapPositionStateApi(this WebApplication app)
{
app.MapGet(
"/game/players/{id}/position",
(string id, IPositionStateStore store) =>
{
if (!store.TryGetPosition(id, out var snap))
{
return Results.NotFound();
}
var body = new PositionStateResponse
{
SchemaVersion = PositionStateResponse.CurrentSchemaVersion,
PlayerId = id,
Position = new PositionVector { X = snap.X, Y = snap.Y, Z = snap.Z },
Sequence = snap.Sequence,
};
return Results.Json(body);
});
app.MapPost(
"/game/players/{id}/move",
(string id, MoveCommandRequest? body, IPositionStateStore store, IOptions gameOptions) =>
{
if (body is null || body.SchemaVersion != MoveCommandRequest.CurrentSchemaVersion)
{
return Results.BadRequest();
}
var target = body.Target;
if (target is null)
{
return Results.BadRequest();
}
if (!store.TryGetPosition(id, out var current))
{
return Results.NotFound();
}
var rules = gameOptions.Value.MovementValidation;
if (!MoveCommandValidation.TryValidate(current, target, rules, out var reasonCode))
{
var rejected = new MoveCommandRejectedResponse
{
SchemaVersion = MoveCommandRejectedResponse.CurrentSchemaVersion,
ReasonCode = reasonCode,
};
return Results.Json(rejected, statusCode: StatusCodes.Status400BadRequest);
}
if (!store.TryApplyMoveTarget(id, target.X, target.Y, target.Z, out var snap))
{
return Results.NotFound();
}
var response = new PositionStateResponse
{
SchemaVersion = PositionStateResponse.CurrentSchemaVersion,
PlayerId = id,
Position = new PositionVector { X = snap.X, Y = snap.Y, Z = snap.Z },
Sequence = snap.Sequence,
};
return Results.Json(response);
});
return app;
}
}