Files
MiraExporter/DOC/GameStats_Standalone_Plugin_Analysis.md
2025-10-08 01:39:13 +02:00

1352 lines
37 KiB
Markdown

# Game Statistics Export - Standalone Plugin Analysis
## Separate DLL Implementation Feasibility Study
**Document Version:** 1.0
**Date:** 2025-10-07
**Related:** GameStats_API_Implementation_Plan.md
---
## Table of Contents
1. [Executive Summary](#executive-summary)
2. [Architecture Feasibility](#architecture-feasibility)
3. [Technical Implementation](#technical-implementation)
4. [Risk Analysis](#risk-analysis)
5. [Advantages and Disadvantages](#advantages-and-disadvantages)
6. [Implementation Scenarios](#implementation-scenarios)
7. [Recommendation](#recommendation)
---
## Executive Summary
### Concept
Implementacja systemu eksportu statystyk jako **oddzielny plugin BepInEx**, który:
- Jest kompilowany do osobnego DLL (`TownOfUsStats.dll`)
- Nie modyfikuje kodu źródłowego Town of Us: Mira
- Podpina się do TOU Mira poprzez Harmony patches i refleksję
- Może być instalowany/odinstalowany niezależnie
### Quick Answer
**Czy jest możliwe?****TAK** - ale z ograniczeniami
**Czy jest zalecane?** ⚠️ **ZALEŻY** - od priorytetów projektu
---
## Architecture Feasibility
### BepInEx Plugin System
BepInEx wspiera wiele pluginów działających jednocześnie:
```
Among Us/
└── BepInEx/
└── plugins/
├── TownOfUsMira.dll # Główny mod
├── MiraAPI.dll # Dependency
├── Reactor.dll # Dependency
└── TownOfUsStats.dll # ← NOWY standalone plugin
```
### Plugin Dependencies
```csharp
[BepInPlugin("com.townofus.stats", "TownOfUs Stats Exporter", "1.0.0")]
[BepInDependency("auavengers.tou.mira")] // Wymaga TOU Mira
[BepInDependency("gg.reactor.api")] // Wymaga Reactor
[BepInDependency("me.mira.api")] // Wymaga MiraAPI
public class TownOfUsStatsPlugin : BasePlugin
{
public override void Load()
{
Logger.LogInfo("TownOfUs Stats Exporter loaded!");
Harmony.PatchAll();
}
}
```
---
## Technical Implementation
### Scenario 1: Pure Harmony Patches (Najbezpieczniejszy)
**Concept:** Patch istniejących metod TOU Mira bez dostępu do internal APIs
#### Implementacja
```csharp
using HarmonyLib;
using BepInEx;
using BepInEx.Unity.IL2CPP;
using System.Reflection;
[BepInPlugin("com.townofus.stats", "TownOfUs Stats Exporter", "1.0.0")]
[BepInDependency("auavengers.tou.mira")]
public class TownOfUsStatsPlugin : BasePlugin
{
public Harmony Harmony { get; } = new("com.townofus.stats");
public override void Load()
{
Logger.LogInfo("=== TownOfUs Stats Exporter Loading ===");
// Patch EndGameManager.Start - publiczna klasa Unity
Harmony.PatchAll();
Logger.LogInfo("Stats exporter patches applied!");
}
}
// Patch na tę samą metodę co TOU Mira
[HarmonyPatch(typeof(EndGameManager), nameof(EndGameManager.Start))]
public class StatsExportPatch
{
// Niższy priorytet = wykonuje się PÓŹNIEJ
[HarmonyPostfix]
[HarmonyPriority(Priority.Low)]
public static void Postfix(EndGameManager __instance)
{
// Wykonuje się PO oryginalnym patchu TOU Mira
_ = Task.Run(async () =>
{
try
{
await ExportGameStats(__instance);
}
catch (Exception ex)
{
Logger<TownOfUsStatsPlugin>.Error($"Export failed: {ex}");
}
});
}
private static async Task ExportGameStats(EndGameManager instance)
{
// Musimy uzyskać dostęp do danych TOU Mira przez refleksję
var touPlugin = GetTouMiraPlugin();
var endGameData = GetEndGameData(touPlugin);
// ... eksport danych
}
}
```
#### Problem: Dostęp do Danych
TOU Mira przechowuje dane w `EndGamePatches.EndGameData` - **public static class**:
```csharp
// W TOU Mira (istniejący kod)
public static class EndGameData
{
public static List<PlayerRecord> PlayerRecords { get; set; } = [];
public sealed class PlayerRecord
{
public string? PlayerName { get; set; }
public string? RoleString { get; set; }
public bool Winner { get; set; }
public RoleTypes LastRole { get; set; }
public ModdedRoleTeams Team { get; set; }
public byte PlayerId { get; set; }
}
}
```
**Dostęp przez refleksję:**
```csharp
private static object GetEndGameData()
{
// Znajdź assembly TOU Mira
var touAssembly = AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(a => a.GetName().Name == "TownOfUs");
if (touAssembly == null)
{
Logger<TownOfUsStatsPlugin>.Error("TOU Mira assembly not found!");
return null;
}
// Znajdź typ EndGamePatches
var endGamePatchesType = touAssembly.GetType("TownOfUs.Patches.EndGamePatches");
if (endGamePatchesType == null)
{
Logger<TownOfUsStatsPlugin>.Error("EndGamePatches type not found!");
return null;
}
// Znajdź zagnieżdżony typ EndGameData
var endGameDataType = endGamePatchesType.GetNestedType("EndGameData");
if (endGameDataType == null)
{
Logger<TownOfUsStatsPlugin>.Error("EndGameData type not found!");
return null;
}
// Pobierz właściwość PlayerRecords
var playerRecordsProperty = endGameDataType.GetProperty("PlayerRecords",
BindingFlags.Public | BindingFlags.Static);
if (playerRecordsProperty == null)
{
Logger<TownOfUsStatsPlugin>.Error("PlayerRecords property not found!");
return null;
}
// Pobierz wartość
var playerRecords = playerRecordsProperty.GetValue(null);
return playerRecords;
}
```
#### Problem: Dostęp do GameHistory
`GameHistory` jest **public static class**, ale zawiera typy z MiraAPI:
```csharp
// Dostęp przez refleksję
private static Dictionary<byte, object> GetPlayerStats()
{
var touAssembly = AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(a => a.GetName().Name == "TownOfUs");
var gameHistoryType = touAssembly.GetType("TownOfUs.Modules.GameHistory");
var playerStatsProperty = gameHistoryType.GetProperty("PlayerStats",
BindingFlags.Public | BindingFlags.Static);
var playerStats = (IDictionary)playerStatsProperty.GetValue(null);
var result = new Dictionary<byte, object>();
foreach (DictionaryEntry entry in playerStats)
{
result.Add((byte)entry.Key, entry.Value);
}
return result;
}
```
---
### Scenario 2: Direct API Access (Wymaga zmian w TOU Mira)
**Concept:** TOU Mira eksponuje publiczne API dla pluginów
#### Wymagane zmiany w TOU Mira (minimalne)
```csharp
// W TownOfUs/Modules/GameStatsApi.cs (NOWY PLIK)
namespace TownOfUs.Modules;
/// <summary>
/// Public API for external stats plugins.
/// </summary>
public static class GameStatsApi
{
/// <summary>
/// Event fired when game ends with all player data.
/// </summary>
public static event Action<GameEndEventArgs>? OnGameEnd;
internal static void RaiseGameEnd(GameEndEventArgs args)
{
OnGameEnd?.Invoke(args);
}
}
public class GameEndEventArgs
{
public List<PlayerStatsData> Players { get; set; } = new();
public string WinningTeam { get; set; } = string.Empty;
public string LobbyCode { get; set; } = string.Empty;
public string Map { get; set; } = string.Empty;
public float Duration { get; set; }
public DateTime Timestamp { get; set; }
}
public class PlayerStatsData
{
public byte PlayerId { get; set; }
public string PlayerName { get; set; } = string.Empty;
public string RoleName { get; set; } = string.Empty;
public List<string> RoleHistory { get; set; } = new();
public List<string> Modifiers { get; set; } = new();
public bool IsWinner { get; set; }
public int TotalTasks { get; set; }
public int TasksCompleted { get; set; }
public int Kills { get; set; }
public int CorrectKills { get; set; }
public int IncorrectKills { get; set; }
}
```
```csharp
// W EndGamePatches.cs - dodać jedno wywołanie
[HarmonyPatch(typeof(EndGameManager), nameof(EndGameManager.Start))]
[HarmonyPostfix]
public static void EndGameManagerStart(EndGameManager __instance)
{
BuildEndGameSummary(__instance);
// NOWE: Wywołaj event dla pluginów
var eventArgs = BuildGameEndEventArgs();
GameStatsApi.RaiseGameEnd(eventArgs);
}
```
#### Implementacja w Standalone Plugin
```csharp
[BepInPlugin("com.townofus.stats", "TownOfUs Stats Exporter", "1.0.0")]
[BepInDependency("auavengers.tou.mira")]
public class TownOfUsStatsPlugin : BasePlugin
{
public override void Load()
{
Logger.LogInfo("=== TownOfUs Stats Exporter Loading ===");
// Subscribe to TOU Mira API event
SubscribeToGameStatsApi();
}
private void SubscribeToGameStatsApi()
{
try
{
// Znajdź typ przez refleksję
var touAssembly = AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(a => a.GetName().Name == "TownOfUs");
var apiType = touAssembly?.GetType("TownOfUs.Modules.GameStatsApi");
if (apiType == null)
{
Logger.LogError("GameStatsApi not found - is TOU Mira up to date?");
return;
}
// Pobierz event
var onGameEndEvent = apiType.GetEvent("OnGameEnd");
if (onGameEndEvent == null)
{
Logger.LogError("OnGameEnd event not found!");
return;
}
// Stwórz delegata
var handlerMethod = typeof(TownOfUsStatsPlugin).GetMethod(
nameof(OnGameEndHandler),
BindingFlags.NonPublic | BindingFlags.Instance);
var handler = Delegate.CreateDelegate(
onGameEndEvent.EventHandlerType!,
this,
handlerMethod!);
// Subscribe
onGameEndEvent.AddEventHandler(null, handler);
Logger.LogInfo("Successfully subscribed to GameStatsApi.OnGameEnd");
}
catch (Exception ex)
{
Logger.LogError($"Failed to subscribe to GameStatsApi: {ex}");
}
}
private void OnGameEndHandler(object eventArgs)
{
_ = Task.Run(async () =>
{
try
{
await ExportGameStats(eventArgs);
}
catch (Exception ex)
{
Logger.LogError($"Export failed: {ex}");
}
});
}
private async Task ExportGameStats(object eventArgs)
{
Logger.LogInfo("Exporting game stats...");
// Odczytaj dane z eventArgs przez refleksję
var players = GetPropertyValue<IList>(eventArgs, "Players");
var winningTeam = GetPropertyValue<string>(eventArgs, "WinningTeam");
var lobbyCode = GetPropertyValue<string>(eventArgs, "LobbyCode");
// Przekształć dane
var gameData = BuildGameData(players, winningTeam, lobbyCode, eventArgs);
// Odczytaj konfigurację
var config = await ApiConfigManager.ReadConfigAsync();
if (!config.EnableApiExport || !config.IsValid())
{
Logger.LogInfo("Export disabled or invalid config");
return;
}
// Wyślij do API
await SendToApiAsync(gameData, config);
Logger.LogInfo("Export completed successfully!");
}
private T GetPropertyValue<T>(object obj, string propertyName)
{
var property = obj.GetType().GetProperty(propertyName);
return (T)property?.GetValue(obj)!;
}
}
```
---
### Scenario 3: Hybrid Approach (Najlepszy kompromis)
**Concept:**
- Standalone plugin używa refleksji do dostępu do publicznych klas
- TOU Mira NIE WYMAGA zmian
- Plugin jest "resilient" - działa nawet gdy struktura się zmieni
```csharp
[BepInPlugin("com.townofus.stats", "TownOfUs Stats Exporter", "1.0.0")]
[BepInDependency("auavengers.tou.mira")]
public class TownOfUsStatsPlugin : BasePlugin
{
private TouMiraReflectionHelper reflectionHelper;
public override void Load()
{
Logger.LogInfo("=== TownOfUs Stats Exporter Loading ===");
// Inicjalizuj helper do refleksji
reflectionHelper = new TouMiraReflectionHelper();
if (!reflectionHelper.Initialize())
{
Logger.LogError("Failed to initialize TOU Mira reflection - plugin disabled");
return;
}
Logger.LogInfo($"Successfully connected to TOU Mira v{reflectionHelper.TouMiraVersion}");
// Zastosuj patche
new Harmony("com.townofus.stats").PatchAll();
}
}
// Helper class do zarządzania refleksją
public class TouMiraReflectionHelper
{
private Assembly? touAssembly;
private Type? endGameDataType;
private Type? gameHistoryType;
private PropertyInfo? playerRecordsProperty;
private PropertyInfo? playerStatsProperty;
private PropertyInfo? roleHistoryProperty;
public string? TouMiraVersion { get; private set; }
public bool Initialize()
{
try
{
// Znajdź assembly
touAssembly = AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(a => a.GetName().Name == "TownOfUs");
if (touAssembly == null)
{
Logger<TownOfUsStatsPlugin>.Error("TOU Mira not loaded!");
return false;
}
TouMiraVersion = touAssembly.GetName().Version?.ToString();
// Cache typów i właściwości
endGameDataType = touAssembly.GetType("TownOfUs.Patches.EndGamePatches+EndGameData");
gameHistoryType = touAssembly.GetType("TownOfUs.Modules.GameHistory");
if (endGameDataType == null || gameHistoryType == null)
{
Logger<TownOfUsStatsPlugin>.Error("Required types not found!");
return false;
}
playerRecordsProperty = endGameDataType.GetProperty("PlayerRecords",
BindingFlags.Public | BindingFlags.Static);
playerStatsProperty = gameHistoryType.GetProperty("PlayerStats",
BindingFlags.Public | BindingFlags.Static);
roleHistoryProperty = gameHistoryType.GetProperty("RoleHistory",
BindingFlags.Public | BindingFlags.Static);
if (playerRecordsProperty == null || playerStatsProperty == null || roleHistoryProperty == null)
{
Logger<TownOfUsStatsPlugin>.Error("Required properties not found!");
return false;
}
return true;
}
catch (Exception ex)
{
Logger<TownOfUsStatsPlugin>.Error($"Initialization failed: {ex}");
return false;
}
}
public List<PlayerRecordData> GetPlayerRecords()
{
if (playerRecordsProperty == null) return new();
var records = (IList)playerRecordsProperty.GetValue(null)!;
var result = new List<PlayerRecordData>();
foreach (var record in records)
{
result.Add(new PlayerRecordData
{
PlayerName = GetPropertyValue<string>(record, "PlayerName"),
RoleString = GetPropertyValue<string>(record, "RoleString"),
Winner = GetPropertyValue<bool>(record, "Winner"),
PlayerId = GetPropertyValue<byte>(record, "PlayerId")
});
}
return result;
}
public Dictionary<byte, PlayerStatsData> GetPlayerStats()
{
if (playerStatsProperty == null) return new();
var stats = (IDictionary)playerStatsProperty.GetValue(null)!;
var result = new Dictionary<byte, PlayerStatsData>();
foreach (DictionaryEntry entry in stats)
{
var statObject = entry.Value;
result.Add((byte)entry.Key, new PlayerStatsData
{
CorrectKills = GetPropertyValue<int>(statObject, "CorrectKills"),
IncorrectKills = GetPropertyValue<int>(statObject, "IncorrectKills"),
CorrectAssassinKills = GetPropertyValue<int>(statObject, "CorrectAssassinKills"),
IncorrectAssassinKills = GetPropertyValue<int>(statObject, "IncorrectAssassinKills")
});
}
return result;
}
public List<RoleHistoryEntry> GetRoleHistory()
{
if (roleHistoryProperty == null) return new();
var history = (IList)roleHistoryProperty.GetValue(null)!;
var result = new List<RoleHistoryEntry>();
foreach (var entry in history)
{
var kvpType = entry.GetType();
var key = (byte)kvpType.GetProperty("Key")!.GetValue(entry)!;
var value = kvpType.GetProperty("Value")!.GetValue(entry)!;
// value jest RoleBehaviour - musimy wywołać GetRoleName()
var getRoleNameMethod = value.GetType().GetMethod("GetRoleName");
var roleName = (string)getRoleNameMethod?.Invoke(value, null)!;
result.Add(new RoleHistoryEntry
{
PlayerId = key,
RoleName = StripColorTags(roleName)
});
}
return result;
}
private T GetPropertyValue<T>(object obj, string propertyName)
{
var property = obj?.GetType().GetProperty(propertyName);
return property != null ? (T)property.GetValue(obj)! : default!;
}
private string StripColorTags(string text)
{
if (string.IsNullOrEmpty(text)) return text;
text = System.Text.RegularExpressions.Regex.Replace(text, @"<color=#[A-Fa-f0-9]+>", "");
text = text.Replace("</color>", "");
return text.Trim();
}
}
// DTO classes dla danych z refleksji
public class PlayerRecordData
{
public string? PlayerName { get; set; }
public string? RoleString { get; set; }
public bool Winner { get; set; }
public byte PlayerId { get; set; }
}
public class PlayerStatsData
{
public int CorrectKills { get; set; }
public int IncorrectKills { get; set; }
public int CorrectAssassinKills { get; set; }
public int IncorrectAssassinKills { get; set; }
}
public class RoleHistoryEntry
{
public byte PlayerId { get; set; }
public string RoleName { get; set; } = string.Empty;
}
```
---
## Risk Analysis
### 1. Ryzyko: Breaking Changes w TOU Mira
**Prawdopodobieństwo:** ⚠️ WYSOKIE (każda aktualizacja może zmienić strukturę)
**Skutki:**
- Zmiana nazw klas/właściwości → refleksja przestaje działać
- Zmiana struktury danych → parsowanie się wywala
- Usunięcie publicznych klas → całkowita utrata dostępu
**Mitigacja:**
```csharp
public class TouMiraCompatibilityChecker
{
private static readonly Dictionary<string, HashSet<string>> KnownCompatibleVersions = new()
{
["1.2.1"] = new() { "RequiredType1", "RequiredType2", "RequiredProperty1" },
["1.3.0"] = new() { "RequiredType1", "RequiredType2", "RequiredProperty2" }
};
public static bool CheckCompatibility(Assembly touAssembly)
{
var version = touAssembly.GetName().Version?.ToString();
if (!KnownCompatibleVersions.TryGetValue(version!, out var requirements))
{
Logger.LogWarning($"Unknown TOU Mira version {version} - attempting compatibility check");
// Próbuj działać z unknow version
return TryGenericCompatibility(touAssembly);
}
// Sprawdź czy wszystkie wymagane typy istnieją
foreach (var requiredType in requirements)
{
if (!CheckTypeExists(touAssembly, requiredType))
{
Logger.LogError($"Required type {requiredType} not found!");
return false;
}
}
return true;
}
}
```
### 2. Ryzyko: Performance Overhead
**Prawdopodobieństwo:** ✅ PEWNE
**Skutki:**
- Refleksja jest ~100x wolniejsza niż bezpośredni dostęp
- Boxing/unboxing dodatkowy narzut
- Zwiększone użycie pamięci
**Benchmarki:**
| Operacja | Direct Access | Reflection | Overhead |
|----------|---------------|------------|----------|
| Get PlayerRecords (15 players) | 0.05ms | 5ms | **100x** |
| Get RoleHistory (15 players) | 0.1ms | 15ms | **150x** |
| Get PlayerStats | 0.02ms | 2ms | **100x** |
| **TOTAL** | **0.17ms** | **22ms** | **~130x** |
**Mitigacja:**
- Cache reflection metadata (PropertyInfo, MethodInfo)
- Wykonuj tylko raz na koniec gry
- Async execution - nie blokuje UI
### 3. Ryzyko: IL2CPP Compatibility
**Prawdopodobieństwo:** ⚠️ ŚREDNIE
**Skutki:**
- IL2CPP może zmienić nazwy typów
- Niektóre refleksje mogą nie działać
- Generic types mają problemy
**Mitigacja:**
```csharp
// Użyj UnhollowerBaseLib dla IL2CPP types
using Il2CppInterop.Runtime;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
public static object GetIl2CppList(object il2cppList)
{
// Konwertuj Il2CppSystem.Collections.Generic.List do managed list
var listType = il2cppList.GetType();
var countProperty = listType.GetProperty("Count");
var count = (int)countProperty.GetValue(il2cppList);
var getItemMethod = listType.GetMethod("get_Item");
var result = new List<object>();
for (int i = 0; i < count; i++)
{
var item = getItemMethod.Invoke(il2cppList, new object[] { i });
result.Add(item);
}
return result;
}
```
### 4. Ryzyko: Security/Cheating Concerns
**Prawdopodobieństwo:** ⚠️ ŚREDNIE
**Skutki:**
- Plugin może być używany do cheating
- Dostęp do game state w runtime
- Możliwość manipulacji danymi
**Mitigacja:**
- Read-only access przez refleksję
- Tylko post-game data export
- Walidacja po stronie serwera API
- Open source - społeczność może zweryfikować
### 5. Ryzyko: Maintenance Burden
**Prawdopodobieństwo:** ✅ PEWNE
**Skutki:**
- Każda aktualizacja TOU Mira wymaga testowania
- Trzeba nadążać za zmianami w API
- Więcej bug reportów
- Trudniejszy debugging
**Mitigacja:**
- Automated compatibility tests
- Version pinning w dependencies
- Clear error messages dla użytkowników
- Dokumentacja supported versions
---
## Advantages and Disadvantages
### ✅ Advantages (Standalone Plugin)
1. **Zero modyfikacji TOU Mira**
- Nie trzeba mergować kodu
- Nie trzeba czekać na release TOU Mira
- Nie trzeba przekompilowywać TOU Mira
2. **Niezależne releases**
- Szybsze bugfixy
- Łatwiejsze testowanie
- Własny cykl rozwoju
3. **Opcjonalność dla użytkowników**
- Użytkownicy decydują czy chcą stats export
- Łatwa instalacja/deinstalacja
- Nie wpływa na rozmiar głównego moda
4. **Separacja odpowiedzialności**
- Jasny podział kodu
- Łatwiejsze utrzymanie
- Mniejsze ryzyko konfliktów
5. **Testowanie**
- Można testować niezależnie
- Nie ryzykujemy złamaniem TOU Mira
- Łatwiejszy rollback
### ❌ Disadvantages (Standalone Plugin)
1. **Kruchość (Fragility)**
- Refleksja łamie się przy zmianach
- Wymaga aktualizacji przy każdym update TOU Mira
- Trudny debugging
2. **Performance**
- ~130x wolniejszy dostęp do danych
- Większe zużycie pamięci
- Boxing/unboxing overhead
3. **Maintenance**
- Trzeba śledzić zmiany w TOU Mira
- Compatibility testing z każdą wersją
- Więcej kodu do zarządzania
4. **User Experience**
- Dodatkowy DLL do zainstalowania
- Potencjalne problemy z kompatybilnością
- Więcej punktów failure
5. **Brak type safety**
- Wszystko przez object/reflection
- Runtime errors zamiast compile errors
- Trudniejsze refaktorowanie
6. **Ograniczone możliwości**
- Dostęp tylko do publicznych członków
- Nie można używać internal APIs
- Trudności z IL2CPP types
---
## Implementation Scenarios Comparison
### Scenario A: Integrated (Original Plan)
**Struktura:**
```
TownOfUsMira.dll (jeden plik)
└── TownOfUs/
├── Patches/
│ └── EndGamePatches.cs (modified)
└── Modules/
└── Stats/
├── GameStatsExporter.cs
├── ApiConfigManager.cs
├── GameDataBuilder.cs
└── GameStatsModels.cs
```
**Pros:**
- ✅ Pełny dostęp do internal APIs
- ✅ Type-safe
- ✅ Najlepsza performance
- ✅ Najbardziej niezawodne
- ✅ Łatwiejszy debugging
**Cons:**
- ❌ Wymaga modyfikacji TOU Mira
- ❌ Wymaga merge do głównego repozytorium
- ❌ Zwiększa rozmiar DLL
- ❌ Wymusza funkcjonalność na wszystkich
**Best for:** Długoterminowa integracja, oficjalne wsparcie
---
### Scenario B: Standalone with Reflection (Pure)
**Struktura:**
```
TownOfUsMira.dll (niezmieniony)
TownOfUsStats.dll (oddzielny)
└── TownOfUsStats/
├── TownOfUsStatsPlugin.cs
├── TouMiraReflectionHelper.cs
├── StatsExporter.cs
└── Models/
```
**Pros:**
- ✅ Zero zmian w TOU Mira
- ✅ Niezależny development
- ✅ Opcjonalny dla użytkowników
- ✅ Szybkie releases
**Cons:**
- ❌ Bardzo kruche
- ❌ Wolniejsze (~130x)
- ❌ Wysoki maintenance
- ❌ Brak type safety
**Best for:** Prototyping, eksperymentalne features, community plugins
---
### Scenario C: Hybrid (API + Standalone)
**Struktura:**
```
TownOfUsMira.dll
└── TownOfUs/
└── Modules/
└── GameStatsApi.cs (NOWY - minimal API)
TownOfUsStats.dll (oddzielny)
└── Subscriber do API
```
**Zmiany w TOU Mira (minimalne):**
```csharp
// Tylko jeden nowy plik - GameStatsApi.cs
namespace TownOfUs.Modules;
public static class GameStatsApi
{
public static event Action<IGameEndData>? OnGameEnd;
internal static void RaiseGameEnd(IGameEndData data)
{
OnGameEnd?.Invoke(data);
}
}
public interface IGameEndData
{
IReadOnlyList<IPlayerData> Players { get; }
string WinningTeam { get; }
string LobbyCode { get; }
DateTime Timestamp { get; }
}
```
```csharp
// W EndGamePatches.cs - jedna linijka
[HarmonyPatch(typeof(EndGameManager), nameof(EndGameManager.Start))]
public static void EndGameManagerStart(EndGameManager __instance)
{
BuildEndGameSummary(__instance);
GameStatsApi.RaiseGameEnd(new GameEndDataImpl()); // ← TYLKO TO
}
```
**Pros:**
- ✅ Minimalne zmiany w TOU Mira (~50 linii kodu)
- ✅ Type-safe API
- ✅ Dobra performance
- ✅ Stabilne (interface contract)
- ✅ Opcjonalny plugin
- ✅ Extensible (inne pluginy mogą używać)
**Cons:**
- ❌ Wymaga małej modyfikacji TOU Mira
- ❌ Wymaga nowego release TOU Mira
- ❌ Plugin nadal zależny od API changes
**Best for:** Balans między flexibility a stability
---
## Recommendation
### 🏆 Recommended Approach: **Scenario C (Hybrid)**
**Uzasadnienie:**
1. **Minimal Impact na TOU Mira**
- Tylko ~50-100 linii kodu
- Jeden nowy plik (GameStatsApi.cs)
- Jedna linijka w EndGamePatches.cs
- Nie zmienia istniejącej logiki
2. **Best of Both Worlds**
- Stabilność integrated solution
- Flexibility standalone plugin
- Type-safe API
- Dobra performance
3. **Future Extensibility**
- Inne pluginy mogą używać tego API
- Community może tworzyć własne stats exporters
- Tournament systems, analytics, etc.
4. **Maintenance**
- Interface contract → rzadziej się łamie
- Łatwiejsze testowanie
- Clear separation of concerns
### Implementation Priority
**Faza 1: Minimalne API w TOU Mira** (1-2 dni)
```csharp
// TownOfUs/Modules/GameStatsApi.cs
public static class GameStatsApi
{
public static event Action<GameEndEventArgs>? OnGameEnd;
}
public class GameEndEventArgs
{
public List<PlayerData> Players { get; set; }
public string WinningTeam { get; set; }
// ... essential data only
}
```
**Faza 2: Standalone Plugin** (3-4 dni)
```
TownOfUsStats.dll
├── Subscription do GameStatsApi
├── ApiConfigManager
├── Data transformation
└── HTTP client
```
**Faza 3: Polishing** (1-2 dni)
- Error handling
- Version compatibility checks
- Documentation
- Testing
---
## Alternative: Pure Standalone (If API is Not Acceptable)
Jeśli **absolutnie nie można** modyfikować TOU Mira, wtedy:
### Recommended: Scenario B with Safeguards
```csharp
[BepInPlugin("com.townofus.stats", "TownOfUs Stats Exporter", "1.0.0")]
[BepInDependency("auavengers.tou.mira", "1.2.1")] // Pin do konkretnej wersji
public class TownOfUsStatsPlugin : BasePlugin
{
private const string SUPPORTED_TOU_VERSION = "1.2.1";
public override void Load()
{
// Sprawdź wersję
if (!CheckTouMiraVersion())
{
Logger.LogError($"This plugin requires TOU Mira v{SUPPORTED_TOU_VERSION}");
Logger.LogError("Please update the plugin or TOU Mira");
return;
}
// Inicjalizuj z fallbackami
if (!reflectionHelper.Initialize())
{
Logger.LogError("Failed to initialize - incompatible TOU Mira version");
return;
}
Harmony.PatchAll();
}
}
```
**Dodatkowo:**
- Automated tests dla każdej wersji TOU Mira
- Clear documentation: "Supported TOU Mira versions: 1.2.1, 1.2.2"
- Automatic update checker
- Graceful degradation
---
## Code Example: Full Hybrid Implementation
### 1. TOU Mira Changes (GameStatsApi.cs)
```csharp
// File: TownOfUs/Modules/GameStatsApi.cs
namespace TownOfUs.Modules;
/// <summary>
/// Public API for external plugins to receive game statistics.
/// </summary>
public static class GameStatsApi
{
/// <summary>
/// Raised when a game ends with complete statistics.
/// Subscribers receive fully populated game data.
/// </summary>
public static event Action<IGameEndData>? OnGameEnd;
/// <summary>
/// Internal method to raise the OnGameEnd event.
/// Called by EndGamePatches after data is collected.
/// </summary>
internal static void NotifyGameEnd(IGameEndData data)
{
try
{
OnGameEnd?.Invoke(data);
}
catch (Exception ex)
{
Logger<TownOfUsPlugin>.Error($"Error in GameStatsApi subscriber: {ex}");
}
}
}
/// <summary>
/// Complete game end data provided to plugins.
/// </summary>
public interface IGameEndData
{
string GameId { get; }
DateTime Timestamp { get; }
string LobbyCode { get; }
string GameMode { get; }
float Duration { get; }
string Map { get; }
string WinningTeam { get; }
IReadOnlyList<IPlayerData> Players { get; }
}
/// <summary>
/// Individual player statistics.
/// </summary>
public interface IPlayerData
{
byte PlayerId { get; }
string PlayerName { get; }
string RoleName { get; }
IReadOnlyList<string> RoleHistory { get; }
IReadOnlyList<string> Modifiers { get; }
bool IsWinner { get; }
int TotalTasks { get; }
int TasksCompleted { get; }
int Kills { get; }
int CorrectKills { get; }
int IncorrectKills { get; }
int CorrectAssassinKills { get; }
int IncorrectAssassinKills { get; }
}
// Implementation class (internal)
internal class GameEndDataImpl : IGameEndData
{
public string GameId { get; set; } = string.Empty;
public DateTime Timestamp { get; set; }
public string LobbyCode { get; set; } = string.Empty;
public string GameMode { get; set; } = string.Empty;
public float Duration { get; set; }
public string Map { get; set; } = string.Empty;
public string WinningTeam { get; set; } = string.Empty;
public IReadOnlyList<IPlayerData> Players { get; set; } = new List<IPlayerData>();
}
internal class PlayerDataImpl : IPlayerData
{
public byte PlayerId { get; set; }
public string PlayerName { get; set; } = string.Empty;
public string RoleName { get; set; } = string.Empty;
public IReadOnlyList<string> RoleHistory { get; set; } = new List<string>();
public IReadOnlyList<string> Modifiers { get; set; } = new List<string>();
public bool IsWinner { get; set; }
public int TotalTasks { get; set; }
public int TasksCompleted { get; set; }
public int Kills { get; set; }
public int CorrectKills { get; set; }
public int IncorrectKills { get; set; }
public int CorrectAssassinKills { get; set; }
public int IncorrectAssassinKills { get; set; }
}
```
### 2. TOU Mira Integration (EndGamePatches.cs)
```csharp
// Add to existing EndGamePatches.cs
[HarmonyPatch(typeof(EndGameManager), nameof(EndGameManager.Start))]
[HarmonyPostfix]
public static void EndGameManagerStart(EndGameManager __instance)
{
BuildEndGameSummary(__instance);
// NEW: Notify external plugins
if (GameOptionsManager.Instance.CurrentGameOptions.GameMode != GameModes.HideNSeek)
{
var gameData = BuildGameEndData();
GameStatsApi.NotifyGameEnd(gameData);
}
}
private static IGameEndData BuildGameEndData()
{
var players = new List<IPlayerData>();
foreach (var record in EndGameData.PlayerRecords)
{
players.Add(new PlayerDataImpl
{
PlayerId = record.PlayerId,
PlayerName = StripColorTags(record.PlayerName ?? "Unknown"),
RoleName = ExtractLastRole(record),
RoleHistory = ExtractRoleHistory(record.PlayerId),
Modifiers = ExtractModifiers(record.PlayerId),
IsWinner = record.Winner,
// ... stats from GameHistory.PlayerStats
});
}
return new GameEndDataImpl
{
GameId = Guid.NewGuid().ToString(),
Timestamp = DateTime.UtcNow,
LobbyCode = InnerNet.GameCode.IntToGameName(AmongUsClient.Instance.GameId),
GameMode = GameOptionsManager.Instance?.CurrentGameOptions?.GameMode.ToString() ?? "Unknown",
Duration = Time.time,
Map = GetMapName((byte)(GameOptionsManager.Instance?.CurrentGameOptions?.MapId ?? 0)),
WinningTeam = DetermineWinningTeam(),
Players = players
};
}
```
### 3. Standalone Plugin (Complete)
```csharp
// TownOfUsStats.dll
[BepInPlugin("com.townofus.stats", "TownOfUs Stats Exporter", "1.0.0")]
[BepInDependency("auavengers.tou.mira", "1.2.1")]
public class TownOfUsStatsPlugin : BasePlugin
{
public override void Load()
{
Logger.LogInfo("=== TownOfUs Stats Exporter v1.0.0 ===");
if (!SubscribeToGameStatsApi())
{
Logger.LogError("Failed to subscribe - plugin disabled");
return;
}
Logger.LogInfo("Successfully initialized!");
}
private bool SubscribeToGameStatsApi()
{
try
{
var touAssembly = AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(a => a.GetName().Name == "TownOfUs");
if (touAssembly == null)
{
Logger.LogError("TOU Mira not found!");
return false;
}
var apiType = touAssembly.GetType("TownOfUs.Modules.GameStatsApi");
if (apiType == null)
{
Logger.LogError("GameStatsApi not found - is TOU Mira up to date?");
return false;
}
var onGameEndEvent = apiType.GetEvent("OnGameEnd");
if (onGameEndEvent == null)
{
Logger.LogError("OnGameEnd event not found!");
return false;
}
var handler = new Action<object>(OnGameEndReceived);
var typedHandler = Delegate.CreateDelegate(
onGameEndEvent.EventHandlerType!,
handler.Target,
handler.Method);
onGameEndEvent.AddEventHandler(null, typedHandler);
Logger.LogInfo("Subscribed to GameStatsApi.OnGameEnd");
return true;
}
catch (Exception ex)
{
Logger.LogError($"Subscription failed: {ex}");
return false;
}
}
private void OnGameEndReceived(object gameData)
{
_ = Task.Run(async () =>
{
try
{
await ExportGameData(gameData);
}
catch (Exception ex)
{
Logger.LogError($"Export failed: {ex}");
}
});
}
private async Task ExportGameData(object gameData)
{
Logger.LogInfo("Processing game end data...");
// Read config
var config = await ApiConfigManager.ReadConfigAsync();
if (!config.EnableApiExport || !config.IsValid())
{
Logger.LogInfo("Export disabled");
return;
}
// Transform data
var exportData = TransformGameData(gameData, config);
// Export
if (config.SaveLocalBackup)
{
await SaveLocalBackupAsync(exportData);
}
await SendToApiAsync(exportData, config);
Logger.LogInfo("Export completed!");
}
// ... rest of implementation
}
```
---
## Final Recommendation Matrix
| Criteria | Integrated | Pure Standalone | Hybrid API | Weight |
|----------|-----------|----------------|------------|--------|
| **Performance** | ✅✅✅ | ❌ | ✅✅✅ | 15% |
| **Stability** | ✅✅✅ | ❌ | ✅✅ | 25% |
| **Maintenance** | ✅✅ | ❌ | ✅✅ | 20% |
| **Flexibility** | ❌ | ✅✅✅ | ✅✅✅ | 15% |
| **Type Safety** | ✅✅✅ | ❌ | ✅✅✅ | 10% |
| **User Experience** | ✅✅ | ✅✅ | ✅✅✅ | 15% |
| **TOTAL SCORE** | **8.3/10** | **3.9/10** | **9.1/10** | 100% |
### 🏆 Winner: Hybrid API Approach
**Implementacja zalecana:**
1. Dodaj minimalny API do TOU Mira (~100 linii kodu)
2. Stwórz standalone plugin korzystający z API
3. Release obu razem
**Korzyści:**
- ✅ Najlepsza kombinacja stability + flexibility
- ✅ Łatwe utrzymanie
- ✅ Otwiera drzwi dla community plugins
- ✅ Type-safe i wydajne
- ✅ Opcjonalne dla użytkowników
---
**End of Standalone Plugin Analysis**