using System; using System.Runtime.InteropServices; using Dalamud.Game.NativeWrapper; using Dalamud.Hooking; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Component.GUI; namespace QuestionableCompanion.Services; public class ErrorRecoveryService : IDisposable { private delegate char LobbyErrorHandlerDelegate(long a1, long a2, long a3); private readonly IPluginLog log; private readonly IGameInteropProvider hookProvider; private readonly IClientState clientState; private readonly IFramework framework; private readonly IGameGui gameGui; private readonly AutoRetainerIPC? autoRetainerIPC; private Hook? lobbyErrorHandlerHook; private DateTime lastDialogClickTime = DateTime.MinValue; public bool IsErrorDisconnect { get; private set; } public string? LastDisconnectedCharacter { get; private set; } public ErrorRecoveryService(IPluginLog log, IGameInteropProvider hookProvider, IClientState clientState, IFramework framework, IGameGui gameGui, AutoRetainerIPC? autoRetainerIPC = null) { this.log = log; this.hookProvider = hookProvider; this.clientState = clientState; this.framework = framework; this.gameGui = gameGui; this.autoRetainerIPC = autoRetainerIPC; framework.Update += OnFrameworkUpdate; InitializeHook(); } private void InitializeHook() { try { lobbyErrorHandlerHook = hookProvider.HookFromSignature("40 53 48 83 EC 30 48 8B D9 49 8B C8 E8 ?? ?? ?? ?? 8B D0", LobbyErrorHandlerDetour); if (lobbyErrorHandlerHook != null && lobbyErrorHandlerHook.Address != IntPtr.Zero) { lobbyErrorHandlerHook.Enable(); } } catch (Exception) { } } private char LobbyErrorHandlerDetour(long a1, long a2, long a3) { try { nint p3 = new IntPtr(a3); byte t1 = Marshal.ReadByte(p3); int num = (((t1 & 0xF) > 0) ? Marshal.ReadInt32(p3 + 8) : 0); _ = 0; if (num != 0) { try { if (autoRetainerIPC != null) { string currentChar = autoRetainerIPC.GetCurrentCharacter(); if (!string.IsNullOrEmpty(currentChar)) { LastDisconnectedCharacter = currentChar; } } } catch (Exception) { } Marshal.WriteInt64(p3 + 8, 16000L); IsErrorDisconnect = true; if ((t1 & 0xF) > 0) { Marshal.ReadInt32(p3 + 8); } else _ = 0; } } catch (Exception) { } return lobbyErrorHandlerHook.Original(a1, a2, a3); } private unsafe void OnFrameworkUpdate(IFramework framework) { try { AtkUnitBasePtr dialoguePtr = gameGui.GetAddonByName("Dialogue"); if (dialoguePtr == IntPtr.Zero) { return; } AtkUnitBase* dialogueAddon = (AtkUnitBase*)(nint)dialoguePtr; if (dialogueAddon == null || !dialogueAddon->IsVisible || (DateTime.Now - lastDialogClickTime).TotalMilliseconds < 1000.0) { return; } AtkTextNode* textNode = dialogueAddon->GetTextNodeById(3u); if (textNode == null) { return; } string text = textNode->NodeText.ToString(); if (string.IsNullOrEmpty(text) || (!text.Contains("server", StringComparison.OrdinalIgnoreCase) && !text.Contains("connection", StringComparison.OrdinalIgnoreCase) && !text.Contains("error", StringComparison.OrdinalIgnoreCase) && !text.Contains("lost", StringComparison.OrdinalIgnoreCase))) { return; } IsErrorDisconnect = true; try { if (autoRetainerIPC != null) { string currentChar = autoRetainerIPC.GetCurrentCharacter(); if (!string.IsNullOrEmpty(currentChar)) { LastDisconnectedCharacter = currentChar; } } } catch { } try { AtkComponentButton* button = dialogueAddon->GetComponentButtonById(4u); if (button != null) { AtkResNode btnRes = button->AtkComponentBase.OwnerNode->AtkResNode; AtkEvent* evt = btnRes.AtkEventManager.Event; dialogueAddon->ReceiveEvent(evt->State.EventType, (int)evt->Param, btnRes.AtkEventManager.Event, null); lastDialogClickTime = DateTime.Now; } } catch (Exception) { } } catch (Exception) { } } public void Reset() { IsErrorDisconnect = false; LastDisconnectedCharacter = null; } public bool RequestRelog() { if (autoRetainerIPC == null || !autoRetainerIPC.IsAvailable) { log.Warning("[ErrorRecovery] AutoRetainer IPC not available - cannot relog"); return false; } if (string.IsNullOrEmpty(LastDisconnectedCharacter)) { log.Warning("[ErrorRecovery] No character to relog to"); return false; } log.Information("[ErrorRecovery] Requesting AutoRetainer relog to: " + LastDisconnectedCharacter); return autoRetainerIPC.SwitchCharacter(LastDisconnectedCharacter); } public void Dispose() { try { framework.Update -= OnFrameworkUpdate; if (lobbyErrorHandlerHook != null) { lobbyErrorHandlerHook.Disable(); lobbyErrorHandlerHook.Dispose(); } } catch (Exception) { } } }