using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using TownOfUsStatsExporter.Models; namespace TownOfUsStatsExporter.Reflection; /// /// Main bridge for accessing TOU Mira data through reflection. /// Caches all reflection metadata for performance. /// public class TouMiraReflectionBridge { private Assembly? touAssembly; private readonly ReflectionCache cache = new(); /// /// Gets the TOU Mira version. /// public string? TouMiraVersion { get; private set; } /// /// Gets the compatibility status message. /// public string CompatibilityStatus { get; private set; } = "Unknown"; /// /// Initialize the reflection bridge by finding TOU Mira and caching reflection metadata. /// /// True if initialization was successful. public bool Initialize() { try { TownOfUsStatsPlugin.Logger.LogInfo("Initializing TOU Mira reflection bridge..."); // Find TOU Mira assembly - try multiple possible names var possibleNames = new[] { "TownOfUs", "TownOfUsMira", "TownOfUs.dll" }; foreach (var name in possibleNames) { touAssembly = AppDomain.CurrentDomain.GetAssemblies() .FirstOrDefault(a => a.GetName().Name == name || a.GetName().Name?.Contains(name) == true); if (touAssembly != null) { TownOfUsStatsPlugin.Logger.LogInfo($"Found TOU Mira assembly: {touAssembly.GetName().Name}"); break; } } if (touAssembly == null) { // Log all loaded assemblies for debugging var allAssemblies = string.Join(", ", AppDomain.CurrentDomain.GetAssemblies() .Select(a => a.GetName().Name) .Where(n => n != null && (n.Contains("Town") || n.Contains("Mira"))) .ToArray()); TownOfUsStatsPlugin.Logger.LogError($"TOU Mira assembly not found! Available assemblies with 'Town' or 'Mira': {allAssemblies}"); return false; } TouMiraVersion = touAssembly.GetName().Version?.ToString() ?? "Unknown"; TownOfUsStatsPlugin.Logger.LogInfo($"Found TOU Mira assembly v{TouMiraVersion}"); // Check version compatibility CompatibilityStatus = VersionCompatibility.CheckVersion(TouMiraVersion); if (CompatibilityStatus.StartsWith("Unsupported")) { TownOfUsStatsPlugin.Logger.LogWarning($"Version compatibility: {CompatibilityStatus}"); TownOfUsStatsPlugin.Logger.LogWarning("Plugin may not work correctly!"); } // Cache reflection metadata if (!CacheReflectionMetadata()) { TownOfUsStatsPlugin.Logger.LogError("Failed to cache reflection metadata"); return false; } TownOfUsStatsPlugin.Logger.LogInfo("Reflection bridge initialized successfully"); return true; } catch (Exception ex) { TownOfUsStatsPlugin.Logger.LogError($"Failed to initialize reflection bridge: {ex}"); return false; } } private bool CacheReflectionMetadata() { try { // Find and cache EndGamePatches type cache.EndGamePatchesType = touAssembly!.GetType("TownOfUs.Patches.EndGamePatches"); if (cache.EndGamePatchesType == null) { TownOfUsStatsPlugin.Logger.LogError("Type not found: TownOfUs.Patches.EndGamePatches"); return false; } // Find and cache EndGameData nested type cache.EndGameDataType = cache.EndGamePatchesType.GetNestedType("EndGameData", BindingFlags.Public); if (cache.EndGameDataType == null) { TownOfUsStatsPlugin.Logger.LogError("Type not found: EndGameData"); return false; } // Find and cache PlayerRecord nested type cache.PlayerRecordType = cache.EndGameDataType.GetNestedType("PlayerRecord", BindingFlags.Public); if (cache.PlayerRecordType == null) { TownOfUsStatsPlugin.Logger.LogError("Type not found: PlayerRecord"); return false; } // Cache PlayerRecords property cache.PlayerRecordsProperty = cache.EndGameDataType.GetProperty( "PlayerRecords", BindingFlags.Public | BindingFlags.Static); if (cache.PlayerRecordsProperty == null) { TownOfUsStatsPlugin.Logger.LogError("Property not found: EndGameData.PlayerRecords"); return false; } // Find and cache GameHistory type cache.GameHistoryType = touAssembly.GetType("TownOfUs.Modules.GameHistory"); if (cache.GameHistoryType == null) { TownOfUsStatsPlugin.Logger.LogError("Type not found: TownOfUs.Modules.GameHistory"); return false; } // Cache GameHistory fields (they are fields, not properties!) cache.PlayerStatsField = cache.GameHistoryType.GetField( "PlayerStats", BindingFlags.Public | BindingFlags.Static); cache.RoleHistoryField = cache.GameHistoryType.GetField( "RoleHistory", BindingFlags.Public | BindingFlags.Static); cache.KilledPlayersField = cache.GameHistoryType.GetField( "KilledPlayers", BindingFlags.Public | BindingFlags.Static); cache.WinningFactionField = cache.GameHistoryType.GetField( "WinningFaction", BindingFlags.Public | BindingFlags.Static); if (cache.PlayerStatsField == null || cache.RoleHistoryField == null) { TownOfUsStatsPlugin.Logger.LogError("Required GameHistory fields not found"); return false; } TownOfUsStatsPlugin.Logger.LogInfo("All required types and properties cached successfully"); return true; } catch (Exception ex) { TownOfUsStatsPlugin.Logger.LogError($"Error caching reflection metadata: {ex}"); return false; } } /// /// Get player records from EndGameData. /// /// List of player record data. public List GetPlayerRecords() { try { TownOfUsStatsPlugin.Logger.LogInfo("Getting player records from EndGameData..."); var playerRecords = cache.PlayerRecordsProperty!.GetValue(null); if (playerRecords == null) { TownOfUsStatsPlugin.Logger.LogWarning("PlayerRecords is null"); return new List(); } TownOfUsStatsPlugin.Logger.LogInfo($"PlayerRecords object retrieved: {playerRecords.GetType().Name}"); // Handle IL2CPP list var recordsList = IL2CPPHelper.ConvertToManagedList(playerRecords); TownOfUsStatsPlugin.Logger.LogInfo($"Converted to managed list: {recordsList.Count} items"); var result = new List(); foreach (var record in recordsList) { if (record == null) { continue; } result.Add(new PlayerRecordData { PlayerName = GetPropertyValue(record, "PlayerName") ?? "Unknown", RoleString = GetPropertyValue(record, "RoleString") ?? string.Empty, Winner = GetPropertyValue(record, "Winner"), PlayerId = GetPropertyValue(record, "PlayerId"), TeamString = GetTeamString(record), }); } TownOfUsStatsPlugin.Logger.LogInfo($"Retrieved {result.Count} player records"); return result; } catch (Exception ex) { TownOfUsStatsPlugin.Logger.LogError($"Error getting player records: {ex}"); return new List(); } } /// /// Get player statistics from GameHistory. /// /// Dictionary of player stats keyed by player ID. public Dictionary GetPlayerStats() { try { var playerStats = cache.PlayerStatsField!.GetValue(null); if (playerStats == null) { TownOfUsStatsPlugin.Logger.LogWarning("PlayerStats is null"); return new Dictionary(); } var statsDict = (IDictionary)playerStats; var result = new Dictionary(); foreach (DictionaryEntry entry in statsDict) { var playerId = (byte)entry.Key; var stats = entry.Value; if (stats == null) { continue; } result[playerId] = new PlayerStatsData { CorrectKills = GetPropertyValue(stats, "CorrectKills"), IncorrectKills = GetPropertyValue(stats, "IncorrectKills"), CorrectAssassinKills = GetPropertyValue(stats, "CorrectAssassinKills"), IncorrectAssassinKills = GetPropertyValue(stats, "IncorrectAssassinKills"), }; } TownOfUsStatsPlugin.Logger.LogInfo($"Retrieved stats for {result.Count} players"); return result; } catch (Exception ex) { TownOfUsStatsPlugin.Logger.LogError($"Error getting player stats: {ex}"); return new Dictionary(); } } /// /// Get role history from GameHistory. /// /// Dictionary of role lists keyed by player ID. public Dictionary> GetRoleHistory() { try { TownOfUsStatsPlugin.Logger.LogInfo("Getting role history from GameHistory..."); var roleHistory = cache.RoleHistoryField!.GetValue(null); if (roleHistory == null) { TownOfUsStatsPlugin.Logger.LogWarning("RoleHistory is null"); return new Dictionary>(); } var historyList = IL2CPPHelper.ConvertToManagedList(roleHistory); TownOfUsStatsPlugin.Logger.LogInfo($"RoleHistory has {historyList.Count} entries"); var result = new Dictionary>(); foreach (var entry in historyList) { if (entry == null) { TownOfUsStatsPlugin.Logger.LogWarning("Null entry in RoleHistory"); continue; } // Entry is KeyValuePair var kvpType = entry.GetType(); TownOfUsStatsPlugin.Logger.LogInfo($"Entry type: {kvpType.Name}"); var keyProp = kvpType.GetProperty("Key"); var valueProp = kvpType.GetProperty("Value"); if (keyProp == null || valueProp == null) { TownOfUsStatsPlugin.Logger.LogError($"Could not find Key or Value properties on {kvpType.Name}"); continue; } var playerId = (byte)keyProp.GetValue(entry)!; var roleBehaviour = valueProp.GetValue(entry); if (roleBehaviour == null) { TownOfUsStatsPlugin.Logger.LogWarning($"Null RoleBehaviour for player {playerId}"); continue; } TownOfUsStatsPlugin.Logger.LogInfo($"Player {playerId}: RoleBehaviour type = {roleBehaviour.GetType().Name}"); // Get role name from RoleBehaviour.GetRoleName() or fallback to type name string? roleName = null; var getRoleNameMethod = roleBehaviour.GetType().GetMethod("GetRoleName"); if (getRoleNameMethod != null) { roleName = getRoleNameMethod.Invoke(roleBehaviour, null) as string; } // Fallback: Extract role name from type name (e.g., "GrenadierRole" -> "Grenadier") if (string.IsNullOrEmpty(roleName)) { var typeName = roleBehaviour.GetType().Name; // Remove "Role" suffix if present if (typeName.EndsWith("Role")) { roleName = typeName.Substring(0, typeName.Length - 4); } // Remove "Tou" suffix if present (e.g., "EngineerTou" -> "Engineer", "TrackerTou" -> "Tracker") if (roleName.EndsWith("Tou")) { roleName = roleName.Substring(0, roleName.Length - 3); } TownOfUsStatsPlugin.Logger.LogInfo($"Player {playerId}: Using type name as role: {roleName}"); } if (string.IsNullOrEmpty(roleName)) { TownOfUsStatsPlugin.Logger.LogWarning($"Could not determine role name for player {playerId}"); continue; } // Skip generic RoleBehaviour (not a real role) if (roleName == "RoleBehaviour") { TownOfUsStatsPlugin.Logger.LogInfo($"Skipping generic RoleBehaviour for player {playerId}"); continue; } TownOfUsStatsPlugin.Logger.LogInfo($"Player {playerId}: Role = {roleName}"); // Skip ghost roles if (roleName.Contains("Ghost")) { TownOfUsStatsPlugin.Logger.LogInfo($"Skipping ghost role: {roleName}"); continue; } // Strip color tags roleName = StripColorTags(roleName); if (!result.ContainsKey(playerId)) { result[playerId] = new List(); } result[playerId].Add(roleName); } TownOfUsStatsPlugin.Logger.LogInfo($"Retrieved role history for {result.Count} players"); return result; } catch (Exception ex) { TownOfUsStatsPlugin.Logger.LogError($"Error getting role history: {ex}"); return new Dictionary>(); } } /// /// Get killed players list. /// /// List of killed player data. public List GetKilledPlayers() { try { var killedPlayers = cache.KilledPlayersField?.GetValue(null); if (killedPlayers == null) { return new List(); } var killedList = IL2CPPHelper.ConvertToManagedList(killedPlayers); var result = new List(); foreach (var killed in killedList) { if (killed == null) { continue; } result.Add(new KilledPlayerData { KillerId = GetPropertyValue(killed, "KillerId"), VictimId = GetPropertyValue(killed, "VictimId"), }); } return result; } catch (Exception ex) { TownOfUsStatsPlugin.Logger.LogError($"Error getting killed players: {ex}"); return new List(); } } /// /// Get winning faction string. /// /// The winning faction name. public string GetWinningFaction() { try { if (cache.WinningFactionField == null) { return string.Empty; } var winningFaction = cache.WinningFactionField.GetValue(null); return winningFaction as string ?? string.Empty; } catch (Exception ex) { TownOfUsStatsPlugin.Logger.LogError($"Error getting winning faction: {ex}"); return string.Empty; } } /// /// Get modifiers for a player. /// /// The player ID. /// List of modifier names. public List GetPlayerModifiers(byte playerId) { try { // Find PlayerControl var player = PlayerControl.AllPlayerControls.ToArray() .FirstOrDefault(p => p.PlayerId == playerId); if (player == null) { return new List(); } // Get modifiers through reflection var getModifiersMethod = player.GetType().GetMethods() .FirstOrDefault(m => m.Name == "GetModifiers" && m.IsGenericMethod); if (getModifiersMethod == null) { return new List(); } // Find GameModifier type var gameModifierType = touAssembly!.GetType("MiraAPI.Modifiers.GameModifier"); if (gameModifierType == null) { return new List(); } var genericMethod = getModifiersMethod.MakeGenericMethod(gameModifierType); var modifiers = genericMethod.Invoke(player, null); if (modifiers == null) { return new List(); } var modifiersList = IL2CPPHelper.ConvertToManagedList(modifiers); var result = new List(); foreach (var modifier in modifiersList) { if (modifier == null) { continue; } var modifierName = GetPropertyValue(modifier, "ModifierName"); if (!string.IsNullOrEmpty(modifierName)) { result.Add(modifierName); } } return result; } catch (Exception ex) { TownOfUsStatsPlugin.Logger.LogError($"Error getting modifiers for player {playerId}: {ex}"); return new List(); } } private string GetTeamString(object record) { try { // Get Team property (ModdedRoleTeams enum) var teamProperty = record.GetType().GetProperty("Team"); if (teamProperty == null) { return "Unknown"; } var team = teamProperty.GetValue(record); if (team == null) { return "Unknown"; } return team.ToString() ?? "Unknown"; } catch { return "Unknown"; } } private T GetPropertyValue(object obj, string propertyName) { try { var property = obj?.GetType().GetProperty(propertyName); if (property == null) { return default!; } var value = property.GetValue(obj); if (value == null) { return default!; } return (T)value; } catch { return default!; } } private string StripColorTags(string text) { if (string.IsNullOrEmpty(text)) { return text; } // Remove all Unity/HTML tags // Pattern matches: , , , text = Regex.Replace(text, @"<[^>]+>", string.Empty); return text.Trim(); } }