using NeoModLoader.services; using UnityEngine; namespace IdleSpectator; /// /// Samples focus / spectator / power-bar state and logs only on changes or faults. /// Tail Player.log for [IdleSpectator][STATE] and [IdleSpectator][BAD]. /// public static class StateProbe { private static bool _prevIdle; private static bool _prevFocus; private static bool _prevVanillaSpec; private static bool _prevBar; private static string _prevUnit = ""; private static float _focusLostSince = -1f; private static float _lastHeartbeatAt = -999f; private static float _lastBadPowerBarAt = -999f; private static float _lastBadGapAt = -999f; private static bool _hadFocusWhileIdle; private const float HeartbeatSeconds = 10f; private const float FocusGapBadSeconds = 0.35f; public static int BadEventCount { get; private set; } public static int StateEventCount { get; private set; } public static void ResetCounters() { BadEventCount = 0; StateEventCount = 0; _lastBadPowerBarAt = -999f; _lastBadGapAt = -999f; _focusLostSince = -1f; _lastHeartbeatAt = -999f; _hadFocusWhileIdle = false; } public static void Update() { if (!ModSettings.DebugStateProbe || !Config.game_loaded || World.world == null) { return; } bool idle = SpectatorMode.Active; bool focus = MoveCamera.hasFocusUnit(); bool vanillaSpec = MoveCamera.inSpectatorMode(); bool bar = CanvasMain.isBottomBarShowing(); string unit = FocusUnitLabel(); bool changed = idle != _prevIdle || focus != _prevFocus || vanillaSpec != _prevVanillaSpec || bar != _prevBar || unit != _prevUnit; if (changed) { StateEventCount++; LogService.LogInfo( $"[IdleSpectator][STATE] idle={idle} focus={focus} vanillaSpec={vanillaSpec} powerBar={bar} unit={unit}"); } if (idle && focus) { _hadFocusWhileIdle = true; } if (idle && _prevFocus && !focus) { BadEventCount++; LogService.LogInfo("[IdleSpectator][BAD] focus dropped while Idle Spectator is on"); _focusLostSince = Time.unscaledTime; } // Only treat prolonged no-focus as bad after we have successfully acquired a target. // Fresh maps often have a short empty window before the first unit exists. if (idle && !focus && _hadFocusWhileIdle) { if (_focusLostSince < 0f) { _focusLostSince = Time.unscaledTime; } else if (Time.unscaledTime - _focusLostSince >= FocusGapBadSeconds && Time.unscaledTime - _lastBadGapAt >= 1f) { _lastBadGapAt = Time.unscaledTime; BadEventCount++; LogService.LogInfo( $"[IdleSpectator][BAD] no focus unit for {Time.unscaledTime - _focusLostSince:0.00}s while idle on (power bar risk)"); } } else if (focus) { _focusLostSince = -1f; } if (idle && bar && Time.unscaledTime - _lastBadPowerBarAt >= 1f) { _lastBadPowerBarAt = Time.unscaledTime; BadEventCount++; LogService.LogInfo("[IdleSpectator][BAD] power bar showing while Idle Spectator is on"); } if (idle && Time.unscaledTime - _lastHeartbeatAt >= HeartbeatSeconds) { _lastHeartbeatAt = Time.unscaledTime; StateEventCount++; LogService.LogInfo( $"[IdleSpectator][STATE] heartbeat idle={idle} focus={focus} vanillaSpec={vanillaSpec} powerBar={bar} unit={unit}"); } _prevIdle = idle; _prevFocus = focus; _prevVanillaSpec = vanillaSpec; _prevBar = bar; _prevUnit = unit; } public static void Reset() { _prevIdle = false; _prevFocus = false; _prevVanillaSpec = false; _prevBar = false; _prevUnit = ""; _focusLostSince = -1f; _lastHeartbeatAt = -999f; ResetCounters(); } private static string FocusUnitLabel() { if (!MoveCamera.hasFocusUnit()) { return "-"; } Actor unit = MoveCamera._focus_unit; if (unit == null) { return "?"; } try { string name = unit.getName(); if (!string.IsNullOrEmpty(name)) { return name; } } catch { // ignore } return unit.asset != null ? unit.asset.id : "unit"; } }