Files

563 lines
27 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using RainOpsMini.Helpers.SquadGameLog;
using SqlSugar;
namespace RainOpsMini.Helpers.SquadGameLog
{
public class GameLogEntry
{
public DateTime? Timestamp { get; set; }
public string LogType { get; set; }
public object Data { get; set; }
public string RawContent { get; set; }
}
public class PlayerGameStats
{
public int Kills { get; set; }
public int Deaths { get; set; }
public int Wounds { get; set; }
public int Revives { get; set; }
}
public class LogSummary
{
public DateTime? LogTime { get; set; }
public string LogType { get; set; }
public string RawContent { get; set; }
}
public class LogStorage
{
private static readonly Lazy<LogStorage> _instance = new Lazy<LogStorage>(() => new LogStorage());
public static LogStorage Instance => _instance.Value;
private LogStorage() { }
public PlayerGameStats GetPlayerStats(string steamId, DateTime startTime)
{
if (string.IsNullOrEmpty(steamId)) return new PlayerGameStats();
var stats = new PlayerGameStats();
using (var db = DbHelper.GetInstance())
{
// Kills: Assuming SteamID in Log_Die is the Killer's SteamID
// Exclude Team Kills
stats.Kills = db.Queryable<Log_Die>()
.Where(x => x.SteamID == steamId && x.LogTime >= startTime && x.IsTK != true)
.Count();
// Deaths: Use VictimSteamID to count deaths
stats.Deaths = db.Queryable<Log_Die>()
.Where(x => x.VictimSteamID == steamId && x.LogTime >= startTime)
.Count();
// Wounds (Inflicted?): Log_Wound has SteamID (Attacker?)
// If Log_Wound SteamID is Attacker:
// stats.Wounds = db.Queryable<Log_Wound>()
// .Where(x => x.SteamID == steamId && x.LogTime >= startTime)
// .Count();
// Revives (Performed): Log_Revived has ReviverSteamID
stats.Revives = db.Queryable<Log_Revived>()
.Where(x => x.ReviverSteamID == steamId && x.LogTime >= startTime)
.Count();
}
return stats;
}
public void RegisterPlayer(string name, string steamId)
{
// Memory optimization: Do not store player mapping in memory
}
public void RegisterDieEvent(DateTime timestamp, string killerSteamId, string victimName)
{
// Memory optimization: Do not store die events in memory
}
public void RegisterKill(string killerSteamId) { }
public void RegisterDeath(string victimName) { }
public void AddLog(string logType, object data, string rawContent, DateTime? timestamp = null)
{
if (string.IsNullOrEmpty(rawContent)) return;
// Generate Hash ID from RawContent
string hashId = SquadLogHelper.GetMD5Hash(rawContent);
// Memory optimization: Removed in-memory deduplication (_processedLogs)
// We rely on database unique constraints (if any) or accept duplicates if file is re-processed.
// Usually FileTimestampAndSizeWatcher avoids re-processing.
// Set the Hash ID to the entity's Id property via Reflection
var idProperty = data.GetType().GetProperty("Id");
if (idProperty != null && idProperty.PropertyType == typeof(string))
{
idProperty.SetValue(data, hashId);
}
// Save to DB
using (var db = DbHelper.GetInstance())
{
try
{
switch (logType)
{
case "Disconnect": db.Insertable((Log_Disconnect)data).ExecuteCommand(); break;
case "MatchLost": db.Insertable((Log_MatchLost)data).ExecuteCommand(); break;
case "CreateSquad": db.Insertable((Log_CreateSquad)data).ExecuteCommand(); break;
case "NewPlayerConnect": db.Insertable((Log_NewPlayerConnect)data).ExecuteCommand(); break;
case "Wound": db.Insertable((Log_Wound)data).ExecuteCommand(); break;
case "Die": db.Insertable((Log_Die)data).ExecuteCommand(); break;
case "Revived": db.Insertable((Log_Revived)data).ExecuteCommand(); break;
case "Attack": db.Insertable((Log_Attack)data).ExecuteCommand(); break;
case "Tick": db.Insertable((Log_Tick)data).ExecuteCommand(); break;
case "BattleEnd": db.Insertable((Log_BattleEnd)data).ExecuteCommand(); break;
case "RemovedPlayer": db.Insertable((Log_RemovedPlayer)data).ExecuteCommand(); break;
case "SetNextLayer": db.Insertable((Log_SetNextLayer)data).ExecuteCommand(); break;
case "ChangeLayer": db.Insertable((Log_ChangeLayer)data).ExecuteCommand(); break;
}
}
catch (Exception ex)
{
// Ignore duplicate key errors (SQLite error code 19 or generic message containing "constraint")
if (!ex.Message.Contains("constraint") && !ex.Message.Contains("UNIQUE"))
{
RainOpsMini.Helpers.RainOpsLog.Log($"[Error] Failed to insert log {logType}: {ex.Message}");
}
}
}
}
private List<GameLogEntry> GetLogsForType<T>(ISqlSugarClient db, string typeName, string search, int page, int pageSize, out int totalCount) where T : class, new()
{
var query = db.Queryable<T>();
if (!string.IsNullOrEmpty(search))
{
query = query.Where("RawContent like @search", new { search = "%" + search + "%" });
}
totalCount = query.Count();
var list = query.OrderBy("LogTime DESC").ToPageList(page, pageSize);
return list.Select(x => {
dynamic d = x;
return new GameLogEntry
{
Timestamp = d.LogTime,
LogType = typeName,
Data = x,
RawContent = d.RawContent
};
}).ToList();
}
public IEnumerable<GameLogEntry> GetLogs(string type, string search, int page, int pageSize, out int totalCount)
{
using (var db = DbHelper.GetInstance())
{
if (!string.IsNullOrEmpty(type))
{
switch (type)
{
case "Disconnect": return GetLogsForType<Log_Disconnect>(db, type, search, page, pageSize, out totalCount);
case "MatchLost": return GetLogsForType<Log_MatchLost>(db, type, search, page, pageSize, out totalCount);
case "CreateSquad": return GetLogsForType<Log_CreateSquad>(db, type, search, page, pageSize, out totalCount);
case "NewPlayerConnect": return GetLogsForType<Log_NewPlayerConnect>(db, type, search, page, pageSize, out totalCount);
case "Wound": return GetLogsForType<Log_Wound>(db, type, search, page, pageSize, out totalCount);
case "Die": return GetLogsForType<Log_Die>(db, type, search, page, pageSize, out totalCount);
case "Revived": return GetLogsForType<Log_Revived>(db, type, search, page, pageSize, out totalCount);
case "Attack": return GetLogsForType<Log_Attack>(db, type, search, page, pageSize, out totalCount);
case "Tick": return GetLogsForType<Log_Tick>(db, type, search, page, pageSize, out totalCount);
case "BattleEnd": return GetLogsForType<Log_BattleEnd>(db, type, search, page, pageSize, out totalCount);
case "RemovedPlayer": return GetLogsForType<Log_RemovedPlayer>(db, type, search, page, pageSize, out totalCount);
case "SetNextLayer": return GetLogsForType<Log_SetNextLayer>(db, type, search, page, pageSize, out totalCount);
case "ChangeLayer": return GetLogsForType<Log_ChangeLayer>(db, type, search, page, pageSize, out totalCount);
default:
totalCount = 0;
return new List<GameLogEntry>();
}
}
else
{
var q1 = db.Queryable<Log_Disconnect>().Select(x => new LogSummary { LogTime = x.LogTime, LogType = "Disconnect", RawContent = x.RawContent });
var q2 = db.Queryable<Log_MatchLost>().Select(x => new LogSummary { LogTime = x.LogTime, LogType = "MatchLost", RawContent = x.RawContent });
var q3 = db.Queryable<Log_CreateSquad>().Select(x => new LogSummary { LogTime = x.LogTime, LogType = "CreateSquad", RawContent = x.RawContent });
var q4 = db.Queryable<Log_NewPlayerConnect>().Select(x => new LogSummary { LogTime = x.LogTime, LogType = "NewPlayerConnect", RawContent = x.RawContent });
var q5 = db.Queryable<Log_Wound>().Select(x => new LogSummary { LogTime = x.LogTime, LogType = "Wound", RawContent = x.RawContent });
var q6 = db.Queryable<Log_Die>().Select(x => new LogSummary { LogTime = x.LogTime, LogType = "Die", RawContent = x.RawContent });
var q7 = db.Queryable<Log_Revived>().Select(x => new LogSummary { LogTime = x.LogTime, LogType = "Revived", RawContent = x.RawContent });
var q8 = db.Queryable<Log_Attack>().Select(x => new LogSummary { LogTime = x.LogTime, LogType = "Attack", RawContent = x.RawContent });
var q9 = db.Queryable<Log_Tick>().Select(x => new LogSummary { LogTime = x.LogTime, LogType = "Tick", RawContent = x.RawContent });
var q10 = db.Queryable<Log_BattleEnd>().Select(x => new LogSummary { LogTime = x.LogTime, LogType = "BattleEnd", RawContent = x.RawContent });
var q11 = db.Queryable<Log_RemovedPlayer>().Select(x => new LogSummary { LogTime = x.LogTime, LogType = "RemovedPlayer", RawContent = x.RawContent });
var q12 = db.Queryable<Log_SetNextLayer>().Select(x => new LogSummary { LogTime = x.LogTime, LogType = "SetNextLayer", RawContent = x.RawContent });
var q13 = db.Queryable<Log_ChangeLayer>().Select(x => new LogSummary { LogTime = x.LogTime, LogType = "ChangeLayer", RawContent = x.RawContent });
var all = db.UnionAll(q1, q2, q3, q4, q5, q6, q7, q8, q9, q10, q11, q12, q13);
if (!string.IsNullOrEmpty(search))
{
all = all.Where(x => x.RawContent.Contains(search));
}
totalCount = all.Count();
var res = all.OrderByDescending(x => x.LogTime).ToPageList(page, pageSize);
return res.Select(x => new GameLogEntry
{
Timestamp = x.LogTime,
LogType = x.LogType,
RawContent = x.RawContent,
Data = null
}).ToList();
}
}
}
public Dictionary<string, int> GetLogStats()
{
using (var db = DbHelper.GetInstance())
{
var stats = new Dictionary<string, int>();
stats["Disconnect"] = db.Queryable<Log_Disconnect>().Count();
stats["MatchLost"] = db.Queryable<Log_MatchLost>().Count();
stats["CreateSquad"] = db.Queryable<Log_CreateSquad>().Count();
stats["NewPlayerConnect"] = db.Queryable<Log_NewPlayerConnect>().Count();
stats["Wound"] = db.Queryable<Log_Wound>().Count();
stats["Die"] = db.Queryable<Log_Die>().Count();
stats["Revived"] = db.Queryable<Log_Revived>().Count();
stats["Attack"] = db.Queryable<Log_Attack>().Count();
stats["Tick"] = db.Queryable<Log_Tick>().Count();
stats["BattleEnd"] = db.Queryable<Log_BattleEnd>().Count();
stats["RemovedPlayer"] = db.Queryable<Log_RemovedPlayer>().Count();
stats["SetNextLayer"] = db.Queryable<Log_SetNextLayer>().Count();
stats["ChangeLayer"] = db.Queryable<Log_ChangeLayer>().Count();
return stats;
}
}
public DateTime? GetLastSquadCreationTime(string steamId)
{
if (string.IsNullOrEmpty(steamId)) return null;
using (var db = DbHelper.GetInstance())
{
var log = db.Queryable<Log_CreateSquad>()
.Where(x => x.SteamID == steamId)
.OrderByDescending(x => x.LogTime)
.First();
return log?.LogTime;
}
}
public void SaveMatchStats(DateTime matchEndTime, string mapName)
{
try
{
using (var db = DbHelper.GetInstance())
{
// 1. Determine Match Start Time (Time of previous BattleEnd)
var lastMatchEnd = db.Queryable<Log_BattleEnd>()
.Where(x => x.LogTime < matchEndTime)
.OrderBy(x => x.LogTime, OrderByType.Desc)
.First();
var startTime = lastMatchEnd?.LogTime ?? matchEndTime.AddHours(-2); // Default to 2 hours if no previous match
// 2. Aggregate Stats from Logs
// Kills
var kills = db.Queryable<Log_Die>()
.Where(x => x.LogTime > startTime && x.LogTime <= matchEndTime && x.IsTK != true)
.GroupBy(x => x.AttackerSteamID)
.Select(x => new { SteamID = x.AttackerSteamID, Count = SqlFunc.AggregateCount(x.Id) })
.ToList();
// Deaths
var deaths = db.Queryable<Log_Die>()
.Where(x => x.LogTime > startTime && x.LogTime <= matchEndTime)
.GroupBy(x => x.VictimSteamID)
.Select(x => new { SteamID = x.VictimSteamID, Count = SqlFunc.AggregateCount(x.Id) })
.ToList();
// TeamKills
var teamKills = db.Queryable<Log_Die>()
.Where(x => x.LogTime > startTime && x.LogTime <= matchEndTime && x.IsTK == true)
.GroupBy(x => x.AttackerSteamID)
.Select(x => new { SteamID = x.AttackerSteamID, Count = SqlFunc.AggregateCount(x.Id) })
.ToList();
// Wounds (Knocks)
var wounds = db.Queryable<Log_Wound>()
.Where(x => x.LogTime > startTime && x.LogTime <= matchEndTime && x.IsTK != true)
.GroupBy(x => x.AttackerSteamID)
.Select(x => new { SteamID = x.AttackerSteamID, Count = SqlFunc.AggregateCount(x.Id) })
.ToList();
// Revives
var revives = db.Queryable<Log_Revived>()
.Where(x => x.LogTime > startTime && x.LogTime <= matchEndTime)
.GroupBy(x => x.ReviverSteamID)
.Select(x => new { SteamID = x.ReviverSteamID, Count = SqlFunc.AggregateCount(x.Id) })
.ToList();
// 3. Get Player Info (Name, Team) - Try to get from RconCache first, then fallback to logs
var playerStatsDict = new Dictionary<string, MatchPlayerStats>();
var currentPlayers = RainOpsMini.Program.RconCache?.Players?.ToList() ?? new List<RainOpsMini.Models.PlayerInfo>();
// Helper to get or create stats entry
MatchPlayerStats GetStats(string steamId)
{
if (string.IsNullOrEmpty(steamId)) return null;
if (!playerStatsDict.ContainsKey(steamId))
{
var pInfo = currentPlayers.FirstOrDefault(p => p.SteamId == steamId);
playerStatsDict[steamId] = new MatchPlayerStats
{
MatchId = $"{mapName}_{matchEndTime:yyyyMMddHHmmss}",
MatchDate = matchEndTime,
MapName = mapName,
SteamId = steamId,
PlayerName = pInfo?.Name ?? "Unknown",
TeamId = pInfo?.TeamId ?? 0,
Role = pInfo?.Role ?? "",
EosId = pInfo?.EosId ?? ""
};
}
return playerStatsDict[steamId];
}
// Populate Stats
foreach (var k in kills) { var s = GetStats(k.SteamID); if (s != null) s.Kills = k.Count; }
foreach (var d in deaths) { var s = GetStats(d.SteamID); if (s != null) s.Deaths = d.Count; }
foreach (var tk in teamKills) { var s = GetStats(tk.SteamID); if (s != null) s.TeamKills = tk.Count; }
foreach (var w in wounds) { var s = GetStats(w.SteamID); if (s != null) s.Wounds = w.Count; }
foreach (var r in revives) { var s = GetStats(r.SteamID); if (s != null) s.Revives = r.Count; }
// Fix missing names from logs if not in RconCache
var allSteamIds = playerStatsDict.Keys.ToList();
foreach (var steamId in allSteamIds)
{
var stats = playerStatsDict[steamId];
if (stats.PlayerName == "Unknown")
{
// Try to find name in logs
var nameLog = db.Queryable<Log_NewPlayerConnect>()
.Where(x => x.SteamID == steamId)
.OrderBy(x => x.LogTime, OrderByType.Desc)
.First();
if (nameLog == null)
{
// Try Die/Wound logs
var dieLog = db.Queryable<Log_Die>()
.Where(x => x.AttackerSteamID == steamId || x.VictimSteamID == steamId)
.OrderBy(x => x.LogTime, OrderByType.Desc)
.First();
if (dieLog != null)
{
stats.PlayerName = dieLog.AttackerSteamID == steamId ? dieLog.AttackerName : dieLog.VictimName;
}
}
else
{
// Log_NewPlayerConnect doesn't have Name usually (based on entity definition earlier)?
// Wait, Log_NewPlayerConnect definition: PlayerController, IP, EOSID, SteamID. No Name.
// Log_CreateSquad has UserName.
// Log_Die has PlayerName/AttackerName.
}
}
}
// Save to DB
if (playerStatsDict.Any())
{
db.Insertable(playerStatsDict.Values.ToList()).ExecuteCommand();
}
}
}
catch (Exception ex)
{
RainOpsMini.Helpers.RainOpsLog.Log($"Error saving match stats: {ex.Message}");
}
}
private ConcurrentDictionary<string, DateTime> _playerConnectTimes = new ConcurrentDictionary<string, DateTime>();
public void UpdatePlayerConnectTime(string steamId, DateTime time)
{
_playerConnectTimes.AddOrUpdate(steamId, time, (k, v) => time);
}
public DateTime? GetPlayerConnectTime(string steamId)
{
if (_playerConnectTimes.TryGetValue(steamId, out var time))
{
return time;
}
return null;
}
public void UpdatePlayerConnect(string steamId, string eosId, string name, DateTime time)
{
try
{
UpdatePlayerConnectTime(steamId, time);
using (var db = DbHelper.GetInstance())
{
var profile = db.Queryable<PlayerProfile>().Single(p => p.SteamID == steamId);
if (profile == null)
{
profile = new PlayerProfile
{
SteamID = steamId,
EOSID = eosId,
LastName = name ?? "Unknown",
FirstSeen = time,
LastSeen = time,
TotalPlaytimeSeconds = 0,
PlayStickiness = 0
};
db.Insertable(profile).ExecuteCommand();
}
else
{
profile.LastSeen = time;
profile.EOSID = eosId;
if (!string.IsNullOrEmpty(name)) profile.LastName = name;
db.Updateable(profile).ExecuteCommand();
}
// Start Session
var openSession = db.Queryable<PlayerSession>()
.Where(s => s.SteamID == steamId && s.EndTime == null)
.OrderBy(s => s.StartTime, OrderByType.Desc)
.First();
if (openSession != null)
{
openSession.EndTime = time;
openSession.DurationSeconds = (time - openSession.StartTime).TotalSeconds;
db.Updateable(openSession).ExecuteCommand();
}
var newSession = new PlayerSession
{
SteamID = steamId,
StartTime = time,
EndTime = null
};
db.Insertable(newSession).ExecuteCommand();
}
Task.Run(() => {
try {
var hours = RainOpsMini.Helpers.SteamHelper.GetSquadRunTime(steamId);
if (hours > 0) UpdateSquadPlaytime(steamId, hours);
} catch {}
});
}
catch (Exception ex)
{
RainOpsMini.Helpers.RainOpsLog.Log($"Error updating player connect: {ex.Message}");
}
}
public void UpdatePlayerDisconnect(string eosId, DateTime time)
{
try
{
using (var db = DbHelper.GetInstance())
{
var profile = db.Queryable<PlayerProfile>().First(p => p.EOSID == eosId);
if (profile == null) return;
var openSession = db.Queryable<PlayerSession>()
.Where(s => s.SteamID == profile.SteamID && s.EndTime == null)
.OrderBy(s => s.StartTime, OrderByType.Desc)
.First();
if (openSession != null)
{
openSession.EndTime = time;
openSession.DurationSeconds = (time - openSession.StartTime).TotalSeconds;
db.Updateable(openSession).ExecuteCommand();
profile.TotalPlaytimeSeconds += openSession.DurationSeconds;
double days = (time - profile.FirstSeen).TotalDays;
if (days < 1) days = 1;
profile.PlayStickiness = (profile.TotalPlaytimeSeconds / 3600.0) / days;
db.Updateable(profile).ExecuteCommand();
}
}
}
catch (Exception ex)
{
RainOpsMini.Helpers.RainOpsLog.Log($"Error updating player disconnect: {ex.Message}");
}
}
public void UpdateSquadPlaytime(string steamId, int hours)
{
using (var db = DbHelper.GetInstance())
{
db.Updateable<PlayerProfile>()
.SetColumns(p => p.SquadPlaytimeHours == hours)
.Where(p => p.SteamID == steamId)
.ExecuteCommand();
}
}
public void UpdatePlayerGameTimeInfo(string steamId, int hours, string name)
{
using (var db = DbHelper.GetInstance())
{
var existing = db.Queryable<PlayerProfile>().First(p => p.SteamID == steamId);
if (existing != null)
{
existing.SquadPlaytimeHours = hours;
if (!string.IsNullOrWhiteSpace(name))
{
existing.LastName = name;
}
db.Updateable(existing).ExecuteCommand();
}
else
{
var newProfile = new PlayerProfile
{
SteamID = steamId,
SquadPlaytimeHours = hours,
LastName = !string.IsNullOrWhiteSpace(name) ? name : "Unknown",
FirstSeen = DateTime.Now,
LastSeen = DateTime.Now,
TotalPlaytimeSeconds = 0,
PlayStickiness = 0,
EOSID = ""
};
db.Insertable(newProfile).ExecuteCommand();
}
}
}
public void UpdatePlayerName(string steamId, string name)
{
using (var db = DbHelper.GetInstance())
{
db.Updateable<PlayerProfile>()
.SetColumns(p => p.LastName == name)
.Where(p => p.SteamID == steamId)
.ExecuteCommand();
}
}
}
}