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

25 KiB

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
  2. Current State Analysis
  3. Reference Implementation Analysis
  4. Architecture Design
  5. Implementation Roadmap
  6. Technical Specifications
  7. Testing Strategy
  8. Security Considerations
  9. 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

{
  "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:

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:

public static GameStatsData BuildExportData(ApiConfig config)
private static PlayerStatsData BuildPlayerData(EndGameData.PlayerRecord record)
private static string ExtractRoleName(RoleBehaviour role)
private static List<string> ExtractRoleHistory(byte playerId)
private static List<string> 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:

[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:

# 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

// 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, @"<color=#[A-Fa-f0-9]+>", "");
    name = name.Replace("</color>", "");

    return name.Trim();
}

Role History Extraction

private static List<string> ExtractRoleHistory(byte playerId)
{
    var roles = new List<string>();

    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<NeutralGhostRole>())
        {
            continue;
        }

        roles.Add(ExtractRoleName(role));
    }

    return roles;
}

Modifier Extraction

private static List<string> ExtractModifiers(PlayerControl player)
{
    var modifiers = new List<string>();

    // Get all game modifiers (TOU and Universal)
    var playerModifiers = player.GetModifiers<GameModifier>()
        .Where(x => x is TouGameModifier || x is UniversalGameModifier);

    foreach (var modifier in playerModifiers)
    {
        modifiers.Add(modifier.ModifierName);
    }

    return modifiers;
}

Winning Team Determination

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

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

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:

// Startup
Logger<TownOfUsPlugin>.Info("GameStatsExporter initialized");

// Configuration
Logger<TownOfUsPlugin>.Info($"Config loaded: EnableExport={config.EnableApiExport}, Endpoint configured={!string.IsNullOrEmpty(config.ApiEndpoint)}");

// Export
Logger<TownOfUsPlugin>.Info("Starting game data export...");
Logger<TownOfUsPlugin>.Info($"Game data exported successfully. Players: {data.Players.Count}, Duration: {sw.ElapsedMilliseconds}ms");

// Errors
Logger<TownOfUsPlugin>.Error($"Failed to send to API: {ex.Message}");
Logger<TownOfUsPlugin>.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:

{
  "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):

{
  "success": true,
  "message": "Game data received",
  "gameId": "b2fe65e1-46f4-4a84-b60b-3c84f5fcc320"
}

Error (400 Bad Request):

{
  "success": false,
  "error": "Invalid token",
  "code": "AUTH_ERROR"
}

Error (422 Unprocessable Entity):

{
  "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<GameModifier>()
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:

// 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:

// 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