Fix data export formatting and implement robust modifier detection

Major improvements to data cleaning, modifier extraction, and role name handling:

## Data Cleaning & Normalization
- Enhanced StripColorTags() to remove ALL HTML/Unity tags using universal regex
- Added removal of modifier symbols (♥, #, ★, etc.) from player names
- Fixed winningTeam containing HTML tags by applying StripColorTags()
- Result: Clean player names without tags or symbols in exported JSON

## Modifier Detection System
- Implemented three-tier modifier detection:
  1. Symbol extraction from <size=60%>SYMBOL</size> tags (primary)
  2. Reflection-based GetModifiers<GameModifier>() (secondary)
  3. RoleString parsing fallback (tertiary)
- Fixed false positive Underdog detection from friend codes (username#1234)
- Symbols now extracted ONLY from size tags, not entire player name
- Fixed Dictionary duplicate key error (removed \u2665 duplicate of ♥)
- Supported symbols: ♥❤♡ (Lovers), # (Underdog), ★☆ (VIP), @ (Sleuth), etc.

## Role Name Handling
- Added fallback for roles without GetRoleName() method
- Extracts role names from type names: "GrenadierRole" → "Grenadier"
- Removes "Tou" suffix: "EngineerTou" → "Engineer"
- Skips generic "RoleBehaviour" type (not a real role)
- Result: Full role history now populated correctly

## Code Quality
- Added comprehensive logging for debugging modifier/role extraction
- Enhanced error handling with per-player try-catch blocks
- Improved code documentation with inline comments

## Documentation
- Added CLAUDE.md with complete architecture guide
- Documented modifier detection system
- Added data cleaning & normalization section
- Included troubleshooting guide and development workflows

Fixes: #1 (HTML tags in export), #2 (missing role history), #3 (false modifier detection)

🎮 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-08 21:42:55 +02:00
parent 1682f5bb60
commit 03d4673c00
3 changed files with 467 additions and 23 deletions

311
CLAUDE.md Normal file
View File

@@ -0,0 +1,311 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
**TownOfUs Stats Exporter** is a standalone BepInEx plugin for Among Us that exports Town of Us Mira game statistics to a cloud API. The plugin operates completely independently of TOU Mira using reflection and Harmony patches - requiring zero modifications to the main mod.
## Key Architecture Principles
### Reflection-Based Approach
The plugin **never directly references TOU Mira types**. All access is via reflection through `TouMiraReflectionBridge.cs` which:
- Caches all Type and PropertyInfo objects at initialization for performance
- Handles IL2CPP interop for Unity collections
- Provides version compatibility checking
- Falls back gracefully if TOU Mira structures change
### Harmony Patching Strategy
Uses `[HarmonyPriority(Priority.Low)]` on the `AmongUsClient.OnGameEnd` patch to ensure TOU Mira's `BuildEndGameData()` runs first and populates `EndGameData.PlayerRecords` before the export begins.
### Async Export Pattern
Export runs in a fire-and-forget Task.Run() to avoid blocking the game UI. The entire export process (reflection, data transformation, API call) happens asynchronously.
## Build Commands
```bash
# Build the plugin
dotnet build -c Release
# Build in debug mode
dotnet build -c Debug
# Clean build
dotnet clean && dotnet build -c Release
```
The compiled DLL will be in `bin/Release/TownOfUsStatsExporter.dll` or `bin/Debug/TownOfUsStatsExporter.dll`.
If the `AmongUs` environment variable is set, the DLL auto-copies to `$(AmongUs)/BepInEx/plugins/` after build.
## Testing & Debugging
### Local Testing
1. Build in Debug mode
2. Copy DLL to `Among Us/BepInEx/plugins/`
3. Launch Among Us with BepInEx console (F10)
4. Check logs for "TownOfUs Stats Exporter" messages
5. Complete a game and watch export logs
### Log Locations
- **BepInEx Console**: Real-time logs (press F10 in game)
- **BepInEx Log File**: `Among Us/BepInEx/LogOutput.log`
- **Local Backups**: `Documents/TownOfUs/GameLogs/Game_YYYYMMDD_HHMMSS_<gameId>.json`
### Common Debug Scenarios
- **"Failed to initialize reflection bridge"**: TOU Mira assembly not found or incompatible version
- **"No player data available"**: Export triggered before TOU Mira populated EndGameData
- **Empty role history**: RoleString fallback parser activates - check RoleString format
## Code Structure & Flow
### Initialization Flow (Plugin Load)
1. `TownOfUsStatsPlugin.Load()` - BepInEx entry point
2. `TouMiraReflectionBridge.Initialize()` - Find TOU Mira assembly, cache reflection metadata
3. `VersionCompatibility.CheckVersion()` - Verify TOU Mira version compatibility
4. `Harmony.PatchAll()` - Apply patches (EndGameExportPatch)
### Export Flow (Game End)
1. **AmongUsClient.OnGameEnd** fires (game ends)
2. **TOU Mira's patch** runs (High priority) - populates EndGameData.PlayerRecords
3. **EndGameExportPatch.Postfix** runs (Low priority) - starts export
- Checks for Hide & Seek mode (skip if true)
- Fires Task.Run() with `StatsExporter.ExportGameStatsAsync()`
4. **StatsExporter.ExportGameStatsAsync()**
- Reads `ApiSet.ini` config
- Calls reflection bridge methods to collect data
- Transforms data via `DataTransformer.TransformToExportFormat()`
- Saves local backup if enabled
- POSTs to API via `ApiClient.SendToApiAsync()`
### Key Reflection Patterns
**Reading TOU Mira Data:**
```csharp
// Get static property value
var playerRecords = cache.PlayerRecordsProperty!.GetValue(null);
// Get instance property value
var value = obj.GetType().GetProperty("PropertyName")?.GetValue(obj);
// Handle IL2CPP lists
var managedList = IL2CPPHelper.ConvertToManagedList(il2cppList);
```
**Adding New Reflected Data:**
1. Add to `ReflectionCache.cs` (Type, PropertyInfo, FieldInfo)
2. Cache in `TouMiraReflectionBridge.CacheReflectionMetadata()`
3. Add getter method in `TouMiraReflectionBridge`
4. Add model to `Models/ReflectedData.cs`
5. Use in `DataTransformer.TransformToExportFormat()`
## Important Constraints
### What You Can Access
- Public classes, properties, fields, methods in TOU Mira
- Static data in `EndGamePatches.EndGameData`
- Static data in `GameHistory` (PlayerStats, RoleHistory, KilledPlayers, WinningFaction)
- PlayerControl instances via `PlayerControl.AllPlayerControls`
### What You Cannot Access
- Internal or private members (C# access modifiers enforced)
- Non-public nested types
- Direct type references (must use reflection strings)
### Version Compatibility
- **Tested**: TOU Mira 1.2.0, 1.2.1
- **Probably Compatible**: Same major version (1.x.x)
- **Incompatible**: Different major version (2.x.x)
The plugin logs warnings for untested versions but attempts to run anyway.
## Configuration System
The plugin searches for `ApiSet.ini` in priority order:
1. Game directory (where DLL is located)
2. `Documents/TownOfUs/ApiSet.ini`
If not found, creates a default template at the Documents location.
**Config Structure (INI format):**
```ini
EnableApiExport=true
ApiToken=your_token_here
ApiEndpoint=https://api.example.com/endpoint
SaveLocalBackup=true
Secret=optional_secret
```
Config is read asynchronously each game - no restart required after editing.
## Error Handling Philosophy
### Fail-Safe Design
- Plugin failures never crash the game
- Export errors are logged but don't interrupt gameplay
- Individual player data errors skip that player, continue with others
- Missing optional data (modifiers, platform) defaults to empty/Unknown
### Error Isolation Layers
1. **Top-level**: `Task.Run()` catches all exceptions
2. **Export-level**: `ExportGameStatsAsync()` catches all exceptions
3. **Component-level**: Each transformer/client method has try-catch
4. **Player-level**: Each player transform has try-catch
## Data Models
### Export Format (GameStatsData)
```
GameStatsData
├── Token (string) - API auth token
├── Secret (string?) - Optional additional secret
├── GameInfo
│ ├── GameId (guid)
│ ├── Timestamp (UTC)
│ ├── LobbyCode (e.g., "ABCDEF")
│ ├── GameMode (Normal/HideNSeek)
│ ├── Duration (seconds)
│ └── Map (string)
├── Players[] (list)
│ ├── PlayerId (byte)
│ ├── PlayerName (string)
│ ├── PlayerTag (friend code)
│ ├── Platform (Steam/Epic/etc)
│ ├── Role (final role string)
│ ├── Roles[] (full role history, excludes ghost roles)
│ ├── Modifiers[] (e.g., ["Button Barry", "Flash"])
│ ├── IsWinner (bool)
│ └── Stats
│ ├── TotalTasks
│ ├── TasksCompleted
│ ├── Kills
│ ├── CorrectKills
│ ├── IncorrectKills
│ ├── CorrectAssassinKills
│ └── IncorrectAssassinKills
└── GameResult
└── WinningTeam (Crewmates/Impostors/Neutrals)
```
### Internal Models (ReflectedData)
Intermediate models used between reflection bridge and transformer:
- `PlayerRecordData` - Raw data from EndGameData.PlayerRecords
- `PlayerStatsData` - Kill statistics from GameHistory.PlayerStats
- `KilledPlayerData` - Kill events from GameHistory.KilledPlayers
## Dependencies
- **BepInEx.Unity.IL2CPP** (6.0.0-be.735) - Plugin framework
- **AmongUs.GameLibs.Steam** (2025.9.9) - Game libraries
- **HarmonyX** (2.13.0) - Runtime patching
- **AllOfUs.MiraAPI** (0.3.0) - MiraAPI types (for PlayerControl, etc)
- **Reactor** (2.3.1) - Required by TOU Mira
- **Il2CppInterop.Runtime** (1.4.6-ci.426) - IL2CPP interop
All dependencies are NuGet packages referenced in `TownOfUsStatsExporter.csproj`.
## Performance Considerations
- Reflection is ~100x slower than direct access, but total overhead is <100ms
- Reflection metadata is cached at plugin load for fast repeated access
- Export runs fully async - zero UI blocking time
- HTTP client is reused (static singleton) to avoid connection overhead
- JSON serialization uses System.Text.Json (fast, modern)
## Modifier Detection System
The plugin uses **three-tier modifier detection**:
### 1. Symbol Extraction (Primary)
Player names can contain special symbols inside HTML tags that indicate modifiers.
**Format**: `<b><size=60%>SYMBOL</size></b>`
Supported symbols:
- `♥` `❤` `♡` `\u2665`**Lovers** modifier
- `#`**Underdog** modifier
- `★` `☆`**VIP** modifier
- `@`**Sleuth** modifier
- `†` `‡`**Bait** modifier
- `§`**Torch** modifier
- `⚡`**Flash** modifier
- `⚔` `🗡`**Assassin** modifier
**Important**:
- Symbols are extracted **only from `<size=...>SYMBOL</size>` tags**, not from entire player name
- This prevents false positives (e.g., `#` in friend codes like `username#1234`)
- Extraction happens **before** HTML tag stripping to preserve modifier information
Example:
```
Input: "<color=#EFBF04>boracik</color> <b><size=60%>♥</size></b>"
Step 1: ExtractModifiersFromPlayerName() finds "♥" in size tag → ["Lovers"]
Step 2: StripColorTags() removes HTML tags AND symbols → "boracik"
Output: playerName="boracik", modifiers=["Lovers"]
```
**Note**: The `StripColorTags()` function removes both HTML tags AND modifier symbols to ensure clean player names.
### 2. Reflection (Secondary)
Attempts to get modifiers via reflection on `PlayerControl.GetModifiers<GameModifier>()`
### 3. RoleString Parsing (Fallback)
Parses modifier names from parentheses in RoleString if both above methods fail.
### Processing Order in DataTransformer.TransformPlayerData()
```
1. ExtractModifiersFromPlayerName(record.PlayerName) → Gets symbols
2. bridge.GetPlayerModifiers(playerId) → Gets via reflection
3. ParseModifiersFromRoleString(record.RoleString) → Parses string
4. StripColorTags(record.PlayerName) → Clean name for export
```
## Adding New Features
### To Add New Modifier Symbol
1. Edit `ExtractModifiersFromPlayerName()` in `Export/DataTransformer.cs`
2. Add symbol to `symbolToModifier` dictionary
3. Rebuild and test
### To Add New Exported Data
1. Check if TOU Mira exposes it publicly (use dnSpy or ILSpy)
2. Add reflection cache entry in `ReflectionCache.cs`
3. Add caching logic in `TouMiraReflectionBridge.CacheReflectionMetadata()`
4. Add getter method in `TouMiraReflectionBridge`
5. Add property to export model in `Models/GameStatsData.cs`
6. Add transformation in `DataTransformer.TransformToExportFormat()`
### To Add New Config Options
1. Add property to `ApiConfig.cs`
2. Add parsing in `ApiConfigManager.ParseIniFile()`
3. Add to default template in `ApiConfigManager.CreateDefaultConfigAsync()`
4. Use in appropriate exporter class
## Data Cleaning & Normalization
All exported data is cleaned to ensure consistency:
### Text Cleaning (StripColorTags)
Removes from all text fields (playerName, winningTeam, etc.):
- HTML/Unity tags: `<color=#XXX>`, `<b>`, `<size=XX%>`, etc.
- Modifier symbols: `♥`, `#`, `★`, `@`, etc.
### Role Name Normalization
- Removes suffixes: `"GrenadierRole"``"Grenadier"`
- Removes TOU suffix: `"EngineerTou"``"Engineer"`
- Skips generic types: `"RoleBehaviour"` (not added to history)
### Modifier Symbol Extraction
Symbols are extracted **before** text cleaning to preserve information:
1. Extract from `<size=60%>SYMBOL</size>` tags
2. Map to modifier names (e.g., `♥``"Lovers"`)
3. Clean text (remove tags and symbols)
## Known Limitations
- Cannot access internal/private TOU Mira members
- Role/modifier parsing falls back to string parsing if reflection fails
- Platform detection may fail on some Among Us versions (defaults to "Unknown")
- No compile-time type safety (all reflection is string-based)
- Breaking changes in TOU Mira require plugin updates
- Symbol encoding may vary by console/log viewer (e.g., `♥` may display as `ÔÖą` in logs)