neon-sprawl/server/NeonSprawl.Server/Game/Items/PlayerInventoryApi.cs

135 lines
4.7 KiB
C#

using NeonSprawl.Server.Game.PositionState;
namespace NeonSprawl.Server.Game.Items;
/// <summary>
/// Maps <c>GET</c>/<c>POST /game/players/{{id}}/inventory</c> — read snapshot and add/remove mutations (NEO-55).
/// </summary>
public static class PlayerInventoryApi
{
public static WebApplication MapPlayerInventoryApi(this WebApplication app)
{
app.MapGet(
"/game/players/{id}/inventory",
(string id, IPositionStateStore positions, IPlayerInventoryStore store) =>
{
var trimmedId = id.Trim();
if (trimmedId.Length == 0 || !positions.TryGetPosition(trimmedId, out _))
{
return Results.NotFound();
}
if (!store.TryGetSnapshot(trimmedId, out var snapshot))
{
return Results.NotFound();
}
return Results.Json(MapSnapshot(trimmedId, snapshot));
});
app.MapPost(
"/game/players/{id}/inventory",
(string id, PlayerInventoryMutationRequest? body, IPositionStateStore positions,
IItemDefinitionRegistry registry, IPlayerInventoryStore store) =>
{
if (body is null || body.SchemaVersion != PlayerInventoryMutationRequest.CurrentSchemaVersion)
{
return Results.BadRequest();
}
if (!TryParseMutationKind(body.MutationKind, out var isAdd))
{
return Results.BadRequest();
}
var trimmedId = id.Trim();
if (trimmedId.Length == 0 || !positions.TryGetPosition(trimmedId, out _))
{
return Results.NotFound();
}
var outcome = isAdd
? PlayerInventoryOperations.TryAddStack(trimmedId, body.ItemId, body.Quantity, registry, store)
: PlayerInventoryOperations.TryRemoveStack(trimmedId, body.ItemId, body.Quantity, registry, store);
return outcome.Kind switch
{
PlayerInventoryMutationKind.StoreMissing => Results.NotFound(),
PlayerInventoryMutationKind.Denied => Results.Json(
BuildMutationResponse(trimmedId, applied: false, outcome.ReasonCode, outcome.Snapshot, store)),
PlayerInventoryMutationKind.Applied => Results.Json(
BuildMutationResponse(trimmedId, applied: true, reasonCode: null, outcome.Snapshot, store)),
_ => throw new InvalidOperationException($"Unexpected inventory mutation outcome: {outcome.Kind}"),
};
});
return app;
}
internal static PlayerInventorySnapshotResponse MapSnapshot(string playerId, PlayerInventorySnapshot snapshot) =>
new()
{
PlayerId = playerId,
SchemaVersion = PlayerInventorySnapshotResponse.CurrentSchemaVersion,
BagSlots = MapSlots(snapshot.BagSlots),
EquipmentSlots = MapSlots(snapshot.EquipmentSlots),
};
private static IReadOnlyList<InventorySlotStateJson> MapSlots(InventorySlotState[] slots)
{
var rows = new InventorySlotStateJson[slots.Length];
for (var i = 0; i < slots.Length; i++)
{
var slot = slots[i];
rows[i] = slot.IsEmpty
? new InventorySlotStateJson { SlotIndex = slot.SlotIndex, Quantity = 0 }
: new InventorySlotStateJson
{
SlotIndex = slot.SlotIndex,
ItemId = slot.ItemId,
Quantity = slot.Quantity,
};
}
return rows;
}
private static PlayerInventoryMutationResponse BuildMutationResponse(
string playerId,
bool applied,
string? reasonCode,
PlayerInventorySnapshot? snapshot,
IPlayerInventoryStore store)
{
if (snapshot is null && !store.TryGetSnapshot(playerId, out snapshot))
{
snapshot = PlayerInventorySnapshot.Empty();
}
return new PlayerInventoryMutationResponse
{
Applied = applied,
ReasonCode = reasonCode,
Inventory = MapSnapshot(playerId, snapshot!),
};
}
private static bool TryParseMutationKind(string? raw, out bool isAdd)
{
if (string.Equals(raw, "add", StringComparison.OrdinalIgnoreCase))
{
isAdd = true;
return true;
}
if (string.Equals(raw, "remove", StringComparison.OrdinalIgnoreCase))
{
isAdd = false;
return true;
}
isAdd = false;
return false;
}
}