using System.Threading; namespace NeonSprawl.Server.Game.PositionState; /// /// Applies NS-17 DDL (once per process, idempotent) and seeds the configured dev player row. /// Used at host startup and by integration tests after TRUNCATE. /// public static class PostgresPositionBootstrap { private static readonly string DdlRelativePath = Path.Combine("db", "migrations", "V001__player_position.sql"); private static readonly Lock SchemaGate = new(); private static int _schemaReady; /// /// Ensures player_position exists. Heavy work (read migration, run DDL) runs at most once per process. /// public static void EnsureSchema(Npgsql.NpgsqlDataSource dataSource) { if (System.Threading.Volatile.Read(ref _schemaReady) != 0) { return; } lock (SchemaGate) { if (System.Threading.Volatile.Read(ref _schemaReady) != 0) { return; } var ddlPath = Path.Combine(AppContext.BaseDirectory, DdlRelativePath); if (!File.Exists(ddlPath)) { throw new FileNotFoundException($"NS-17 DDL not found at '{ddlPath}'.", ddlPath); } var ddl = File.ReadAllText(ddlPath); using var conn = dataSource.OpenConnection(); using var cmd = new Npgsql.NpgsqlCommand(ddl, conn); cmd.ExecuteNonQuery(); System.Threading.Volatile.Write(ref _schemaReady, 1); } } /// Idempotent insert for configured seed players at default position. public static void SeedDevPlayer(Npgsql.NpgsqlDataSource dataSource, GamePositionOptions options) { using var conn = dataSource.OpenConnection(); SeedDevPlayer(conn, options); } /// Idempotent insert using an existing open connection (e.g. test harness after TRUNCATE). public static void SeedDevPlayer(Npgsql.NpgsqlConnection connection, GamePositionOptions options) { var devNorm = NormalizePlayerId(options.DevPlayerId); var p = options.DefaultPosition ?? new DefaultPositionOptions(); using var cmd = new Npgsql.NpgsqlCommand( """ INSERT INTO player_position (player_id, pos_x, pos_y, pos_z, sequence) VALUES (@pid, @x, @y, @z, 0) ON CONFLICT (player_id) DO NOTHING; """, connection); cmd.Parameters.AddWithValue("pid", devNorm); cmd.Parameters.AddWithValue("x", p.X); cmd.Parameters.AddWithValue("y", p.Y); cmd.Parameters.AddWithValue("z", p.Z); cmd.ExecuteNonQuery(); } private static string NormalizePlayerId(string? playerId) { var t = playerId?.Trim(); if (string.IsNullOrEmpty(t)) { throw new InvalidOperationException("Game:DevPlayerId must be non-empty."); } return t.ToLowerInvariant(); } }