# Game Statistics API Export - Implementation Plan ## Town of Us: Mira Edition **Document Version:** 1.0 **Date:** 2025-10-07 **Author:** Implementation Analysis --- ## Table of Contents 1. [Executive Summary](#executive-summary) 2. [Current State Analysis](#current-state-analysis) 3. [Reference Implementation Analysis](#reference-implementation-analysis) 4. [Architecture Design](#architecture-design) 5. [Implementation Roadmap](#implementation-roadmap) 6. [Technical Specifications](#technical-specifications) 7. [Testing Strategy](#testing-strategy) 8. [Security Considerations](#security-considerations) 9. [Maintenance and Monitoring](#maintenance-and-monitoring) --- ## Executive Summary This document outlines the implementation plan for adding game statistics API export functionality to Town of Us: Mira, based on the existing implementation in ToU-stats/EndGamePatch.cs. The system will collect comprehensive game data at the end of each match and optionally send it to a configured API endpoint. ### Key Objectives - Export detailed game statistics including player roles, modifiers, stats, and match results - Provide optional local backup functionality - Support secure configuration through external INI file - Maintain backward compatibility with existing EndGame functionality - Follow ToU Mira's architecture using MiraAPI patterns ### Target JSON Output Format ```json { "token": "string", "secret": "string", "gameInfo": { "gameId": "guid", "timestamp": "ISO8601", "lobbyCode": "XXXXX", "gameMode": "Normal|HideNSeek", "duration": 527.189, "map": "The Skeld|MIRA HQ|Polus|Airship|The Fungle|Submerged" }, "players": [ { "playerId": 0, "playerName": "string", "playerTag": "string|null", "platform": "string", "role": "string", "roles": ["string"], "modifiers": ["string"], "isWinner": true|false, "stats": { "totalTasks": 10, "tasksCompleted": 8, "kills": 0, "correctKills": 0, "incorrectKills": 0, "correctAssassinKills": 0, "incorrectAssassinKills": 0 } } ], "gameResult": { "winningTeam": "Crewmates|Impostors|Neutrals|Unknown" } } ``` --- ## Current State Analysis ### ToU Mira Architecture **Framework:** MiraAPI 0.3.0 + Reactor 2.3.1 + BepInEx IL2CPP 6.0.0 **Key Components:** 1. **EndGamePatches.cs** - Current end game handling - `BuildEndGameData()` - Collects player records into `EndGameData.PlayerRecords` - `BuildEndGameSummary()` - Creates UI summary display - `AmongUsClientGameEndPatch()` - Harmony patch on `AmongUsClient.OnGameEnd` - `EndGameData` class - Stores player records with: - PlayerName (with winner highlighting) - RoleString (formatted with colors) - Winner flag - LastRole (RoleTypes) - Team (ModdedRoleTeams) - PlayerId 2. **GameHistory.cs** - Game state tracking - `RoleHistory` - List of all role changes per player - `KilledPlayers` - DeadPlayer records with killer/victim/time - `PlayerStats` - Dictionary of player statistics - CorrectKills, IncorrectKills - CorrectAssassinKills, IncorrectAssassinKills - `EndGameSummary` - String summary for UI 3. **Role System** - `ITownOfUsRole` interface with `RoleAlignment` enum - `ModdedRoleTeams` enum (Crewmate, Impostor, Neutral, Custom) - Role name resolution via `GetRoleName()` - Support for role history tracking 4. **Modifier System** - `GameModifier`, `TouGameModifier`, `UniversalGameModifier` - `AllianceGameModifier` for team alliances - Modifier color and name support ### Current Data Flow ``` Game End → AmongUsClient.OnGameEnd [Harmony Patch] → BuildEndGameData() → Iterate PlayerControl.AllPlayerControls → Extract role history from GameHistory.RoleHistory → Extract modifiers from playerControl.GetModifiers() → Extract stats from GameHistory.PlayerStats → Build RoleString with colors/formatting → Check winner status from EndGameResult.CachedWinners → Add to EndGameData.PlayerRecords → EndGameManager.Start [Harmony Patch] → BuildEndGameSummary() → Display UI with player records ``` ### Differences from ToU-stats Reference | Aspect | ToU-stats | ToU Mira | |--------|-----------|----------| | Framework | Harmony patches only | MiraAPI + Harmony | | Role Storage | `Role.RoleHistory` | `GameHistory.RoleHistory` (MiraAPI RoleBehaviour) | | Role Types | Custom enum `RoleEnum` | MiraAPI `RoleTypes` + custom roles | | Stats Storage | Within role string parsing | `GameHistory.PlayerStats` dictionary | | Team System | Simple faction checks | `ModdedRoleTeams` enum + `RoleAlignment` | | Modifiers | String parsing from role string | MiraAPI modifier system | | Platform Detection | Hardcoded "PC" | Can detect from player data | | Task Tracking | Role-based `TotalTasks`/`TasksLeft` | Available via player stats | --- ## Reference Implementation Analysis ### ToU-stats EndGamePatch.cs Architecture **Key Classes:** 1. **ApiConfig** - `EnableApiExport` (bool) - Master toggle - `ApiToken` (string) - Authentication token - `ApiEndpoint` (string) - Target URL - `SaveLocalBackup` (bool) - Local JSON saving - `Secret` (string) - Additional security key 2. **Data Models** - `GameApiData` - Root container - `GameInfo` - Match metadata - `PlayerData` - Individual player statistics - `PlayerStats` - Numerical stats - `GameResult` - Win conditions - `SpecialWinner` - Secondary winners (Jester, Executioner, etc.) 3. **Processing Pipeline** ``` SendGameDataToApi() [async] → ReadApiConfig() → BuildGameData() → ExtractPlayerData() for each player → CleanPlayerName() → IsPlayerWinner() → ExtractMainRole() → ExtractModifiers() [string parsing] → ExtractStats() [regex parsing] → DetermineWinningTeam() → SaveJsonLocally() [if enabled] → SendToApi() [HTTP POST] ``` 4. **Key Features** - Async execution to avoid blocking UI - Local data copy before async processing - INI file configuration (game dir or Documents/TownOfUs) - 30-second HTTP timeout - Comprehensive error logging - 1-second delay before clearing data --- ## Architecture Design ### System Components ``` ┌─────────────────────────────────────────────────────────────┐ │ TownOfUs Plugin │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ EndGamePatches.cs (existing) │ │ │ │ - BuildEndGameData() │ │ │ │ - AmongUsClientGameEndPatch() [Harmony] │ │ │ └───────────────┬───────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ GameStatsExporter.cs (NEW) │ │ │ │ - ExportGameDataAsync() │ │ │ │ - BuildExportData() │ │ │ │ - SendToApiAsync() │ │ │ │ - SaveLocalBackupAsync() │ │ │ └───────────────┬───────────────────────────────────────┘ │ │ │ │ │ ├─────────────────┬────────────────────┐ │ │ ▼ ▼ ▼ │ │ ┌──────────────────┐ ┌──────────────────┐ ┌───────────┐ │ │ │ ApiConfigManager│ │ GameDataBuilder │ │ DataModels│ │ │ │ (NEW) │ │ (NEW) │ │ (NEW) │ │ │ └──────────────────┘ └──────────────────┘ └───────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` ### Module Descriptions #### 1. GameStatsExporter (Main Orchestrator) **Location:** `TownOfUs/Modules/GameStatsExporter.cs` **Responsibilities:** - Coordinate the export process - Manage async execution - Handle errors and logging - Interface with existing EndGameData **Key Methods:** ```csharp public static async Task ExportGameDataAsync() public static Task ExportGameDataBackground() // Fire-and-forget wrapper ``` #### 2. ApiConfigManager (Configuration Handler) **Location:** `TownOfUs/Modules/Stats/ApiConfigManager.cs` **Responsibilities:** - Read/write ApiSet.ini configuration - Validate configuration values - Provide default configuration template - Search in multiple locations (game dir, Documents) **Configuration Locations:** 1. `{GameDirectory}/ApiSet.ini` 2. `{Documents}/TownOfUs/ApiSet.ini` #### 3. GameDataBuilder (Data Transformation) **Location:** `TownOfUs/Modules/Stats/GameDataBuilder.cs` **Responsibilities:** - Transform EndGameData to export format - Extract role names (clean, without color tags) - Build role history arrays - Map modifiers to strings - Determine winning team - Calculate task completion **Key Methods:** ```csharp public static GameStatsData BuildExportData(ApiConfig config) private static PlayerStatsData BuildPlayerData(EndGameData.PlayerRecord record) private static string ExtractRoleName(RoleBehaviour role) private static List ExtractRoleHistory(byte playerId) private static List ExtractModifiers(PlayerControl player) private static string DetermineWinningTeam() ``` #### 4. DataModels (Data Structures) **Location:** `TownOfUs/Modules/Stats/GameStatsModels.cs` **Classes:** - `GameStatsData` - Root export object - `GameInfoData` - Match metadata - `PlayerStatsData` - Individual player data - `PlayerStatsNumbers` - Numerical statistics - `GameResultData` - Win conditions ### Integration Points #### Integration with EndGamePatches.cs **Modified Patch:** ```csharp [HarmonyPatch(typeof(EndGameManager), nameof(EndGameManager.Start))] [HarmonyPostfix] public static void EndGameManagerStart(EndGameManager __instance) { BuildEndGameSummary(__instance); // NEW: Trigger stats export if (GameOptionsManager.Instance.CurrentGameOptions.GameMode != GameModes.HideNSeek) { _ = GameStatsExporter.ExportGameDataBackground(); } } ``` #### Integration with GameHistory - Read from `GameHistory.RoleHistory` for role sequences - Read from `GameHistory.PlayerStats` for statistics - Read from `GameHistory.KilledPlayers` for kill tracking - Use `GameHistory.WinningFaction` if available --- ## Implementation Roadmap ### Phase 1: Data Models (Day 1) **Tasks:** 1. Create `TownOfUs/Modules/Stats/` directory 2. Implement `GameStatsModels.cs` with all data classes 3. Add JSON serialization attributes 4. Write unit tests for model serialization **Deliverables:** - ✓ All data model classes - ✓ JSON serialization working - ✓ Test coverage for models ### Phase 2: Configuration System (Day 1-2) **Tasks:** 1. Implement `ApiConfigManager.cs` 2. Create INI file reading/writing logic 3. Implement configuration validation 4. Create default configuration template 5. Test multi-location search **Deliverables:** - ✓ Config file reading/writing - ✓ Default template generation - ✓ Validation logic - ✓ Error handling ### Phase 3: Data Builder (Day 2-3) **Tasks:** 1. Implement `GameDataBuilder.cs` 2. Create role name extraction (strip color codes) 3. Build role history from GameHistory 4. Extract modifiers from MiraAPI modifier system 5. Map ModdedRoleTeams to winning team strings 6. Calculate task completion percentages 7. Handle platform detection 8. Test with various game scenarios **Deliverables:** - ✓ Complete data transformation - ✓ Role history extraction - ✓ Modifier mapping - ✓ Stats aggregation - ✓ Test coverage ### Phase 4: Export System (Day 3-4) **Tasks:** 1. Implement `GameStatsExporter.cs` 2. Create async export pipeline 3. Implement HTTP client with timeout 4. Add local backup functionality 5. Implement comprehensive logging 6. Add error handling and recovery **Deliverables:** - ✓ Async export working - ✓ HTTP POST implementation - ✓ Local backup system - ✓ Error handling - ✓ Logging integration ### Phase 5: Integration (Day 4-5) **Tasks:** 1. Modify `EndGamePatches.cs` to call exporter 2. Test with actual game sessions 3. Verify data accuracy 4. Test error scenarios (no config, network failure) 5. Verify UI doesn't block during export **Deliverables:** - ✓ Integrated with EndGame flow - ✓ No blocking on UI - ✓ Error scenarios handled - ✓ Data validation passed ### Phase 6: Testing and Documentation (Day 5-6) **Tasks:** 1. Comprehensive integration testing 2. Performance testing (large lobbies) 3. Network failure scenarios 4. Configuration validation testing 5. Update user documentation 6. Create API endpoint specification **Deliverables:** - ✓ Full test suite - ✓ Performance benchmarks - ✓ User documentation - ✓ API specification - ✓ Configuration guide --- ## Technical Specifications ### Configuration File Format **File Name:** `ApiSet.ini` **Locations (Priority Order):** 1. `{GameInstallDir}/ApiSet.ini` 2. `{UserDocuments}/TownOfUs/ApiSet.ini` **Format:** ```ini # TownOfUs API Exporter Configuration # Whether to enable API export (true/false) EnableApiExport=false # API Authentication Token ApiToken= # API Endpoint URL ApiEndpoint= # Whether to save local backup copies (true/false) SaveLocalBackup=false # Additional secret/password for API authentication Secret= # Example configuration: # EnableApiExport=true # ApiToken=your_secret_token_here # ApiEndpoint=https://api.example.com/api/among-data # SaveLocalBackup=true # Secret=your_secret_key_here ``` ### Data Extraction Rules #### Role Name Extraction ```csharp // ToU Mira uses RoleBehaviour from MiraAPI private static string ExtractRoleName(RoleBehaviour role) { if (role == null) return "Unknown"; // Use GetRoleName() for localized name var name = role.GetRoleName(); // Strip color tags name = Regex.Replace(name, @"", ""); name = name.Replace("", ""); return name.Trim(); } ``` #### Role History Extraction ```csharp private static List ExtractRoleHistory(byte playerId) { var roles = new List(); foreach (var roleEntry in GameHistory.RoleHistory.Where(x => x.Key == playerId)) { var role = roleEntry.Value; // Skip ghost roles if (role.Role is RoleTypes.CrewmateGhost or RoleTypes.ImpostorGhost || role.Role == (RoleTypes)RoleId.Get()) { continue; } roles.Add(ExtractRoleName(role)); } return roles; } ``` #### Modifier Extraction ```csharp private static List ExtractModifiers(PlayerControl player) { var modifiers = new List(); // Get all game modifiers (TOU and Universal) var playerModifiers = player.GetModifiers() .Where(x => x is TouGameModifier || x is UniversalGameModifier); foreach (var modifier in playerModifiers) { modifiers.Add(modifier.ModifierName); } return modifiers; } ``` #### Winning Team Determination ```csharp private static string DetermineWinningTeam() { // Use GameHistory.WinningFaction if available if (!string.IsNullOrEmpty(GameHistory.WinningFaction)) { return GameHistory.WinningFaction; } // Fallback: Check winner records var winners = EndGameData.PlayerRecords.Where(x => x.Winner).ToList(); if (!winners.Any()) return "Unknown"; var firstWinner = winners.First(); return firstWinner.Team switch { ModdedRoleTeams.Crewmate => "Crewmates", ModdedRoleTeams.Impostor => "Impostors", ModdedRoleTeams.Neutral => "Neutrals", _ => "Unknown" }; } ``` #### Platform Detection ```csharp private static string GetPlayerPlatform(PlayerControl player) { // Check player platform data if (player.Data?.Platform != null) { return player.Data.Platform.ToString(); } // Default fallback return "Unknown"; } ``` #### Player Tag Extraction ```csharp private static string GetPlayerTag(PlayerControl player) { // Check if player has friend code/tag visible if (player.Data?.FriendCode != null && !string.IsNullOrEmpty(player.Data.FriendCode)) { return player.Data.FriendCode; } return null; } ``` ### HTTP Communication #### Request Format ``` POST {ApiEndpoint} Content-Type: application/json User-Agent: TownOfUs-Mira-DataExporter/1.2.1 {GameStatsData JSON} ``` #### Timeout Configuration - Connection timeout: 30 seconds - Read/Write timeout: 30 seconds #### Error Handling - Network errors: Log and continue (don't crash game) - Timeout errors: Log timeout message - HTTP error codes: Log status code and response body - JSON serialization errors: Log error and data that failed ### Local Backup System **Directory:** `{UserDocuments}/TownOfUs/GameLogs/` **File Naming:** `Game_{yyyyMMdd_HHmmss}_{gameId}.json` **Example:** `Game_20250921_210247_b2fe65e1.json` **Format:** Pretty-printed JSON (WriteIndented=true) --- ## Testing Strategy ### Unit Tests **Location:** `TownOfUs.Tests/Modules/Stats/` **Test Cases:** 1. **Model Serialization** - JSON serialization/deserialization - Null value handling - Empty collection handling 2. **Configuration Manager** - INI file parsing - Default value handling - Invalid configuration handling - Multi-location search 3. **Data Builder** - Role name extraction - Color tag stripping - Modifier extraction - Stats aggregation - Team determination ### Integration Tests **Test Scenarios:** 1. **Complete Game Flow** - Start game with 10 players - Assign various roles and modifiers - Play to completion - Verify exported data accuracy 2. **Network Scenarios** - Successful API call - Network timeout - Connection failure - Invalid endpoint - HTTP error responses (4xx, 5xx) 3. **Configuration Scenarios** - No config file (disabled) - Config in game directory - Config in documents directory - Invalid config values - Partial configuration 4. **Edge Cases** - Hide & Seek mode (should skip export) - Empty lobby - All spectators - Role changes (Amnesiac) - Multiple modifiers per player ### Performance Tests **Metrics:** - Export time for 15 players: < 500ms - UI blocking time: 0ms (async execution) - Memory usage increase: < 5MB - JSON size for 15 players: ~10-15KB --- ## Security Considerations ### Configuration Security 1. **Token Protection** - Store tokens in INI file (not in code) - INI file should not be committed to repository - Add ApiSet.ini to .gitignore 2. **Secret Key** - Additional authentication layer - Prevents accidental data submission - Server-side validation required 3. **File Permissions** - INI file readable only by game process - Local backup directory permissions restricted ### Network Security 1. **HTTPS Enforcement** - Require HTTPS endpoints - Validate SSL certificates - Reject self-signed certificates in production 2. **Data Validation** - Validate endpoint URL format - Sanitize player names (XSS prevention) - Limit JSON payload size 3. **Privacy** - No personally identifiable information (PII) - Player names only (not IP addresses) - Optional: Hash player names before sending ### Error Message Security - Don't expose full file paths in logs - Don't log sensitive tokens - Redact tokens in error messages --- ## Maintenance and Monitoring ### Logging Strategy **Log Levels:** - **Info:** Configuration loaded, export started/completed - **Warning:** Config not found, export disabled, network timeout - **Error:** HTTP errors, serialization failures, file I/O errors **Log Messages:** ```csharp // Startup Logger.Info("GameStatsExporter initialized"); // Configuration Logger.Info($"Config loaded: EnableExport={config.EnableApiExport}, Endpoint configured={!string.IsNullOrEmpty(config.ApiEndpoint)}"); // Export Logger.Info("Starting game data export..."); Logger.Info($"Game data exported successfully. Players: {data.Players.Count}, Duration: {sw.ElapsedMilliseconds}ms"); // Errors Logger.Error($"Failed to send to API: {ex.Message}"); Logger.Warning("API export is disabled - skipping"); ``` ### Monitoring Metrics **Client-Side:** - Export success rate - Average export duration - Network error rate - Local backup success rate **Server-Side (API):** - Requests per minute - Invalid token rate - Data validation failure rate - Average payload size ### Version Compatibility **API Versioning:** Add version field to GameStatsData: ```json { "version": "1.0", "modVersion": "1.2.1", ... } ``` **Breaking Changes:** - Increment major version (1.0 → 2.0) - Maintain backward compatibility for 1 version **Non-Breaking Changes:** - Add optional fields with defaults - Add new enum values with "Unknown" fallback ### Troubleshooting Guide **Common Issues:** 1. **Export not working** - Check `EnableApiExport=true` in config - Verify endpoint URL is correct - Check BepInEx logs for errors 2. **Network timeout** - Verify internet connection - Check firewall settings - Verify endpoint is accessible 3. **Invalid data** - Check JSON format in local backup - Verify all roles have names - Check for null reference errors 4. **Performance issues** - Check export duration in logs - Verify async execution (UI not blocking) - Consider disabling local backup --- ## Appendix A: File Structure ``` TownOfUs/ ├── Modules/ │ ├── GameHistory.cs (existing) │ ├── Stats/ │ │ ├── GameStatsExporter.cs (NEW) │ │ ├── ApiConfigManager.cs (NEW) │ │ ├── GameDataBuilder.cs (NEW) │ │ └── GameStatsModels.cs (NEW) │ └── ... ├── Patches/ │ ├── EndGamePatches.cs (modified) │ └── ... └── ... Documents/TownOfUs/ ├── ApiSet.ini (user config) └── GameLogs/ ├── Game_20250921_210247_b2fe65e1.json └── ... ``` --- ## Appendix B: API Endpoint Specification ### Request **Method:** POST **Content-Type:** application/json **Headers:** - `User-Agent: TownOfUs-Mira-DataExporter/{version}` **Body:** GameStatsData JSON ### Response **Success (200 OK):** ```json { "success": true, "message": "Game data received", "gameId": "b2fe65e1-46f4-4a84-b60b-3c84f5fcc320" } ``` **Error (400 Bad Request):** ```json { "success": false, "error": "Invalid token", "code": "AUTH_ERROR" } ``` **Error (422 Unprocessable Entity):** ```json { "success": false, "error": "Invalid data format", "code": "VALIDATION_ERROR", "details": ["Missing required field: gameInfo.timestamp"] } ``` --- ## Appendix C: Migration from ToU-stats ### Key Differences Table | Aspect | ToU-stats | ToU Mira Implementation | |--------|-----------|------------------------| | Role Access | `Role.RoleHistory` | `GameHistory.RoleHistory` | | Role Type | `RoleEnum` | `RoleBehaviour` + `RoleTypes` | | Stats Access | Parsed from role string | `GameHistory.PlayerStats` dictionary | | Modifier Access | String parsing | `player.GetModifiers()` | | Team System | String-based faction checks | `ModdedRoleTeams` enum | | Color Removal | Not needed (stored separately) | Strip from `GetRoleName()` | | Task Info | `player.TotalTasks`, `player.TasksLeft` | Available via GameHistory | | Platform | Hardcoded "PC" | `player.Data.Platform` | | Async Pattern | `Task.Run(async () => {...})` | Same pattern maintained | ### Code Mapping Examples **Role History Loop:** ```csharp // ToU-stats foreach (var role in Role.RoleHistory.Where(x => x.Key == playerControl.PlayerId)) { if (role.Value == RoleEnum.Crewmate) { ... } } // ToU Mira foreach (var roleEntry in GameHistory.RoleHistory.Where(x => x.Key == playerControl.PlayerId)) { var role = roleEntry.Value; if (role.Role == RoleTypes.Crewmate) { ... } } ``` **Stats Access:** ```csharp // ToU-stats var player = Role.GetRole(playerControl); stats.Kills = player.Kills; // ToU Mira if (GameHistory.PlayerStats.TryGetValue(playerControl.PlayerId, out var stats)) { // Use stats directly } ``` --- ## Appendix D: Example JSON Output See reference file: `ToU-stats/Game_20250921_210247_b2fe65e1.json` --- ## Document Change Log | Version | Date | Changes | Author | |---------|------|---------|--------| | 1.0 | 2025-10-07 | Initial document creation | Analysis | --- **End of Implementation Plan**