Initial commit
This commit is contained in:
955
DOC/GameStats_API_Implementation_Plan.md
Normal file
955
DOC/GameStats_API_Implementation_Plan.md
Normal file
@@ -0,0 +1,955 @@
|
||||
# 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**
|
||||
Reference in New Issue
Block a user