Server
This commit is contained in:
327
StatsDb.cs
Normal file
327
StatsDb.cs
Normal file
@@ -0,0 +1,327 @@
|
||||
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
|
||||
Reference in New Issue
Block a user