129 lines
4.7 KiB
C#
129 lines
4.7 KiB
C#
using NeonSprawl.Server.Game.PositionState;
|
|
|
|
namespace NeonSprawl.Server.Game.AbilityInput;
|
|
|
|
/// <summary>Maps hotbar loadout read/update APIs (NEO-29).</summary>
|
|
public static class HotbarLoadoutApi
|
|
{
|
|
public const string ReasonSlotOutOfBounds = "slot_out_of_bounds";
|
|
public const string ReasonUnknownAbility = "unknown_ability";
|
|
public const string ReasonDuplicateSlot = "duplicate_slot";
|
|
|
|
public static WebApplication MapHotbarLoadoutApi(this WebApplication app)
|
|
{
|
|
app.MapGet(
|
|
"/game/players/{id}/hotbar-loadout",
|
|
(string id, IPositionStateStore positions, IPlayerHotbarLoadoutStore store) =>
|
|
{
|
|
if (!positions.TryGetPosition(id, out _))
|
|
{
|
|
return Results.NotFound();
|
|
}
|
|
|
|
if (!store.TryGetBindings(id, out var bindings))
|
|
{
|
|
return Results.NotFound();
|
|
}
|
|
|
|
return Results.Json(BuildLoadoutResponse(id, bindings));
|
|
});
|
|
|
|
app.MapPost(
|
|
"/game/players/{id}/hotbar-loadout",
|
|
(string id, HotbarLoadoutUpdateRequest? body, IPositionStateStore positions, IPlayerHotbarLoadoutStore store) =>
|
|
{
|
|
if (body is null || body.SchemaVersion != HotbarLoadoutUpdateRequest.CurrentSchemaVersion)
|
|
{
|
|
return Results.BadRequest();
|
|
}
|
|
|
|
if (!positions.TryGetPosition(id, out _))
|
|
{
|
|
return Results.NotFound();
|
|
}
|
|
|
|
if (!store.TryGetBindings(id, out var before))
|
|
{
|
|
return Results.NotFound();
|
|
}
|
|
|
|
var updates = body.Slots ?? [];
|
|
var parsedUpdates = new List<HotbarSlotBinding>(updates.Count);
|
|
var seenSlots = new HashSet<int>();
|
|
foreach (var update in updates)
|
|
{
|
|
if (update.SlotIndex < 0 || update.SlotIndex >= HotbarLoadoutResponse.SlotCountV1)
|
|
{
|
|
return Results.Json(
|
|
new HotbarLoadoutUpdateResponse
|
|
{
|
|
Updated = false,
|
|
ReasonCode = ReasonSlotOutOfBounds,
|
|
Loadout = BuildLoadoutResponse(id, before),
|
|
});
|
|
}
|
|
|
|
if (!seenSlots.Add(update.SlotIndex))
|
|
{
|
|
return Results.Json(
|
|
new HotbarLoadoutUpdateResponse
|
|
{
|
|
Updated = false,
|
|
ReasonCode = ReasonDuplicateSlot,
|
|
Loadout = BuildLoadoutResponse(id, before),
|
|
});
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(update.AbilityId) ||
|
|
!PrototypeAbilityRegistry.TryNormalizeKnown(update.AbilityId, out var normalizedAbilityId))
|
|
{
|
|
return Results.Json(
|
|
new HotbarLoadoutUpdateResponse
|
|
{
|
|
Updated = false,
|
|
ReasonCode = ReasonUnknownAbility,
|
|
Loadout = BuildLoadoutResponse(id, before),
|
|
});
|
|
}
|
|
|
|
parsedUpdates.Add(new HotbarSlotBinding(update.SlotIndex, normalizedAbilityId));
|
|
}
|
|
|
|
if (!store.TryUpsertBindings(id, parsedUpdates, out var after))
|
|
{
|
|
return Results.NotFound();
|
|
}
|
|
|
|
return Results.Json(
|
|
new HotbarLoadoutUpdateResponse
|
|
{
|
|
Updated = true,
|
|
Loadout = BuildLoadoutResponse(id, after),
|
|
});
|
|
});
|
|
|
|
return app;
|
|
}
|
|
|
|
private static HotbarLoadoutResponse BuildLoadoutResponse(string playerId, IReadOnlyDictionary<int, string> bindings)
|
|
{
|
|
var slots = new List<HotbarSlotStateJson>(HotbarLoadoutResponse.SlotCountV1);
|
|
for (var i = 0; i < HotbarLoadoutResponse.SlotCountV1; i++)
|
|
{
|
|
bindings.TryGetValue(i, out var abilityId);
|
|
slots.Add(
|
|
new HotbarSlotStateJson
|
|
{
|
|
SlotIndex = i,
|
|
AbilityId = abilityId,
|
|
});
|
|
}
|
|
|
|
return new HotbarLoadoutResponse
|
|
{
|
|
PlayerId = playerId.Trim(),
|
|
Slots = slots,
|
|
};
|
|
}
|
|
}
|