# 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_.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**: `SYMBOL` 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 `SYMBOL` 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: "boracik " 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()` ### 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: ``, ``, ``, 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 `SYMBOL` 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)