956 lines
25 KiB
Markdown
956 lines
25 KiB
Markdown
# 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<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:**
|
|
```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, @"<color=#[A-Fa-f0-9]+>", "");
|
|
name = name.Replace("</color>", "");
|
|
|
|
return name.Trim();
|
|
}
|
|
```
|
|
|
|
#### Role History Extraction
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```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<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:
|
|
|
|
```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<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:**
|
|
```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**
|