142 lines
5.6 KiB
C#
142 lines
5.6 KiB
C#
using Microsoft.Extensions.Options;
|
|
using NeonSprawl.Server.Game.Npc;
|
|
|
|
namespace NeonSprawl.Server.Game.PositionState;
|
|
|
|
/// <summary>Maps HTTP routes for authoritative position read and move (NS-15 / NS-16 / NS-19).</summary>
|
|
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, IThreatStateStore threatStore, INpcBehaviorDefinitionRegistry behaviorRegistry, IOptions<GamePositionOptions> 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();
|
|
}
|
|
|
|
AggroOperations.TryClearOnLeashForPlayer(id, store, behaviorRegistry, threatStore);
|
|
|
|
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);
|
|
});
|
|
|
|
app.MapPost(
|
|
"/game/players/{id}/move-stream",
|
|
(string id, MoveStreamRequest? body, IPositionStateStore store, IThreatStateStore threatStore, INpcBehaviorDefinitionRegistry behaviorRegistry, IOptions<GamePositionOptions> gameOptions) =>
|
|
{
|
|
if (body is null || body.SchemaVersion != MoveStreamRequest.CurrentSchemaVersion)
|
|
{
|
|
return Results.BadRequest();
|
|
}
|
|
|
|
var targets = body.Targets;
|
|
if (targets is null || targets.Count == 0 || targets.Count > MoveStreamRequest.MaxTargets)
|
|
{
|
|
return Results.BadRequest();
|
|
}
|
|
|
|
if (!store.TryGetPosition(id, out var current))
|
|
{
|
|
return Results.NotFound();
|
|
}
|
|
|
|
var rules = gameOptions.Value.MovementValidation;
|
|
// Validate the full chain before applying so a mid-chain reject does not leave a partial apply.
|
|
var probe = current;
|
|
foreach (var target in targets)
|
|
{
|
|
if (!MoveCommandValidation.TryValidate(probe, target, rules, out var reasonCode))
|
|
{
|
|
var rejected = new MoveCommandRejectedResponse
|
|
{
|
|
SchemaVersion = MoveCommandRejectedResponse.CurrentSchemaVersion,
|
|
ReasonCode = reasonCode,
|
|
};
|
|
return Results.Json(rejected, statusCode: StatusCodes.Status400BadRequest);
|
|
}
|
|
|
|
probe = new PositionSnapshot(target.X, target.Y, target.Z, probe.Sequence);
|
|
}
|
|
|
|
PositionSnapshot lastSnap = current;
|
|
foreach (var target in targets)
|
|
{
|
|
if (!store.TryApplyMoveTarget(id, target.X, target.Y, target.Z, out var snap))
|
|
{
|
|
return Results.NotFound();
|
|
}
|
|
|
|
lastSnap = snap;
|
|
}
|
|
|
|
AggroOperations.TryClearOnLeashForPlayer(id, store, behaviorRegistry, threatStore);
|
|
|
|
var response = new PositionStateResponse
|
|
{
|
|
SchemaVersion = PositionStateResponse.CurrentSchemaVersion,
|
|
PlayerId = id,
|
|
Position = new PositionVector { X = lastSnap.X, Y = lastSnap.Y, Z = lastSnap.Z },
|
|
Sequence = lastSnap.Sequence,
|
|
};
|
|
return Results.Json(response);
|
|
});
|
|
|
|
return app;
|
|
}
|
|
}
|