qstbak/Questionable/Questionable.Controller.GameUi/CreditsController.cs
2026-08-17 20:29:32 +10:00

249 lines
7.8 KiB
C#

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<CreditsController> _logger;
private static readonly object _lock = new object();
public CreditsController(IAddonLifecycle addonLifecycle, Configuration configuration, ICondition condition, ILogger<CreditsController> 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).");
}
}
}