using System; using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.ClientState.Conditions; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Component.GUI; using Microsoft.Extensions.Logging; namespace Questionable.Controller.GameUi; internal sealed class CreditsController : IDisposable { private static CreditsController? _instance; private static IAddonLifecycle? _registeredLifecycle; private static readonly IAddonLifecycle.AddonEventDelegate _creditScrollHandler = delegate(AddonEvent t, AddonArgs a) { _instance?.CreditScrollPostSetup(t, a); }; private static readonly IAddonLifecycle.AddonEventDelegate _creditHandler = delegate(AddonEvent t, AddonArgs a) { _instance?.CreditPostSetup(t, a); }; private static readonly IAddonLifecycle.AddonEventDelegate _creditPlayerHandler = delegate(AddonEvent t, AddonArgs a) { _instance?.CreditPlayerPostSetup(t, a); }; private static readonly string[] CreditScrollArray = new string[1] { "CreditScroll" }; private static readonly string[] CreditArray = new string[1] { "Credit" }; private static readonly string[] CreditPlayerArray = new string[1] { "CreditPlayer" }; private static bool _deferDisposeUntilCutsceneEnds; private static bool _pendingDispose; private static int _handledConsecutiveCutscenes; private static int _maxConsecutiveSkips = 5; private static long? _lastHandledAt; private const long ReentrancySuppressMs = 500L; private static long _lastHandledAddr; private const long ConsecutiveResetWindowMs = 10000L; private readonly IAddonLifecycle _addonLifecycle; private readonly Configuration _configuration; private readonly ICondition _condition; private readonly ILogger _logger; private static readonly object _lock = new object(); public CreditsController(IAddonLifecycle addonLifecycle, Configuration configuration, ICondition condition, ILogger logger) { _addonLifecycle = addonLifecycle; _configuration = configuration; _condition = condition; _logger = logger; lock (_lock) { if (_instance == null) { _instance = this; _registeredLifecycle = _addonLifecycle; _addonLifecycle.RegisterListener(AddonEvent.PostSetup, CreditScrollArray, _creditScrollHandler); _addonLifecycle.RegisterListener(AddonEvent.PostSetup, CreditArray, _creditHandler); _addonLifecycle.RegisterListener(AddonEvent.PostSetup, CreditPlayerArray, _creditPlayerHandler); _condition.ConditionChange += OnConditionChange; _logger.LogDebug("CreditsController: registered listeners and ready to skip up to {MaxSkips} successive cutscenes.", _maxConsecutiveSkips); } } } private void OnConditionChange(ConditionFlag flag, bool value) { if ((flag == ConditionFlag.OccupiedInCutSceneEvent || flag == ConditionFlag.WatchingCutscene || flag == ConditionFlag.WatchingCutscene78) ? true : false) { if (value) { DeferDisposeUntilCutsceneEnds(); } else if (!_condition[ConditionFlag.OccupiedInCutSceneEvent] && !_condition[ConditionFlag.WatchingCutscene] && !_condition[ConditionFlag.WatchingCutscene78]) { NotifyCutsceneEnded(); } } } private unsafe void CreditScrollPostSetup(AddonEvent type, AddonArgs args) { HandleCutscene(delegate { _logger.LogDebug("CreditScrollPostSetup: attempting to close credits sequence (scroll)."); AtkUnitBase* address = (AtkUnitBase*)args.Addon.Address; address->FireCallbackInt(-2); }, args); } private unsafe void CreditPostSetup(AddonEvent type, AddonArgs args) { HandleCutscene(delegate { _logger.LogDebug("CreditPostSetup: attempting to close credits sequence."); AtkUnitBase* address = (AtkUnitBase*)args.Addon.Address; address->FireCallbackInt(-2); }, args); } private unsafe void CreditPlayerPostSetup(AddonEvent type, AddonArgs args) { HandleCutscene(delegate { _logger.LogDebug("CreditPlayerPostSetup: attempting to close CreditPlayer."); AtkUnitBase* address = (AtkUnitBase*)args.Addon.Address; address->Close(fireCallback: true); }, args); } private void HandleCutscene(Action nativeAction, AddonArgs args) { if (_configuration.General.CinemaMode) { _logger.LogDebug("HandleCutscene: Cinema Mode enabled, not skipping cutscene."); return; } long num = ((args.Addon.Address == IntPtr.Zero) ? 0 : ((IntPtr)args.Addon.Address).ToInt64()); if (num == 0L) { _logger.LogDebug("HandleCutscene: Addon address is zero, skipping."); return; } lock (_lock) { long? lastHandledAt = _lastHandledAt; if (lastHandledAt.HasValue) { long valueOrDefault = lastHandledAt.GetValueOrDefault(); if (Environment.TickCount64 - valueOrDefault > 10000) { if (_handledConsecutiveCutscenes != 0) { _logger.LogDebug("HandleCutscene: long pause detected ({ElapsedMs}ms), resetting consecutive counter (was {Was}).", Environment.TickCount64 - valueOrDefault, _handledConsecutiveCutscenes); } _handledConsecutiveCutscenes = 0; } } lastHandledAt = _lastHandledAt; if (lastHandledAt.HasValue) { long valueOrDefault2 = lastHandledAt.GetValueOrDefault(); if (Environment.TickCount64 - valueOrDefault2 < 500 && num == _lastHandledAddr) { _logger.LogDebug("HandleCutscene: suppressed re-entrant invocation for same addon (addr=0x{Address:X}).", num); return; } } if (_handledConsecutiveCutscenes >= _maxConsecutiveSkips) { _logger.LogInformation("HandleCutscene: reached max consecutive skips ({MaxSkips}), pausing until cutscenes stop for a while.", _maxConsecutiveSkips); return; } _lastHandledAt = Environment.TickCount64; _lastHandledAddr = num; _handledConsecutiveCutscenes++; _logger.LogDebug("HandleCutscene: handling cutscene #{Count} (addr=0x{Address:X}).", _handledConsecutiveCutscenes, num); } try { nativeAction(); _logger.LogDebug("HandleCutscene: native action executed for addon at 0x{Address:X}.", num); } catch (Exception exception) { _logger.LogError(exception, "HandleCutscene: exception while executing native action for addon at 0x{Address:X}.", num); } _logger.LogDebug("HandleCutscene: handled {Count}/{Max} consecutive cutscenes.", _handledConsecutiveCutscenes, _maxConsecutiveSkips); } public static void DeferDisposeUntilCutsceneEnds() { lock (_lock) { _deferDisposeUntilCutsceneEnds = true; if (_instance != null) { _instance._logger.LogDebug("CreditsController: deferring dispose until cutscene ends."); } } } public static void NotifyCutsceneEnded() { lock (_lock) { _deferDisposeUntilCutsceneEnds = false; if (_instance != null) { _instance._logger.LogDebug("CreditsController: cutscene ended, processing pending dispose state."); } if (_pendingDispose && _instance != null) { _instance._logger.LogDebug("CreditsController: pending dispose detected, performing unregister now."); _instance.Dispose(); } } } public void Dispose() { lock (_lock) { if (_instance != this) { return; } if (_deferDisposeUntilCutsceneEnds) { _pendingDispose = true; _logger.LogDebug("CreditsController.Dispose deferred until cutscene end."); return; } if (_registeredLifecycle != null) { _registeredLifecycle.UnregisterListener(AddonEvent.PostSetup, CreditPlayerArray, _creditPlayerHandler); _registeredLifecycle.UnregisterListener(AddonEvent.PostSetup, CreditArray, _creditHandler); _registeredLifecycle.UnregisterListener(AddonEvent.PostSetup, CreditScrollArray, _creditScrollHandler); } _condition.ConditionChange -= OnConditionChange; _registeredLifecycle = null; _instance = null; _pendingDispose = false; _handledConsecutiveCutscenes = 0; _lastHandledAddr = 0L; _lastHandledAt = null; _logger.LogDebug("CreditsController listeners unregistered and disposed (primary instance)."); } } }