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 = ResourceNodeInstanceIds.Normalize(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 = ResourceNodeInstanceIds.Normalize(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 = ResourceNodeInstanceIds.Normalize(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;
}
///
public bool TrySetRemainingGathers(string interactableId, int remainingGathers)
{
var key = ResourceNodeInstanceIds.Normalize(interactableId);
if (key.Length == 0 || remainingGathers < 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 UPDATE
SET remaining_gathers = EXCLUDED.remaining_gathers,
updated_at = now();
""",
conn);
cmd.Parameters.AddWithValue("id", key);
cmd.Parameters.AddWithValue("remaining", remainingGathers);
cmd.ExecuteNonQuery();
return true;
}
}