117 lines
3.7 KiB
C#
117 lines
3.7 KiB
C#
namespace NeonSprawl.Server.Game.AbilityInput;
|
|
|
|
/// <summary>PostgreSQL-backed hotbar loadout store keyed by normalized player id.</summary>
|
|
public sealed class PostgresPlayerHotbarLoadoutStore(Npgsql.NpgsqlDataSource dataSource) : IPlayerHotbarLoadoutStore
|
|
{
|
|
public bool TryGetBindings(string playerId, out IReadOnlyDictionary<int, string> bindings)
|
|
{
|
|
var norm = NormalizePlayerId(playerId);
|
|
if (norm.Length == 0)
|
|
{
|
|
bindings = EmptyBindings();
|
|
return false;
|
|
}
|
|
|
|
PostgresHotbarLoadoutBootstrap.EnsureSchema(dataSource);
|
|
using var conn = dataSource.OpenConnection();
|
|
if (!PlayerExists(conn, norm))
|
|
{
|
|
bindings = EmptyBindings();
|
|
return false;
|
|
}
|
|
|
|
bindings = ReadBindings(conn, norm);
|
|
return true;
|
|
}
|
|
|
|
public bool TryUpsertBindings(string playerId, IReadOnlyList<HotbarSlotBinding> updates, out IReadOnlyDictionary<int, string> bindings)
|
|
{
|
|
var norm = NormalizePlayerId(playerId);
|
|
if (norm.Length == 0)
|
|
{
|
|
bindings = EmptyBindings();
|
|
return false;
|
|
}
|
|
|
|
PostgresHotbarLoadoutBootstrap.EnsureSchema(dataSource);
|
|
using var conn = dataSource.OpenConnection();
|
|
using var tx = conn.BeginTransaction();
|
|
|
|
if (!PlayerExists(conn, norm, tx))
|
|
{
|
|
tx.Rollback();
|
|
bindings = EmptyBindings();
|
|
return false;
|
|
}
|
|
|
|
foreach (var update in updates)
|
|
{
|
|
using var cmd = new Npgsql.NpgsqlCommand(
|
|
"""
|
|
INSERT INTO player_hotbar_loadout (player_id, slot_index, ability_id)
|
|
VALUES (@pid, @slot, @ability)
|
|
ON CONFLICT (player_id, slot_index)
|
|
DO UPDATE SET ability_id = EXCLUDED.ability_id, updated_at = now();
|
|
""",
|
|
conn,
|
|
tx);
|
|
cmd.Parameters.AddWithValue("pid", norm);
|
|
cmd.Parameters.AddWithValue("slot", update.SlotIndex);
|
|
cmd.Parameters.AddWithValue("ability", update.AbilityId);
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
bindings = ReadBindings(conn, norm, tx);
|
|
tx.Commit();
|
|
return true;
|
|
}
|
|
|
|
private static IReadOnlyDictionary<int, string> ReadBindings(
|
|
Npgsql.NpgsqlConnection conn,
|
|
string playerIdNormalized,
|
|
Npgsql.NpgsqlTransaction? tx = null)
|
|
{
|
|
var map = new Dictionary<int, string>();
|
|
using var cmd = new Npgsql.NpgsqlCommand(
|
|
"""
|
|
SELECT slot_index, ability_id
|
|
FROM player_hotbar_loadout
|
|
WHERE player_id = @pid
|
|
ORDER BY slot_index;
|
|
""",
|
|
conn,
|
|
tx);
|
|
cmd.Parameters.AddWithValue("pid", playerIdNormalized);
|
|
using var reader = cmd.ExecuteReader();
|
|
while (reader.Read())
|
|
{
|
|
map[reader.GetInt32(0)] = reader.GetString(1);
|
|
}
|
|
|
|
return map;
|
|
}
|
|
|
|
private static bool PlayerExists(Npgsql.NpgsqlConnection conn, string playerIdNormalized, Npgsql.NpgsqlTransaction? tx = null)
|
|
{
|
|
using var cmd = new Npgsql.NpgsqlCommand(
|
|
"SELECT 1 FROM player_position WHERE player_id = @pid LIMIT 1;",
|
|
conn,
|
|
tx);
|
|
cmd.Parameters.AddWithValue("pid", playerIdNormalized);
|
|
return cmd.ExecuteScalar() is not null;
|
|
}
|
|
|
|
private static IReadOnlyDictionary<int, string> EmptyBindings() => new Dictionary<int, string>();
|
|
|
|
private static string NormalizePlayerId(string? playerId)
|
|
{
|
|
var t = playerId?.Trim();
|
|
if (string.IsNullOrEmpty(t))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
return t.ToLowerInvariant();
|
|
}
|
|
}
|