using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Party; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Group; using Lumina.Excel; using Lumina.Excel.Sheets; using QuestionableCompanion.Models; namespace QuestionableCompanion.Services; public class HelperManager : IDisposable { private readonly Configuration configuration; private readonly IPluginLog log; private readonly ICommandManager commandManager; private readonly ICondition condition; private readonly IClientState clientState; private readonly IFramework framework; private readonly PartyInviteService partyInviteService; private readonly MultiClientIPC multiClientIPC; private readonly CrossProcessIPC crossProcessIPC; private readonly PartyInviteAutoAccept partyInviteAutoAccept; private readonly MemoryHelper memoryHelper; private readonly LANHelperClient? lanHelperClient; private readonly IPartyList partyList; private bool isInDuty; private List<(string Name, ushort WorldId)> availableHelpers = new List<(string, ushort)>(); private Dictionary<(string, ushort), bool> helperReadyStatus = new Dictionary<(string, ushort), bool>(); public HelperManager(Configuration configuration, IPluginLog log, ICommandManager commandManager, ICondition condition, IClientState clientState, IFramework framework, PartyInviteService partyInviteService, MultiClientIPC multiClientIPC, CrossProcessIPC crossProcessIPC, PartyInviteAutoAccept partyInviteAutoAccept, MemoryHelper memoryHelper, LANHelperClient? lanHelperClient, IPartyList partyList) { this.configuration = configuration; this.log = log; this.commandManager = commandManager; this.condition = condition; this.clientState = clientState; this.framework = framework; this.partyInviteService = partyInviteService; this.multiClientIPC = multiClientIPC; this.crossProcessIPC = crossProcessIPC; this.memoryHelper = memoryHelper; this.lanHelperClient = lanHelperClient; this.partyInviteAutoAccept = partyInviteAutoAccept; this.partyList = partyList; condition.ConditionChange += OnConditionChanged; multiClientIPC.OnHelperRequested += OnHelperRequested; multiClientIPC.OnHelperDismissed += OnHelperDismissed; multiClientIPC.OnHelperAvailable += OnHelperAvailable; crossProcessIPC.OnHelperRequested += OnHelperRequested; crossProcessIPC.OnHelperDismissed += OnHelperDismissed; crossProcessIPC.OnHelperAvailable += OnHelperAvailable; crossProcessIPC.OnHelperReady += OnHelperReady; crossProcessIPC.OnHelperInParty += OnHelperInParty; crossProcessIPC.OnHelperInDuty += OnHelperInDuty; crossProcessIPC.OnRequestHelperAnnouncements += OnRequestHelperAnnouncements; if (configuration.IsHighLevelHelper) { log.Information("[HelperManager] Will announce helper availability on next frame"); } log.Information("[HelperManager] Initialized"); } public void AnnounceIfHelper() { if (configuration.IsHighLevelHelper) { IPlayerCharacter localPlayer = clientState.LocalPlayer; if (localPlayer == null) { log.Warning("[HelperManager] LocalPlayer is null, cannot announce helper"); return; } string localName = localPlayer.Name.ToString(); ushort localWorldId = (ushort)localPlayer.HomeWorld.RowId; multiClientIPC.AnnounceHelperAvailable(localName, localWorldId); crossProcessIPC.AnnounceHelper(); log.Information($"[HelperManager] Announced as helper: {localName}@{localWorldId} (both IPC systems)"); } } public void InviteHelpers() { if (!configuration.IsQuester) { log.Debug("[HelperManager] Not a Quester, skipping helper invites"); return; } if (configuration.HelperSelection == HelperSelectionMode.ManualInput) { if (string.IsNullOrEmpty(configuration.ManualHelperName)) { log.Warning("[HelperManager] Manual Input mode selected but no helper name configured!"); return; } Task.Run(async delegate { log.Information("[HelperManager] Manual Input mode: Inviting " + configuration.ManualHelperName); string[] parts = configuration.ManualHelperName.Split('@'); if (parts.Length != 2) { log.Error("[HelperManager] Invalid manual helper format: " + configuration.ManualHelperName + " (expected: CharacterName@WorldName)"); } else { string helperName = parts[0].Trim(); string worldName = parts[1].Trim(); ushort worldId = 0; ExcelSheet worldSheet = Plugin.DataManager.GetExcelSheet(); if (worldSheet != null) { foreach (World world in worldSheet) { if (world.Name.ExtractText().Equals(worldName, StringComparison.OrdinalIgnoreCase)) { worldId = (ushort)world.RowId; break; } } } if (worldId == 0) { log.Error("[HelperManager] Could not find world ID for: " + worldName); } else { log.Information($"[HelperManager] Resolved helper: {helperName}@{worldId} ({worldName})"); bool alreadyInParty = false; if (partyList != null) { foreach (IPartyMember member in partyList) { if (member.Name.ToString() == helperName && member.World.RowId == worldId) { alreadyInParty = true; break; } } } if (alreadyInParty) { log.Information("[HelperManager] helper " + helperName + " is ALREADY in party! Skipping disband/invite."); } else { DisbandParty(); await Task.Delay(500); log.Information("[HelperManager] Sending direct invite to " + helperName + " (Manual Input - no IPC wait)"); if (partyInviteService.InviteToParty(helperName, worldId)) { log.Information("[HelperManager] Successfully invited " + helperName); } else { log.Error("[HelperManager] Failed to invite " + helperName); } } } } }); return; } log.Information("[HelperManager] Requesting helper announcements..."); RequestHelperAnnouncements(); Task.Run(async delegate { await Task.Delay(1000); List<(string Name, ushort WorldId)> helpersToInvite = new List<(string, ushort)>(); if (configuration.HelperSelection == HelperSelectionMode.Auto) { if (availableHelpers.Count == 0) { log.Warning("[HelperManager] No helpers available via IPC!"); if (lanHelperClient != null) { log.Information("[HelperManager] Checking for LAN helpers..."); LANHelperInfo lanHelper = lanHelperClient.GetFirstAvailableHelper(); if (lanHelper != null) { log.Information("[HelperManager] Found LAN helper: " + lanHelper.Name + " at " + lanHelper.IPAddress); await InviteLANHelper(lanHelper.IPAddress, lanHelper.Name, lanHelper.WorldId); return; } } log.Warning("[HelperManager] Make sure helper clients are running with 'I'm a High-Level Helper' enabled"); return; } helpersToInvite.AddRange(availableHelpers); log.Information($"[HelperManager] Auto mode: Inviting {helpersToInvite.Count} AUTO-DISCOVERED helper(s)..."); } else if (configuration.HelperSelection == HelperSelectionMode.Dropdown) { if (string.IsNullOrEmpty(configuration.PreferredHelper)) { log.Warning("[HelperManager] Dropdown mode selected but no helper chosen!"); return; } string[] parts = configuration.PreferredHelper.Split('@'); if (parts.Length != 2) { log.Error("[HelperManager] Invalid preferred helper format: " + configuration.PreferredHelper); return; } string helperName = parts[0].Trim(); string worldName = parts[1].Trim(); (string, ushort) matchingHelper = availableHelpers.FirstOrDefault<(string, ushort)>(delegate((string Name, ushort WorldId) h) { ExcelSheet excelSheet = Plugin.DataManager.GetExcelSheet(); string text2 = "Unknown"; if (excelSheet != null) { foreach (World current in excelSheet) { if (current.RowId == h.WorldId) { text2 = current.Name.ExtractText(); break; } } } return h.Name == helperName && text2 == worldName; }); var (text, num) = matchingHelper; if (text == null && num == 0) { log.Warning("[HelperManager] Preferred helper " + configuration.PreferredHelper + " not found in discovered helpers!"); return; } helpersToInvite.Add(matchingHelper); log.Information("[HelperManager] Dropdown mode: Inviting selected helper " + configuration.PreferredHelper); } bool allHelpersPresent = false; if (partyList != null && partyList.Length > 0 && helpersToInvite.Count > 0) { int presentCount = 0; foreach (var (hName, hWorld) in helpersToInvite) { foreach (IPartyMember member in partyList) { if (member.Name.ToString() == hName && member.World.RowId == hWorld) { presentCount++; break; } } } if (presentCount >= helpersToInvite.Count) { allHelpersPresent = true; } } if (allHelpersPresent) { log.Information("[HelperManager] All desired helpers are ALREADY in party! Skipping disband."); } else if (partyList != null && partyList.Length > 1) { bool anyHelperPresent = false; foreach (var (hName2, hWorld2) in helpersToInvite) { foreach (IPartyMember member2 in partyList) { if (member2.Name.ToString() == hName2 && member2.World.RowId == hWorld2) { anyHelperPresent = true; break; } } } if (anyHelperPresent) { log.Information("[HelperManager] Some helpers already in party - NOT disbanding, simply inviting remaining."); } else { DisbandParty(); await Task.Delay(500); } } else { DisbandParty(); await Task.Delay(500); } foreach (var (name, worldId) in helpersToInvite) { if (string.IsNullOrEmpty(name) || worldId == 0) { log.Warning($"[HelperManager] Invalid helper: {name}@{worldId}"); } else { log.Information($"[HelperManager] Requesting helper: {name}@{worldId}"); helperReadyStatus[(name, worldId)] = false; multiClientIPC.RequestHelper(name, worldId); crossProcessIPC.RequestHelper(name, worldId); log.Information("[HelperManager] Waiting for " + name + " to be ready..."); DateTime timeout = DateTime.Now.AddSeconds(10.0); while (!helperReadyStatus.GetValueOrDefault((name, worldId), defaultValue: false) && DateTime.Now < timeout) { await Task.Delay(100); } if (!helperReadyStatus.GetValueOrDefault((name, worldId), defaultValue: false)) { log.Warning("[HelperManager] Timeout waiting for " + name + " to be ready!"); } else { log.Information("[HelperManager] " + name + " is ready! Sending invite..."); if (partyInviteService.InviteToParty(name, worldId)) { log.Information("[HelperManager] Successfully invited " + name); } else { log.Error("[HelperManager] Failed to invite " + name); } await Task.Delay(500); } } } }); } public List<(string Name, ushort WorldId)> GetAvailableHelpers() { List<(string, ushort)> allHelpers = new List<(string, ushort)>(availableHelpers); if (lanHelperClient != null) { foreach (LANHelperInfo lanHelper in lanHelperClient.DiscoveredHelpers) { if (!allHelpers.Any<(string, ushort)>(((string Name, ushort WorldId) h) => h.Name == lanHelper.Name && h.WorldId == lanHelper.WorldId)) { allHelpers.Add((lanHelper.Name, lanHelper.WorldId)); } } } return allHelpers; } private void LeaveParty() { try { log.Information("[HelperManager] Leaving party"); framework.RunOnFrameworkThread(delegate { memoryHelper.SendChatMessage("/leave"); log.Information("[HelperManager] /leave command sent via UIModule"); }); } catch (Exception ex) { log.Error("[HelperManager] Failed to leave party: " + ex.Message); } } public void DisbandParty() { try { log.Information("[HelperManager] Disbanding party"); framework.RunOnFrameworkThread(delegate { memoryHelper.SendChatMessage("/leave"); log.Information("[HelperManager] /leave command sent via UIModule"); }); multiClientIPC.DismissHelper(); crossProcessIPC.DismissHelper(); } catch (Exception ex) { log.Error("[HelperManager] Failed to disband party: " + ex.Message); } } private void OnConditionChanged(ConditionFlag flag, bool value) { if (flag == ConditionFlag.BoundByDuty) { if (value && !isInDuty) { isInDuty = true; OnDutyEnter(); } else if (!value && isInDuty) { isInDuty = false; OnDutyLeave(); } } } private void OnDutyEnter() { log.Debug("[HelperManager] Entered duty"); if (!configuration.IsHighLevelHelper) { return; } configuration.CurrentHelperStatus = HelperStatus.InDungeon; configuration.Save(); log.Information("[HelperManager] Helper status: InDungeon"); IPlayerCharacter localPlayer = clientState.LocalPlayer; if (localPlayer != null) { string helperName = localPlayer.Name.ToString(); ushort helperWorld = (ushort)localPlayer.HomeWorld.RowId; crossProcessIPC.BroadcastHelperStatus(helperName, helperWorld, "InDungeon"); } log.Information("[HelperManager] Starting AutoDuty (High-Level Helper)"); Task.Run(async delegate { log.Information("[HelperManager] Waiting 5s before starting AutoDuty..."); await Task.Delay(5000); framework.RunOnFrameworkThread(delegate { try { commandManager.ProcessCommand("/ad start"); log.Information("[HelperManager] AutoDuty started"); } catch (Exception ex) { log.Error("[HelperManager] Failed to start AutoDuty: " + ex.Message); } }); }); } private unsafe void OnDutyLeave() { log.Information("[HelperManager] Left duty"); if (configuration.IsHighLevelHelper) { if (configuration.CurrentHelperStatus == HelperStatus.InDungeon) { configuration.CurrentHelperStatus = HelperStatus.Available; configuration.Save(); log.Information("[HelperManager] Helper status: Available"); IPlayerCharacter localPlayer = clientState.LocalPlayer; if (localPlayer != null) { string helperName = localPlayer.Name.ToString(); ushort helperWorld = (ushort)localPlayer.HomeWorld.RowId; crossProcessIPC.BroadcastHelperStatus(helperName, helperWorld, "Available"); } } log.Information("[HelperManager] Stopping AutoDuty (High-Level Helper)"); framework.RunOnFrameworkThread(delegate { try { commandManager.ProcessCommand("/ad stop"); log.Information("[HelperManager] AutoDuty stopped"); } catch (Exception ex) { log.Error("[HelperManager] Failed to stop AutoDuty: " + ex.Message); } }); log.Information("[HelperManager] Leaving party after duty (High-Level Helper)"); Task.Run(async delegate { log.Information("[HelperManager] Waiting 4 seconds for duty to fully complete..."); await Task.Delay(4000); for (int attempt = 1; attempt <= 3; attempt++) { bool inParty = false; GroupManager* groupManager = GroupManager.Instance(); if (groupManager != null) { GroupManager.Group* group = groupManager->GetGroup(); if (group != null && group->MemberCount > 1) { inParty = true; } } if (!inParty) { log.Information("[HelperManager] Successfully left party or already solo"); break; } log.Information($"[HelperManager] Attempt {attempt}/3: Still in party - sending /leave command"); LeaveParty(); if (attempt < 3) { await Task.Delay(2000); } } }); } if (configuration.IsQuester) { log.Information("[HelperManager] Disbanding party after duty (Quester)"); DisbandParty(); } } private unsafe void OnHelperRequested(string characterName, ushort worldId) { if (!configuration.IsHighLevelHelper) { log.Debug("[HelperManager] Not a High-Level Helper, ignoring request"); return; } IPlayerCharacter localPlayer = clientState.LocalPlayer; if (localPlayer == null) { log.Warning("[HelperManager] Local player is null!"); return; } string localName = localPlayer.Name.ToString(); ushort localWorldId = (ushort)localPlayer.HomeWorld.RowId; if (!(localName == characterName) || localWorldId != worldId) { return; } log.Information("[HelperManager] Helper request is for me! Checking status..."); Task.Run(async delegate { bool needsToLeaveParty = false; bool isInDuty = false; GroupManager* groupManager = GroupManager.Instance(); if (groupManager != null) { GroupManager.Group* group = groupManager->GetGroup(); if (group != null && group->MemberCount > 0) { bool requesterInParty = false; if (partyList != null) { foreach (IPartyMember member in partyList) { if (member.Name.ToString() == characterName && member.World.RowId == worldId) { requesterInParty = true; break; } } } if (requesterInParty) { log.Information($"[HelperManager] Request from {characterName}@{worldId} who is ALREADY in my party! Ignoring leave request."); needsToLeaveParty = false; } else { needsToLeaveParty = true; log.Information("[HelperManager] Currently in party (but not with requester), notifying quester..."); crossProcessIPC.NotifyHelperInParty(localName, localWorldId); if (condition[ConditionFlag.BoundByDuty]) { isInDuty = true; log.Information("[HelperManager] Currently in duty, notifying quester..."); crossProcessIPC.NotifyHelperInDuty(localName, localWorldId); } } } } if (!isInDuty) { if (needsToLeaveParty) { LeaveParty(); await Task.Delay(1000); } log.Information("[HelperManager] Ready to accept invite!"); partyInviteAutoAccept.EnableAutoAccept(); crossProcessIPC.NotifyHelperReady(localName, localWorldId); } }); } private void OnHelperDismissed() { if (configuration.IsHighLevelHelper) { log.Information("[HelperManager] Received dismiss signal, leaving party..."); DisbandParty(); } } private void OnHelperAvailable(string characterName, ushort worldId) { if (configuration.IsQuester && !availableHelpers.Any<(string, ushort)>(((string Name, ushort WorldId) h) => h.Name == characterName && h.WorldId == worldId)) { availableHelpers.Add((characterName, worldId)); log.Information($"[HelperManager] Helper discovered: {characterName}@{worldId} (Total: {availableHelpers.Count})"); } } private void OnHelperReady(string characterName, ushort worldId) { if (configuration.IsQuester) { log.Information($"[HelperManager] Helper {characterName}@{worldId} is ready!"); helperReadyStatus[(characterName, worldId)] = true; } } private void OnHelperInParty(string characterName, ushort worldId) { if (configuration.IsQuester) { log.Information($"[HelperManager] Helper {characterName}@{worldId} is in a party, waiting for them to leave..."); } } private void OnHelperInDuty(string characterName, ushort worldId) { if (configuration.IsQuester) { log.Warning($"[HelperManager] Helper {characterName}@{worldId} is in a duty! Cannot invite until they leave."); } } private void OnRequestHelperAnnouncements() { if (configuration.IsHighLevelHelper) { log.Information("[HelperManager] Received request for helper announcements, announcing..."); AnnounceIfHelper(); } } public void RequestHelperAnnouncements() { crossProcessIPC.RequestHelperAnnouncements(); } private async Task InviteLANHelper(string ipAddress, string helperName, ushort worldId) { if (lanHelperClient == null) { return; } log.Information("[HelperManager] ========================================"); log.Information("[HelperManager] === INVITING LAN HELPER ==="); log.Information("[HelperManager] Helper: " + helperName); log.Information("[HelperManager] IP: " + ipAddress); log.Information("[HelperManager] ========================================"); DisbandParty(); await Task.Delay(500); log.Information("[HelperManager] Sending helper request to " + ipAddress + "..."); if (!(await lanHelperClient.RequestHelperAsync(ipAddress, "LAN Dungeon"))) { log.Error("[HelperManager] Failed to send helper request to " + ipAddress); return; } await Task.Delay(1000); log.Information("[HelperManager] Sending party invite to " + helperName + "..."); if (!partyInviteService.InviteToParty(helperName, worldId)) { log.Error("[HelperManager] Failed to invite " + helperName + " to party"); return; } await lanHelperClient.NotifyInviteSentAsync(ipAddress, helperName); log.Information("[HelperManager] ✓ LAN helper invite complete"); } public void Dispose() { condition.ConditionChange -= OnConditionChanged; multiClientIPC.OnHelperRequested -= OnHelperRequested; multiClientIPC.OnHelperDismissed -= OnHelperDismissed; multiClientIPC.OnHelperAvailable -= OnHelperAvailable; crossProcessIPC.OnHelperRequested -= OnHelperRequested; crossProcessIPC.OnHelperDismissed -= OnHelperDismissed; crossProcessIPC.OnHelperAvailable -= OnHelperAvailable; crossProcessIPC.OnHelperReady -= OnHelperReady; crossProcessIPC.OnHelperInParty -= OnHelperInParty; crossProcessIPC.OnHelperInDuty -= OnHelperInDuty; crossProcessIPC.OnRequestHelperAnnouncements -= OnRequestHelperAnnouncements; } }