326 lines
12 KiB
Markdown
326 lines
12 KiB
Markdown
# 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.
|
|
|
|
### Game Duration Tracking
|
|
The plugin tracks actual gameplay duration (not lobby time) using `GameStartTimePatch`:
|
|
- Patches `ShipStatus.Begin` to capture when the game round starts
|
|
- Stores start time as `Time.time` when gameplay begins
|
|
- Calculates duration as difference between game end time and start time
|
|
- Resets on each game to ensure accurate per-game timing
|
|
|
|
### 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, GameStartTimePatch)
|
|
|
|
### Game Start Flow
|
|
1. **ShipStatus.Begin** fires (game round starts, gameplay begins)
|
|
2. **GameStartTimePatch.OnShipStatusBegin** runs - records `Time.time` as game start time
|
|
|
|
### Export Flow (Game End)
|
|
1. **AmongUsClient.OnGameEnd** fires (game ends)
|
|
2. **GameStartTimePatch.OnGameEndPrefix** runs - logs game duration
|
|
3. **TOU Mira's patch** runs (High priority) - populates EndGameData.PlayerRecords
|
|
4. **EndGameExportPatch.Postfix** runs (Low priority) - starts export
|
|
- Checks for Hide & Seek mode (skip if true)
|
|
- Fires Task.Run() with `StatsExporter.ExportGameStatsAsync()`
|
|
5. **StatsExporter.ExportGameStatsAsync()**
|
|
- Reads `ApiSet.ini` config
|
|
- Calls reflection bridge methods to collect data
|
|
- Transforms data via `DataTransformer.TransformToExportFormat()`
|
|
- Uses `GameStartTimePatch.GetGameDuration()` for accurate game time
|
|
- Saves local backup if enabled
|
|
- POSTs to API via `ApiClient.SendToApiAsync()`
|
|
6. **GameStartTimePatch.OnGameEndPostfix** runs - clears game start time for next game
|
|
|
|
### 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, actual gameplay time from intro end to game end)
|
|
│ └── 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)
|
|
- to memorize |