Files
MiraExporter/CLAUDE.md
2025-10-10 23:00:21 +02:00

12 KiB

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

# 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:

// 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):

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:

  • \u2665Lovers 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