328 lines
12 KiB
C#
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
|