Files
Server/StatsDb.cs
2026-04-26 12:44:06 +02:00

328 lines
12 KiB
C#

namespace GeoSus.Server;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
// SQLite databáze pro statistiky hráčů
public class StatsDb : IDisposable
{
private readonly SqliteConnection _connection;
private readonly ILogger<StatsDb> _logger;
private readonly object _lock = new();
public StatsDb(string dbPath, ILogger<StatsDb> logger)
{
_logger = logger;
var directory = Path.GetDirectoryName(dbPath);
if (!string.IsNullOrEmpty(directory))
Directory.CreateDirectory(directory);
_connection = new SqliteConnection($"Data Source={dbPath}");
_connection.Open();
InitializeSchema();
}
private void InitializeSchema()
{
var sql = @"
CREATE TABLE IF NOT EXISTS player_stats (
client_uuid TEXT PRIMARY KEY,
display_name TEXT,
total_games INTEGER DEFAULT 0,
games_as_crew INTEGER DEFAULT 0,
games_as_impostor INTEGER DEFAULT 0,
crew_wins INTEGER DEFAULT 0,
impostor_wins INTEGER DEFAULT 0,
total_kills INTEGER DEFAULT 0,
total_deaths INTEGER DEFAULT 0,
tasks_completed INTEGER DEFAULT 0,
bodies_reported INTEGER DEFAULT 0,
emergency_meetings_called INTEGER DEFAULT 0,
times_voted_out INTEGER DEFAULT 0,
successful_votes INTEGER DEFAULT 0,
total_playtime_seconds INTEGER DEFAULT 0,
cheat_incidents INTEGER DEFAULT 0,
last_seen_utc TEXT,
created_at_utc TEXT
);
CREATE INDEX IF NOT EXISTS idx_crew_wins ON player_stats(crew_wins DESC);
CREATE INDEX IF NOT EXISTS idx_impostor_wins ON player_stats(impostor_wins DESC);
CREATE INDEX IF NOT EXISTS idx_total_games ON player_stats(total_games DESC);
";
using var cmd = _connection.CreateCommand();
cmd.CommandText = sql;
cmd.ExecuteNonQuery();
_logger.LogInformation("StatsDb inicializována");
}
#region CRUD operace
public void EnsurePlayerExists(string clientUuid, string displayName)
{
lock (_lock)
{
var sql = @"
INSERT INTO player_stats (client_uuid, display_name, created_at_utc, last_seen_utc)
VALUES (@uuid, @name, @now, @now)
ON CONFLICT(client_uuid) DO UPDATE SET
display_name = @name,
last_seen_utc = @now
";
using var cmd = _connection.CreateCommand();
cmd.CommandText = sql;
cmd.Parameters.AddWithValue("@uuid", clientUuid);
cmd.Parameters.AddWithValue("@name", displayName);
cmd.Parameters.AddWithValue("@now", DateTime.UtcNow.ToString("o"));
cmd.ExecuteNonQuery();
}
}
public PlayerStats? GetPlayerStats(string clientUuid)
{
lock (_lock)
{
var sql = "SELECT * FROM player_stats WHERE client_uuid = @uuid";
using var cmd = _connection.CreateCommand();
cmd.CommandText = sql;
cmd.Parameters.AddWithValue("@uuid", clientUuid);
using var reader = cmd.ExecuteReader();
if (reader.Read())
{
return ReadPlayerStats(reader);
}
return null;
}
}
public List<PlayerStats> GetLeaderboard(string sortBy, int limit = 100)
{
lock (_lock)
{
var validColumns = new[] {
"crew_wins", "impostor_wins", "total_games",
"total_kills", "tasks_completed"
};
if (!validColumns.Contains(sortBy))
sortBy = "total_games";
var sql = $"SELECT * FROM player_stats ORDER BY {sortBy} DESC LIMIT @limit";
using var cmd = _connection.CreateCommand();
cmd.CommandText = sql;
cmd.Parameters.AddWithValue("@limit", limit);
var results = new List<PlayerStats>();
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
results.Add(ReadPlayerStats(reader));
}
return results;
}
}
#endregion
#region Inkrementální aktualizace
public void IncrementKills(string clientUuid)
{
IncrementColumn(clientUuid, "total_kills");
}
public void IncrementDeaths(string clientUuid)
{
IncrementColumn(clientUuid, "total_deaths");
}
public void IncrementTasksCompleted(string clientUuid)
{
IncrementColumn(clientUuid, "tasks_completed");
}
public void IncrementBodiesReported(string clientUuid)
{
IncrementColumn(clientUuid, "bodies_reported");
}
public void IncrementEmergencyMeetings(string clientUuid)
{
IncrementColumn(clientUuid, "emergency_meetings_called");
}
public void IncrementTimesVotedOut(string clientUuid)
{
IncrementColumn(clientUuid, "times_voted_out");
}
public void IncrementSuccessfulVotes(string clientUuid)
{
IncrementColumn(clientUuid, "successful_votes");
}
public void IncrementCheatIncidents(string clientUuid)
{
IncrementColumn(clientUuid, "cheat_incidents");
}
private void IncrementColumn(string clientUuid, string column)
{
lock (_lock)
{
var sql = $@"
UPDATE player_stats
SET {column} = {column} + 1, last_seen_utc = @now
WHERE client_uuid = @uuid
";
using var cmd = _connection.CreateCommand();
cmd.CommandText = sql;
cmd.Parameters.AddWithValue("@uuid", clientUuid);
cmd.Parameters.AddWithValue("@now", DateTime.UtcNow.ToString("o"));
cmd.ExecuteNonQuery();
}
}
#endregion
#region Game end aktualizace
public void RecordGameEnd(GameEndStats stats)
{
lock (_lock)
{
using var transaction = _connection.BeginTransaction();
try
{
foreach (var player in stats.Players)
{
var sql = @"
UPDATE player_stats SET
total_games = total_games + 1,
games_as_crew = games_as_crew + @asCrew,
games_as_impostor = games_as_impostor + @asImpostor,
crew_wins = crew_wins + @crewWin,
impostor_wins = impostor_wins + @impostorWin,
total_playtime_seconds = total_playtime_seconds + @playtime,
last_seen_utc = @now
WHERE client_uuid = @uuid
";
using var cmd = _connection.CreateCommand();
cmd.CommandText = sql;
cmd.Parameters.AddWithValue("@uuid", player.ClientUuid);
cmd.Parameters.AddWithValue("@asCrew", player.WasCrew ? 1 : 0);
cmd.Parameters.AddWithValue("@asImpostor", player.WasImpostor ? 1 : 0);
cmd.Parameters.AddWithValue("@crewWin", player.WasCrew && stats.CrewWon ? 1 : 0);
cmd.Parameters.AddWithValue("@impostorWin", player.WasImpostor && !stats.CrewWon ? 1 : 0);
cmd.Parameters.AddWithValue("@playtime", player.PlaytimeSeconds);
cmd.Parameters.AddWithValue("@now", DateTime.UtcNow.ToString("o"));
cmd.ExecuteNonQuery();
}
transaction.Commit();
_logger.LogInformation("Game stats uloženy pro {Count} hráčů", stats.Players.Count);
}
catch
{
transaction.Rollback();
throw;
}
}
}
#endregion
private PlayerStats ReadPlayerStats(SqliteDataReader reader)
{
return new PlayerStats
{
ClientUuid = reader.GetString(reader.GetOrdinal("client_uuid")),
DisplayName = reader.IsDBNull(reader.GetOrdinal("display_name"))
? null : reader.GetString(reader.GetOrdinal("display_name")),
TotalGames = reader.GetInt32(reader.GetOrdinal("total_games")),
GamesAsCrew = reader.GetInt32(reader.GetOrdinal("games_as_crew")),
GamesAsImpostor = reader.GetInt32(reader.GetOrdinal("games_as_impostor")),
CrewWins = reader.GetInt32(reader.GetOrdinal("crew_wins")),
ImpostorWins = reader.GetInt32(reader.GetOrdinal("impostor_wins")),
TotalKills = reader.GetInt32(reader.GetOrdinal("total_kills")),
TotalDeaths = reader.GetInt32(reader.GetOrdinal("total_deaths")),
TasksCompleted = reader.GetInt32(reader.GetOrdinal("tasks_completed")),
BodiesReported = reader.GetInt32(reader.GetOrdinal("bodies_reported")),
EmergencyMeetingsCalled = reader.GetInt32(reader.GetOrdinal("emergency_meetings_called")),
TimesVotedOut = reader.GetInt32(reader.GetOrdinal("times_voted_out")),
SuccessfulVotes = reader.GetInt32(reader.GetOrdinal("successful_votes")),
TotalPlaytimeSeconds = reader.GetInt32(reader.GetOrdinal("total_playtime_seconds")),
CheatIncidents = reader.GetInt32(reader.GetOrdinal("cheat_incidents")),
LastSeenUtc = reader.IsDBNull(reader.GetOrdinal("last_seen_utc"))
? null : reader.GetString(reader.GetOrdinal("last_seen_utc")),
CreatedAtUtc = reader.IsDBNull(reader.GetOrdinal("created_at_utc"))
? null : reader.GetString(reader.GetOrdinal("created_at_utc"))
};
}
public void Dispose()
{
_connection.Close();
_connection.Dispose();
}
}
#region DTO
public class PlayerStats
{
public required string ClientUuid { get; set; }
public string? DisplayName { get; set; }
public int TotalGames { get; set; }
public int GamesAsCrew { get; set; }
public int GamesAsImpostor { get; set; }
public int CrewWins { get; set; }
public int ImpostorWins { get; set; }
public int TotalKills { get; set; }
public int TotalDeaths { get; set; }
public int TasksCompleted { get; set; }
public int BodiesReported { get; set; }
public int EmergencyMeetingsCalled { get; set; }
public int TimesVotedOut { get; set; }
public int SuccessfulVotes { get; set; }
public int TotalPlaytimeSeconds { get; set; }
public int CheatIncidents { get; set; }
public string? LastSeenUtc { get; set; }
public string? CreatedAtUtc { get; set; }
// Computed properties pro API
public double CrewWinRate => GamesAsCrew > 0 ? (double)CrewWins / GamesAsCrew : 0;
public double ImpostorWinRate => GamesAsImpostor > 0 ? (double)ImpostorWins / GamesAsImpostor : 0;
public double KillDeathRatio => TotalDeaths > 0 ? (double)TotalKills / TotalDeaths : TotalKills;
public double AverageTasksPerGame => TotalGames > 0 ? (double)TasksCompleted / TotalGames : 0;
}
public class GameEndStats
{
public bool CrewWon { get; set; }
public List<PlayerGameStats> Players { get; set; } = new();
}
public class PlayerGameStats
{
public required string ClientUuid { get; set; }
public bool WasCrew { get; set; }
public bool WasImpostor { get; set; }
public int PlaytimeSeconds { get; set; }
}
#endregion