From 0177220d895187154ed61c46d423ea0d05f6b31a Mon Sep 17 00:00:00 2001 From: Bartosz Gradzik Date: Fri, 10 Oct 2025 23:00:21 +0200 Subject: [PATCH] Poprawka czasu gry --- CLAUDE.md | 25 ++++++++--- Export/DataTransformer.cs | 6 ++- Patches/GameStartTimePatch.cs | 79 +++++++++++++++++++++++++++++++++++ TownOfUsStatsExporter.csproj | 2 +- TownOfUsStatsPlugin.cs | 2 +- 5 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 Patches/GameStartTimePatch.cs diff --git a/CLAUDE.md b/CLAUDE.md index 9fb3a92..cace068 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,6 +18,13 @@ The plugin **never directly references TOU Mira types**. All access is via refle ### 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. @@ -63,20 +70,27 @@ If the `AmongUs` environment variable is set, the DLL auto-copies to `$(AmongUs) 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) +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. **TOU Mira's patch** runs (High priority) - populates EndGameData.PlayerRecords -3. **EndGameExportPatch.Postfix** runs (Low priority) - starts export +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()` -4. **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 @@ -164,7 +178,7 @@ GameStatsData │ ├── Timestamp (UTC) │ ├── LobbyCode (e.g., "ABCDEF") │ ├── GameMode (Normal/HideNSeek) -│ ├── Duration (seconds) +│ ├── Duration (seconds, actual gameplay time from intro end to game end) │ └── Map (string) ├── Players[] (list) │ ├── PlayerId (byte) @@ -309,3 +323,4 @@ Symbols are extracted **before** text cleaning to preserve information: - 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 \ No newline at end of file diff --git a/Export/DataTransformer.cs b/Export/DataTransformer.cs index 709c7a7..2f73e33 100644 --- a/Export/DataTransformer.cs +++ b/Export/DataTransformer.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using TownOfUsStatsExporter.Models; +using TownOfUsStatsExporter.Patches; using UnityEngine; namespace TownOfUsStatsExporter.Export; @@ -72,13 +73,16 @@ public static class DataTransformer private static GameInfoData BuildGameInfo() { + // Get actual game duration (from intro end to game end) instead of total Time.time + var gameDuration = GameStartTimePatch.GetGameDuration(); + return new GameInfoData { GameId = Guid.NewGuid().ToString(), Timestamp = DateTime.UtcNow, LobbyCode = InnerNet.GameCode.IntToGameName(AmongUsClient.Instance.GameId), GameMode = GameOptionsManager.Instance?.CurrentGameOptions?.GameMode.ToString() ?? "Unknown", - Duration = Time.time, + Duration = gameDuration, Map = GetMapName((byte)(GameOptionsManager.Instance?.CurrentGameOptions?.MapId ?? 0)), }; } diff --git a/Patches/GameStartTimePatch.cs b/Patches/GameStartTimePatch.cs new file mode 100644 index 0000000..fcb187f --- /dev/null +++ b/Patches/GameStartTimePatch.cs @@ -0,0 +1,79 @@ +using HarmonyLib; +using UnityEngine; + +namespace TownOfUsStatsExporter.Patches; + +/// +/// Tracks when the game actually starts (after intro cutscene ends). +/// This allows calculating actual game duration instead of lobby time. +/// +[HarmonyPatch] +public static class GameStartTimePatch +{ + /// + /// Time when the game started (after intro cutscene), in Unity Time.time format. + /// Null if game hasn't started yet. + /// + public static float? GameStartTime { get; private set; } + + /// + /// Duration of the last completed game in seconds. + /// Set when the game ends, persists until next game starts. + /// + public static float LastGameDuration { get; private set; } + + /// + /// Gets the duration of the last completed game in seconds. + /// This value is set when the game ends and remains available for async export. + /// + public static float GetGameDuration() + { + return LastGameDuration; + } + + /// + /// Patch on ShipStatus.Begin - called when the game round starts. + /// This is more reliable than IntroCutscene.OnDestroy as it's always called when gameplay begins. + /// + [HarmonyPatch(typeof(ShipStatus), nameof(ShipStatus.Begin))] + [HarmonyPostfix] + public static void OnShipStatusBegin() + { + GameStartTime = Time.time; + LastGameDuration = 0f; // Reset duration for new game + TownOfUsStatsPlugin.Logger.LogInfo($"Game started at Time.time = {GameStartTime.Value:F2}"); + } + + /// + /// Calculate and store game duration when game ends. + /// This runs before the export, so duration is available for async export. + /// + [HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnGameEnd))] + [HarmonyPrefix] + public static void OnGameEndPrefix() + { + if (GameStartTime.HasValue) + { + LastGameDuration = Time.time - GameStartTime.Value; + TownOfUsStatsPlugin.Logger.LogInfo($"Game ended. Duration: {LastGameDuration:F2} seconds ({LastGameDuration / 60:F2} minutes)"); + } + else + { + LastGameDuration = 0f; + TownOfUsStatsPlugin.Logger.LogWarning("Game ended but GameStartTime was not set! Duration will be 0."); + } + } + + /// + /// Clear game start time after game ends to prepare for next game. + /// LastGameDuration is preserved for async export. + /// + [HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnGameEnd))] + [HarmonyPostfix] + [HarmonyPriority(Priority.Last)] + public static void OnGameEndPostfix() + { + GameStartTime = null; + TownOfUsStatsPlugin.Logger.LogInfo("Game start time cleared (duration preserved for export)"); + } +} diff --git a/TownOfUsStatsExporter.csproj b/TownOfUsStatsExporter.csproj index 3467216..fd4f1f1 100644 --- a/TownOfUsStatsExporter.csproj +++ b/TownOfUsStatsExporter.csproj @@ -8,7 +8,7 @@ false Town Of Us Stats Exporter - Standalone plugin for exporting game statistics TownOfUsStatsExporter - 1.0.3 + 1.0.4 ToU Mira Team diff --git a/TownOfUsStatsPlugin.cs b/TownOfUsStatsPlugin.cs index ee3a4bb..ce59955 100644 --- a/TownOfUsStatsPlugin.cs +++ b/TownOfUsStatsPlugin.cs @@ -32,7 +32,7 @@ public class TownOfUsStatsPlugin : BasePlugin /// /// Plugin version. /// - public const string PluginVersion = "1.0.3"; + public const string PluginVersion = "1.0.4"; /// /// Logger instance for the plugin.