189 lines
6.7 KiB
C#
189 lines
6.7 KiB
C#
namespace NeonSprawl.Server.Game.Factions;
|
|
|
|
/// <summary>PostgreSQL-backed faction standing keyed by normalized player id + faction id (NEO-135).</summary>
|
|
public sealed class PostgresFactionStandingStore(
|
|
Npgsql.NpgsqlDataSource dataSource,
|
|
IFactionDefinitionRegistry factionRegistry) : IFactionStandingStore
|
|
{
|
|
/// <inheritdoc />
|
|
public bool CanWritePlayer(string playerId)
|
|
{
|
|
var player = FactionStandingIds.NormalizePlayerId(playerId);
|
|
if (player.Length == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
PostgresPlayerFactionStandingBootstrap.EnsureSchema(dataSource);
|
|
using var conn = dataSource.OpenConnection();
|
|
return PlayerExists(conn, player);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public FactionStandingReadOutcome TryGetStanding(string playerId, string factionId)
|
|
{
|
|
var player = FactionStandingIds.NormalizePlayerId(playerId);
|
|
var faction = FactionStandingIds.NormalizeFactionId(factionId);
|
|
if (player.Length == 0)
|
|
{
|
|
return DenyRead(FactionStandingReasonCodes.PlayerNotWritable);
|
|
}
|
|
|
|
if (faction.Length == 0)
|
|
{
|
|
return DenyRead(FactionStandingReasonCodes.UnknownFaction);
|
|
}
|
|
|
|
if (!factionRegistry.TryGetDefinition(faction, out var definition) || definition is null)
|
|
{
|
|
return DenyRead(FactionStandingReasonCodes.UnknownFaction);
|
|
}
|
|
|
|
PostgresPlayerFactionStandingBootstrap.EnsureSchema(dataSource);
|
|
using var conn = dataSource.OpenConnection();
|
|
if (!PlayerExists(conn, player))
|
|
{
|
|
return DenyRead(FactionStandingReasonCodes.PlayerNotWritable);
|
|
}
|
|
|
|
using var cmd = new Npgsql.NpgsqlCommand(
|
|
"""
|
|
SELECT standing
|
|
FROM player_faction_standing
|
|
WHERE player_id = @pid AND faction_id = @fid
|
|
LIMIT 1;
|
|
""",
|
|
conn);
|
|
cmd.Parameters.AddWithValue("pid", player);
|
|
cmd.Parameters.AddWithValue("fid", faction);
|
|
var scalar = cmd.ExecuteScalar();
|
|
var raw = scalar is null or DBNull
|
|
? 0
|
|
: scalar is int xi
|
|
? xi
|
|
: Convert.ToInt32(scalar, System.Globalization.CultureInfo.InvariantCulture);
|
|
return new FactionStandingReadOutcome(true, null, FactionStandingIds.ClampStanding(raw, definition));
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public FactionStandingMutationOutcome TryApplyStandingDelta(string playerId, string factionId, int deltaAmount)
|
|
{
|
|
var player = FactionStandingIds.NormalizePlayerId(playerId);
|
|
var faction = FactionStandingIds.NormalizeFactionId(factionId);
|
|
if (player.Length == 0)
|
|
{
|
|
return DenyMutation(FactionStandingReasonCodes.PlayerNotWritable);
|
|
}
|
|
|
|
if (faction.Length == 0)
|
|
{
|
|
return DenyMutation(FactionStandingReasonCodes.UnknownFaction);
|
|
}
|
|
|
|
if (!factionRegistry.TryGetDefinition(faction, out var definition) || definition is null)
|
|
{
|
|
return DenyMutation(FactionStandingReasonCodes.UnknownFaction);
|
|
}
|
|
|
|
PostgresPlayerFactionStandingBootstrap.EnsureSchema(dataSource);
|
|
using var conn = dataSource.OpenConnection();
|
|
using var tx = conn.BeginTransaction();
|
|
if (!PlayerExists(conn, player, tx))
|
|
{
|
|
tx.Rollback();
|
|
return DenyMutation(FactionStandingReasonCodes.PlayerNotWritable);
|
|
}
|
|
|
|
var raw = 0;
|
|
using (var sel = new Npgsql.NpgsqlCommand(
|
|
"""
|
|
SELECT standing
|
|
FROM player_faction_standing
|
|
WHERE player_id = @pid AND faction_id = @fid
|
|
LIMIT 1
|
|
FOR UPDATE;
|
|
""",
|
|
conn,
|
|
tx))
|
|
{
|
|
sel.Parameters.AddWithValue("pid", player);
|
|
sel.Parameters.AddWithValue("fid", faction);
|
|
var scalar = sel.ExecuteScalar();
|
|
raw = scalar is null or DBNull
|
|
? 0
|
|
: scalar is int xi
|
|
? xi
|
|
: Convert.ToInt32(scalar, System.Globalization.CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
var previousStanding = FactionStandingIds.ClampStanding(raw, definition);
|
|
var newStanding = FactionStandingIds.ClampStanding(previousStanding + deltaAmount, definition);
|
|
|
|
using var upsert = new Npgsql.NpgsqlCommand(
|
|
"""
|
|
INSERT INTO player_faction_standing (player_id, faction_id, standing, updated_at)
|
|
VALUES (@pid, @fid, @standing, now())
|
|
ON CONFLICT (player_id, faction_id)
|
|
DO UPDATE SET standing = EXCLUDED.standing, updated_at = now();
|
|
""",
|
|
conn,
|
|
tx);
|
|
upsert.Parameters.AddWithValue("pid", player);
|
|
upsert.Parameters.AddWithValue("fid", faction);
|
|
upsert.Parameters.AddWithValue("standing", newStanding);
|
|
upsert.ExecuteNonQuery();
|
|
tx.Commit();
|
|
return new FactionStandingMutationOutcome(true, null, previousStanding, newStanding);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public bool TryClearStanding(string playerId, string factionId)
|
|
{
|
|
var player = FactionStandingIds.NormalizePlayerId(playerId);
|
|
var faction = FactionStandingIds.NormalizeFactionId(factionId);
|
|
if (player.Length == 0 || faction.Length == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!factionRegistry.TryGetDefinition(faction, out _))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
PostgresPlayerFactionStandingBootstrap.EnsureSchema(dataSource);
|
|
using var conn = dataSource.OpenConnection();
|
|
if (!PlayerExists(conn, player))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
using var cmd = new Npgsql.NpgsqlCommand(
|
|
"""
|
|
DELETE FROM player_faction_standing
|
|
WHERE player_id = @pid AND faction_id = @fid;
|
|
""",
|
|
conn);
|
|
cmd.Parameters.AddWithValue("pid", player);
|
|
cmd.Parameters.AddWithValue("fid", faction);
|
|
cmd.ExecuteNonQuery();
|
|
return true;
|
|
}
|
|
|
|
private static bool PlayerExists(Npgsql.NpgsqlConnection conn, string playerIdNormalized, Npgsql.NpgsqlTransaction? tx = null)
|
|
{
|
|
using var cmd = new Npgsql.NpgsqlCommand(
|
|
"SELECT 1 FROM player_position WHERE player_id = @pid LIMIT 1;",
|
|
conn,
|
|
tx);
|
|
cmd.Parameters.AddWithValue("pid", playerIdNormalized);
|
|
return cmd.ExecuteScalar() is not null;
|
|
}
|
|
|
|
private static FactionStandingReadOutcome DenyRead(string reasonCode) =>
|
|
new(false, reasonCode, 0);
|
|
|
|
private static FactionStandingMutationOutcome DenyMutation(string reasonCode) =>
|
|
new(false, reasonCode, 0, 0);
|
|
}
|