NEO-32: optional NEON_SPRAWL_API_LOG stdout for ability-cast
parent
ae74f570dc
commit
f4ba4cdd92
|
|
@ -22,6 +22,7 @@ body:json {
|
||||||
tests {
|
tests {
|
||||||
test("status 200", function () {
|
test("status 200", function () {
|
||||||
// NEO-30 review follow-up: cast JSON contract unchanged (server comments only).
|
// NEO-30 review follow-up: cast JSON contract unchanged (server comments only).
|
||||||
|
// Optional: NEON_SPRAWL_API_LOG=1 on server prints ability_cast_requested for accepted JSON.
|
||||||
expect(res.getStatus()).to.equal(200);
|
expect(res.getStatus()).to.equal(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,14 @@
|
||||||
|
|
||||||
## 1) Code / docs sanity
|
## 1) Code / docs sanity
|
||||||
|
|
||||||
- [ ] Open `server/NeonSprawl.Server/Game/AbilityInput/AbilityCastApi.cs` and confirm **NEO-30** comments mark **`ability_cast_denied`** on each `Results.Json` deny and **`ability_cast_requested`** on the final accept return; each deny branch notes **TODO(E9.M1)**.
|
- [ ] Open `server/NeonSprawl.Server/Game/AbilityInput/AbilityCastApi.cs` and confirm **NEO-30** comments mark **`ability_cast_denied`** on each JSON deny return and **`ability_cast_requested`** on the final accept return; each deny branch notes **TODO(E9.M1)**.
|
||||||
- [ ] Confirm **400** / **404** early returns are documented as **not** Slice 3 `ability_cast_denied` JSON paths.
|
- [ ] Confirm **400** / **404** early returns are documented as **not** Slice 3 `ability_cast_denied` JSON paths.
|
||||||
- [ ] Skim `docs/decomposition/modules/E1_M4_AbilityInputScaffold.md` — snapshot mentions NEO-30 hook file paths; **E1M4-05** row links to Linear **NEO-30**.
|
- [ ] Skim `docs/decomposition/modules/E1_M4_AbilityInputScaffold.md` — snapshot mentions NEO-30 hook file paths; **E1M4-05** row links to Linear **NEO-30**.
|
||||||
|
|
||||||
## 2) Client hook vocabulary (regression with NEO-31 / NEO-28)
|
## 2) Client hook vocabulary (regression with NEO-31 / NEO-28)
|
||||||
|
|
||||||
- [ ] With server running and slot **0** bound to `prototype_pulse`, **Tab** lock **alpha**, press **1**: Godot Output line starts with **`ability_cast_requested`** and includes `slot=0`, `ability_id=prototype_pulse`, `targetId=` consistent with HUD lock.
|
- [ ] With server running and slot **0** bound to `prototype_pulse`, **Tab** lock **alpha**, press **1**: Godot Output line starts with **`ability_cast_requested`** and includes `slot=0`, `ability_id=prototype_pulse`, `targetId=` consistent with HUD lock.
|
||||||
|
- [ ] **Optional (server stdout):** same scenario with **`NEON_SPRAWL_API_LOG=1`** (see [server README — Run](../../server/README.md)) — server console shows **`[ability-cast] ability_cast_requested`** for that accept (Slice 3 hook name alignment).
|
||||||
- [ ] **Without** lock (or wrong target), press **1**: **`CastFeedbackLabel`** shows **`ability_cast_denied: …`** with a **non-empty** reason; Output warning includes **`ability_cast_denied`** and **`reasonCode=`** (see [`docs/manual-qa/NEO-28.md`](NEO-28.md) for concrete cases).
|
- [ ] **Without** lock (or wrong target), press **1**: **`CastFeedbackLabel`** shows **`ability_cast_denied: …`** with a **non-empty** reason; Output warning includes **`ability_cast_denied`** and **`reasonCode=`** (see [`docs/manual-qa/NEO-28.md`](NEO-28.md) for concrete cases).
|
||||||
|
|
||||||
## 3) Server JSON deny (reason non-empty)
|
## 3) Server JSON deny (reason non-empty)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,6 @@ Prereq: game server running (`NeonSprawl.Server`), client pointed at same `base_
|
||||||
|
|
||||||
2. **Server deny on spam** — Immediately double-tap cast during cooling (or remove client guard temporarily): second attempt should show `ability_cast_denied: on_cooldown` (server) on the cast feedback line when POST completes.
|
2. **Server deny on spam** — Immediately double-tap cast during cooling (or remove client guard temporarily): second attempt should show `ability_cast_denied: on_cooldown` (server) on the cast feedback line when POST completes.
|
||||||
|
|
||||||
3. **Client guard** — While slot 0 shows cooling, press `1` repeatedly. Expect **`ability_cast_denied: on_cooldown (client guard)`** on **`CastFeedbackLabel`** on most presses (local mirror blocks the POST). No server console logging required. **Optional proof the server still enforces cooldown:** in Bruno (or `curl`), send **`POST …/ability-cast`** twice in a row with the same valid body while the snapshot still shows slot 0 cooling — second response **`accepted": false`**, **`reasonCode": "on_cooldown"`** (same contract as step 2).
|
3. **Client guard** — While slot 0 shows cooling, press `1` repeatedly. Expect **`ability_cast_denied: on_cooldown (client guard)`** on **`CastFeedbackLabel`** on most presses (local mirror blocks the POST). **Optional:** run the server with **`NEON_SPRAWL_API_LOG=1`** (or launch profile **`http+apiLog`**) — each cast that reaches the server and returns JSON prints **`[ability-cast] ability_cast_denied …`** or **`ability_cast_requested`** to the process console, so rapid `1` with the guard on should produce **fewer** lines than without the guard. **Optional proof the server still enforces cooldown:** in Bruno (or `curl`), send **`POST …/ability-cast`** twice in a row with the same valid body while the snapshot still shows slot 0 cooling — second response **`accepted": false`**, **`reasonCode": "on_cooldown"`** (same contract as step 2).
|
||||||
|
|
||||||
4. **Reconnect / rehydrate** — With slot 0 mid-cooldown, restart the client (or reload scene) while server keeps running; after boot expect cooldown line to match server within one GET cycle (same dev player id).
|
4. **Reconnect / rehydrate** — With slot 0 mid-cooldown, restart the client (or reload scene) while server keeps running; after boot expect cooldown line to match server within one GET cycle (same dev player id).
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ namespace NeonSprawl.Server.Tests.Game.AbilityInput;
|
||||||
/// NEO-30: every JSON deny (<see cref="AbilityCastResponse.Accepted"/> <c>false</c>) asserts a non-whitespace
|
/// NEO-30: every JSON deny (<see cref="AbilityCastResponse.Accepted"/> <c>false</c>) asserts a non-whitespace
|
||||||
/// <see cref="AbilityCastResponse.ReasonCode"/> — matches Slice 3 <c>ability_cast_denied</c> payload expectations;
|
/// <see cref="AbilityCastResponse.ReasonCode"/> — matches Slice 3 <c>ability_cast_denied</c> payload expectations;
|
||||||
/// authoritative hook-site comments live on <see cref="AbilityCastApi"/> (cast route: <c>MapAbilityCastApi</c> extension in the same source file).
|
/// authoritative hook-site comments live on <see cref="AbilityCastApi"/> (cast route: <c>MapAbilityCastApi</c> extension in the same source file).
|
||||||
|
/// Optional dev: <c>NEON_SPRAWL_API_LOG=1</c> enables one-line <c>ability_cast_*</c> stdout per JSON cast response (see <see cref="Diagnostics.PrototypeApiConsoleLog"/>).
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed class AbilityCastApiTests
|
public sealed class AbilityCastApiTests
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
namespace NeonSprawl.Server.Diagnostics;
|
||||||
|
|
||||||
|
/// <summary>Optional one-line stdout traces for prototype HTTP routes (manual QA / dev until E9.M1).</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Enable with environment variable <c>NEON_SPRAWL_API_LOG</c> set to <c>1</c>, <c>true</c>, <c>yes</c>, or <c>on</c> (case-insensitive).
|
||||||
|
/// </remarks>
|
||||||
|
public static class PrototypeApiConsoleLog
|
||||||
|
{
|
||||||
|
/// <summary>Slice 3 hook names; matches product telemetry vocabulary in <c>AbilityCastApi</c> comments.</summary>
|
||||||
|
public static bool IsAbilityCastStdoutEnabled()
|
||||||
|
{
|
||||||
|
var raw = Environment.GetEnvironmentVariable("NEON_SPRAWL_API_LOG");
|
||||||
|
if (string.IsNullOrWhiteSpace(raw))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var v = raw.Trim();
|
||||||
|
return v is "1" or "true" or "yes"
|
||||||
|
|| string.Equals(v, "on", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>One line per JSON ability-cast response (HTTP 200 with <c>AbilityCastResponse</c>).</summary>
|
||||||
|
public static void WriteAbilityCastLine(
|
||||||
|
string playerId,
|
||||||
|
int slotIndex,
|
||||||
|
string? abilityId,
|
||||||
|
string? targetId,
|
||||||
|
bool accepted,
|
||||||
|
string? reasonCode)
|
||||||
|
{
|
||||||
|
if (!IsAbilityCastStdoutEnabled())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accepted)
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
"[ability-cast] ability_cast_requested player={0} slot={1} ability={2} target={3}",
|
||||||
|
playerId,
|
||||||
|
slotIndex,
|
||||||
|
abilityId ?? "",
|
||||||
|
targetId ?? "");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
"[ability-cast] ability_cast_denied player={0} slot={1} reason={2}",
|
||||||
|
playerId,
|
||||||
|
slotIndex,
|
||||||
|
reasonCode ?? "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using NeonSprawl.Server.Diagnostics;
|
||||||
using NeonSprawl.Server.Game.PositionState;
|
using NeonSprawl.Server.Game.PositionState;
|
||||||
using NeonSprawl.Server.Game.Targeting;
|
using NeonSprawl.Server.Game.Targeting;
|
||||||
using NeonSprawl.Server.Game.World;
|
using NeonSprawl.Server.Game.World;
|
||||||
|
|
@ -9,6 +10,7 @@ namespace NeonSprawl.Server.Game.AbilityInput;
|
||||||
/// Epic 1 Slice 3 product telemetry names <c>ability_cast_requested</c> and <c>ability_cast_denied</c>
|
/// Epic 1 Slice 3 product telemetry names <c>ability_cast_requested</c> and <c>ability_cast_denied</c>
|
||||||
/// (on JSON denies, <see cref="AbilityCastResponse.ReasonCode"/> — wire JSON <c>reasonCode</c>) — hook sites are marked inline below.
|
/// (on JSON denies, <see cref="AbilityCastResponse.ReasonCode"/> — wire JSON <c>reasonCode</c>) — hook sites are marked inline below.
|
||||||
/// TODO(E9.M1): emit cataloged events (payload: route player id, slotIndex, abilityId, targetId; on deny include reasonCode).
|
/// TODO(E9.M1): emit cataloged events (payload: route player id, slotIndex, abilityId, targetId; on deny include reasonCode).
|
||||||
|
/// Optional dev stdout: set <c>NEON_SPRAWL_API_LOG=1</c> (see <see cref="Diagnostics.PrototypeApiConsoleLog"/>) for one line per JSON cast response.
|
||||||
/// HTTP 400/404 here are outside the v1 cast deny contract (no JSON <c>AbilityCastResponse</c>).
|
/// HTTP 400/404 here are outside the v1 cast deny contract (no JSON <c>AbilityCastResponse</c>).
|
||||||
/// NEO-32: JSON deny <c>on_cooldown</c> when the slot is inside the prototype global cooldown window.
|
/// NEO-32: JSON deny <c>on_cooldown</c> when the slot is inside the prototype global cooldown window.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
|
|
@ -28,6 +30,22 @@ public static class AbilityCastApi
|
||||||
/// <summary>Slot still inside server-authoritative cooldown window (NEO-32).</summary>
|
/// <summary>Slot still inside server-authoritative cooldown window (NEO-32).</summary>
|
||||||
public const string ReasonOnCooldown = "on_cooldown";
|
public const string ReasonOnCooldown = "on_cooldown";
|
||||||
|
|
||||||
|
private static IResult JsonAbilityCast(
|
||||||
|
string id,
|
||||||
|
AbilityCastRequest body,
|
||||||
|
AbilityCastResponse response,
|
||||||
|
string? abilityIdForLog = null)
|
||||||
|
{
|
||||||
|
PrototypeApiConsoleLog.WriteAbilityCastLine(
|
||||||
|
id,
|
||||||
|
body.SlotIndex,
|
||||||
|
abilityIdForLog ?? body.AbilityId,
|
||||||
|
body.TargetId,
|
||||||
|
response.Accepted,
|
||||||
|
response.ReasonCode);
|
||||||
|
return Results.Json(response);
|
||||||
|
}
|
||||||
|
|
||||||
public static WebApplication MapAbilityCastApi(this WebApplication app)
|
public static WebApplication MapAbilityCastApi(this WebApplication app)
|
||||||
{
|
{
|
||||||
app.MapPost(
|
app.MapPost(
|
||||||
|
|
@ -62,7 +80,9 @@ public static class AbilityCastApi
|
||||||
if (body.SlotIndex < 0 || body.SlotIndex >= HotbarLoadoutResponse.SlotCountV1)
|
if (body.SlotIndex < 0 || body.SlotIndex >= HotbarLoadoutResponse.SlotCountV1)
|
||||||
{
|
{
|
||||||
// NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
|
// NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
|
||||||
return Results.Json(
|
return JsonAbilityCast(
|
||||||
|
id,
|
||||||
|
body,
|
||||||
new AbilityCastResponse
|
new AbilityCastResponse
|
||||||
{
|
{
|
||||||
Accepted = false,
|
Accepted = false,
|
||||||
|
|
@ -74,7 +94,9 @@ public static class AbilityCastApi
|
||||||
string.IsNullOrWhiteSpace(boundAbility))
|
string.IsNullOrWhiteSpace(boundAbility))
|
||||||
{
|
{
|
||||||
// NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
|
// NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
|
||||||
return Results.Json(
|
return JsonAbilityCast(
|
||||||
|
id,
|
||||||
|
body,
|
||||||
new AbilityCastResponse
|
new AbilityCastResponse
|
||||||
{
|
{
|
||||||
Accepted = false,
|
Accepted = false,
|
||||||
|
|
@ -86,7 +108,9 @@ public static class AbilityCastApi
|
||||||
!PrototypeAbilityRegistry.TryNormalizeKnown(body.AbilityId, out var normalizedRequest))
|
!PrototypeAbilityRegistry.TryNormalizeKnown(body.AbilityId, out var normalizedRequest))
|
||||||
{
|
{
|
||||||
// NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
|
// NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
|
||||||
return Results.Json(
|
return JsonAbilityCast(
|
||||||
|
id,
|
||||||
|
body,
|
||||||
new AbilityCastResponse
|
new AbilityCastResponse
|
||||||
{
|
{
|
||||||
Accepted = false,
|
Accepted = false,
|
||||||
|
|
@ -97,7 +121,9 @@ public static class AbilityCastApi
|
||||||
if (!string.Equals(normalizedRequest, boundAbility, StringComparison.Ordinal))
|
if (!string.Equals(normalizedRequest, boundAbility, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
// NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
|
// NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
|
||||||
return Results.Json(
|
return JsonAbilityCast(
|
||||||
|
id,
|
||||||
|
body,
|
||||||
new AbilityCastResponse
|
new AbilityCastResponse
|
||||||
{
|
{
|
||||||
Accepted = false,
|
Accepted = false,
|
||||||
|
|
@ -108,7 +134,9 @@ public static class AbilityCastApi
|
||||||
if (string.IsNullOrWhiteSpace(body.TargetId))
|
if (string.IsNullOrWhiteSpace(body.TargetId))
|
||||||
{
|
{
|
||||||
// NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
|
// NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
|
||||||
return Results.Json(
|
return JsonAbilityCast(
|
||||||
|
id,
|
||||||
|
body,
|
||||||
new AbilityCastResponse { Accepted = false, ReasonCode = ReasonInvalidTarget });
|
new AbilityCastResponse { Accepted = false, ReasonCode = ReasonInvalidTarget });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,7 +144,9 @@ public static class AbilityCastApi
|
||||||
if (!PrototypeTargetRegistry.TryGet(lookupKey, out var entry))
|
if (!PrototypeTargetRegistry.TryGet(lookupKey, out var entry))
|
||||||
{
|
{
|
||||||
// NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
|
// NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
|
||||||
return Results.Json(
|
return JsonAbilityCast(
|
||||||
|
id,
|
||||||
|
body,
|
||||||
new AbilityCastResponse { Accepted = false, ReasonCode = ReasonInvalidTarget });
|
new AbilityCastResponse { Accepted = false, ReasonCode = ReasonInvalidTarget });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,14 +155,18 @@ public static class AbilityCastApi
|
||||||
!string.Equals(lookupKey, lockIdLowercase, StringComparison.Ordinal))
|
!string.Equals(lookupKey, lockIdLowercase, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
// NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
|
// NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
|
||||||
return Results.Json(
|
return JsonAbilityCast(
|
||||||
|
id,
|
||||||
|
body,
|
||||||
new AbilityCastResponse { Accepted = false, ReasonCode = ReasonInvalidTarget });
|
new AbilityCastResponse { Accepted = false, ReasonCode = ReasonInvalidTarget });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!HorizontalReach.IsWithinHorizontalRadius(snap.X, snap.Z, entry.X, entry.Z, entry.LockRadius))
|
if (!HorizontalReach.IsWithinHorizontalRadius(snap.X, snap.Z, entry.X, entry.Z, entry.LockRadius))
|
||||||
{
|
{
|
||||||
// NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
|
// NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
|
||||||
return Results.Json(
|
return JsonAbilityCast(
|
||||||
|
id,
|
||||||
|
body,
|
||||||
new AbilityCastResponse { Accepted = false, ReasonCode = ReasonOutOfRange });
|
new AbilityCastResponse { Accepted = false, ReasonCode = ReasonOutOfRange });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,7 +174,9 @@ public static class AbilityCastApi
|
||||||
if (cooldowns.IsOnCooldown(id, body.SlotIndex, now))
|
if (cooldowns.IsOnCooldown(id, body.SlotIndex, now))
|
||||||
{
|
{
|
||||||
// NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
|
// NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
|
||||||
return Results.Json(
|
return JsonAbilityCast(
|
||||||
|
id,
|
||||||
|
body,
|
||||||
new AbilityCastResponse { Accepted = false, ReasonCode = ReasonOnCooldown });
|
new AbilityCastResponse { Accepted = false, ReasonCode = ReasonOnCooldown });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,7 +184,11 @@ public static class AbilityCastApi
|
||||||
|
|
||||||
// NEO-30 telemetry hook site: `ability_cast_requested` at authoritative accept (TODO(E9.M1): catalog emit).
|
// NEO-30 telemetry hook site: `ability_cast_requested` at authoritative accept (TODO(E9.M1): catalog emit).
|
||||||
// Payload notes for E9.M1: player id = route `id`, body.SlotIndex, normalized ability id, body.TargetId (lock-aligned).
|
// Payload notes for E9.M1: player id = route `id`, body.SlotIndex, normalized ability id, body.TargetId (lock-aligned).
|
||||||
return Results.Json(new AbilityCastResponse { Accepted = true });
|
return JsonAbilityCast(
|
||||||
|
id,
|
||||||
|
body,
|
||||||
|
new AbilityCastResponse { Accepted = true },
|
||||||
|
normalizedRequest);
|
||||||
});
|
});
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,16 @@
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"http+apiLog": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"applicationUrl": "http://localhost:5253",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
|
"NEON_SPRAWL_API_LOG": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"IIS Express": {
|
"IIS Express": {
|
||||||
"commandName": "IISExpress",
|
"commandName": "IISExpress",
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,14 @@ dotnet run
|
||||||
- Default URL is printed by Kestrel (see `Properties/launchSettings.json`, typically `http://localhost:5253`).
|
- Default URL is printed by Kestrel (see `Properties/launchSettings.json`, typically `http://localhost:5253`).
|
||||||
- Check `GET /health` for a JSON heartbeat.
|
- Check `GET /health` for a JSON heartbeat.
|
||||||
|
|
||||||
|
**Optional prototype API stdout (manual QA / dev):** set **`NEON_SPRAWL_API_LOG`** to **`1`**, **`true`**, **`yes`**, or **`on`** (case-insensitive). While enabled, each **`POST …/ability-cast`** that returns JSON **`AbilityCastResponse`** (HTTP 200) prints one line to the process console: either **`ability_cast_requested`** (accept) or **`ability_cast_denied`** with **`reason=`** (deny). **`400`** / **`404`** paths do not emit these lines. Implementation: `Diagnostics/PrototypeApiConsoleLog.cs` (wired from `AbilityCastApi`). Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
NEON_SPRAWL_API_LOG=1 dotnet run --project server/NeonSprawl.Server/NeonSprawl.Server.csproj
|
||||||
|
```
|
||||||
|
|
||||||
|
IDE: use launch profile **`http+apiLog`** (same URL as **`http`**, sets the variable).
|
||||||
|
|
||||||
## Position persistence (NEO-8)
|
## Position persistence (NEO-8)
|
||||||
|
|
||||||
When **`ConnectionStrings:NeonSprawl`** is set to a valid PostgreSQL connection string, the server uses the **`player_position`** table (relational columns `pos_x` … `sequence`, not JSONB). DDL lives in [`server/db/migrations/V001__player_position.sql`](../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 store’s constructor seed (not on every HTTP request).
|
When **`ConnectionStrings:NeonSprawl`** is set to a valid PostgreSQL connection string, the server uses the **`player_position`** table (relational columns `pos_x` … `sequence`, not JSONB). DDL lives in [`server/db/migrations/V001__player_position.sql`](../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 store’s constructor seed (not on every HTTP request).
|
||||||
|
|
@ -249,7 +257,7 @@ Prototype **cast intent** (no full combat resolution — see E5.M1). **NEO-28**
|
||||||
| `out_of_range` | Locked target is outside horizontal reach of authoritative player position vs. prototype anchor (NEO-28). |
|
| `out_of_range` | Locked target is outside horizontal reach of authoritative player position vs. prototype anchor (NEO-28). |
|
||||||
| `on_cooldown` | Slot is still inside the server-authoritative cooldown window after a prior successful accept (NEO-32). |
|
| `on_cooldown` | Slot is still inside the server-authoritative cooldown window after a prior successful accept (NEO-32). |
|
||||||
|
|
||||||
Implementation: `server/NeonSprawl.Server/Game/AbilityInput/AbilityCastApi.cs`, `AbilityCastDtos.cs`.
|
Implementation: `server/NeonSprawl.Server/Game/AbilityInput/AbilityCastApi.cs`, `AbilityCastDtos.cs`. Optional **Slice 3–named** one-line stdout per JSON response when **`NEON_SPRAWL_API_LOG`** is set (see **Optional prototype API stdout** under **Run** above).
|
||||||
|
|
||||||
## Cooldown snapshot (NEO-32)
|
## Cooldown snapshot (NEO-32)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue