namespace NeonSprawl.Server.Game.PositionState; /// PostgreSQL-backed (NS-17). Player ids are stored normalized: invariant lower + trim for case-insensitive parity with . public sealed class PostgresPositionStateStore(Npgsql.NpgsqlDataSource dataSource) : IPositionStateStore { public bool TryGetPosition(string playerId, out PositionSnapshot snapshot) { var norm = NormalizePlayerId(playerId); if (norm.Length == 0) { snapshot = default; return false; } PostgresPositionBootstrap.EnsureSchema(dataSource); using var conn = dataSource.OpenConnection(); using var cmd = new Npgsql.NpgsqlCommand( "SELECT pos_x, pos_y, pos_z, sequence FROM player_position WHERE player_id = @pid;", conn); cmd.Parameters.AddWithValue("pid", norm); using var reader = cmd.ExecuteReader(); if (!reader.Read()) { snapshot = default; return false; } snapshot = ReadSnapshot(reader); return true; } public bool TryApplyMoveTarget(string playerId, double x, double y, double z, out PositionSnapshot snapshot) { var norm = NormalizePlayerId(playerId); if (norm.Length == 0) { snapshot = default; return false; } PostgresPositionBootstrap.EnsureSchema(dataSource); using var conn = dataSource.OpenConnection(); using var cmd = new Npgsql.NpgsqlCommand( """ UPDATE player_position SET pos_x = @x, pos_y = @y, pos_z = @z, sequence = sequence + 1, updated_at = now() WHERE player_id = @pid RETURNING pos_x, pos_y, pos_z, sequence; """, conn); cmd.Parameters.AddWithValue("pid", norm); cmd.Parameters.AddWithValue(nameof(x), x); cmd.Parameters.AddWithValue(nameof(y), y); cmd.Parameters.AddWithValue(nameof(z), z); using var reader = cmd.ExecuteReader(); if (!reader.Read()) { snapshot = default; return false; } snapshot = ReadSnapshot(reader); return true; } private static PositionSnapshot ReadSnapshot(Npgsql.NpgsqlDataReader reader) { var seq = reader.GetInt64(3); return new PositionSnapshot( reader.GetDouble(0), reader.GetDouble(1), reader.GetDouble(2), unchecked((ulong)seq)); } private static string NormalizePlayerId(string? playerId) { var t = playerId?.Trim(); if (string.IsNullOrEmpty(t)) { return string.Empty; } return t.ToLowerInvariant(); } }