namespace NeonSprawl.Server.Game.Gathering; /// PostgreSQL-backed world node instance capacity keyed by interactable id (NEO-61). public sealed class PostgresResourceNodeInstanceStore(Npgsql.NpgsqlDataSource dataSource) : IResourceNodeInstanceStore { /// public bool TryGetRemainingGathers(string interactableId, out ResourceNodeInstanceSnapshot snapshot) { var key = InMemoryResourceNodeInstanceStore.NormalizeInteractableId(interactableId); if (key.Length == 0) { snapshot = default; return false; } PostgresResourceNodeInstanceBootstrap.EnsureSchema(dataSource); using var conn = dataSource.OpenConnection(); using var cmd = new Npgsql.NpgsqlCommand( "SELECT remaining_gathers FROM resource_node_instance WHERE interactable_id = @id;", conn); cmd.Parameters.AddWithValue("id", key); var scalar = cmd.ExecuteScalar(); if (scalar is null or DBNull) { snapshot = default; return false; } var remaining = Convert.ToInt32(scalar); snapshot = new ResourceNodeInstanceSnapshot(key, remaining); return true; } /// public bool TryEnsureInitialized(string interactableId, int initialRemainingGathers) { var key = InMemoryResourceNodeInstanceStore.NormalizeInteractableId(interactableId); if (key.Length == 0 || initialRemainingGathers < 0) { return false; } PostgresResourceNodeInstanceBootstrap.EnsureSchema(dataSource); using var conn = dataSource.OpenConnection(); using var cmd = new Npgsql.NpgsqlCommand( """ INSERT INTO resource_node_instance (interactable_id, remaining_gathers, updated_at) VALUES (@id, @remaining, now()) ON CONFLICT (interactable_id) DO NOTHING; """, conn); cmd.Parameters.AddWithValue("id", key); cmd.Parameters.AddWithValue("remaining", initialRemainingGathers); cmd.ExecuteNonQuery(); return true; } /// public bool TryDecrementRemainingGathers( string interactableId, out int previousRemaining, out int remainingGathers) { previousRemaining = 0; remainingGathers = 0; var key = InMemoryResourceNodeInstanceStore.NormalizeInteractableId(interactableId); if (key.Length == 0) { return false; } PostgresResourceNodeInstanceBootstrap.EnsureSchema(dataSource); using var conn = dataSource.OpenConnection(); using var cmd = new Npgsql.NpgsqlCommand( """ UPDATE resource_node_instance SET remaining_gathers = remaining_gathers - 1, updated_at = now() WHERE interactable_id = @id AND remaining_gathers > 0 RETURNING remaining_gathers + 1 AS previous_remaining, remaining_gathers; """, conn); cmd.Parameters.AddWithValue("id", key); using var reader = cmd.ExecuteReader(); if (!reader.Read()) { if (TryGetRemainingGathers(key, out var depleted) && depleted.RemainingGathers == 0) { remainingGathers = 0; } return false; } previousRemaining = reader.GetInt32(0); remainingGathers = reader.GetInt32(1); return true; } }