86 lines
3.0 KiB
C#
86 lines
3.0 KiB
C#
using System.Threading;
|
|
|
|
namespace NeonSprawl.Server.Game.PositionState;
|
|
|
|
/// <summary>
|
|
/// Applies NS-17 DDL (once per process, idempotent) and seeds the configured dev player row.
|
|
/// Used at host startup and by integration tests after <c>TRUNCATE</c>.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Ensures <c>player_position</c> exists. Heavy work (read migration, run DDL) runs at most once per process.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>Idempotent insert for configured seed players at default position.</summary>
|
|
public static void SeedDevPlayer(Npgsql.NpgsqlDataSource dataSource, GamePositionOptions options)
|
|
{
|
|
using var conn = dataSource.OpenConnection();
|
|
SeedDevPlayer(conn, options);
|
|
}
|
|
|
|
/// <summary>Idempotent insert using an existing open connection (e.g. test harness after <c>TRUNCATE</c>).</summary>
|
|
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();
|
|
}
|
|
}
|