using NeonSprawl.Server.Game.PositionState; namespace NeonSprawl.Server.Game.Items; /// /// Maps GET/POST /game/players/{{id}}/inventory — read snapshot and add/remove mutations (NEO-55). /// 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 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; } }