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.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Group; 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 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) { 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.partyInviteAutoAccept = partyInviteAutoAccept; 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; } log.Information("[HelperManager] Requesting helper announcements..."); RequestHelperAnnouncements(); Task.Run(async delegate { await Task.Delay(1000); if (availableHelpers.Count == 0) { log.Warning("[HelperManager] No helpers available via IPC!"); log.Warning("[HelperManager] Make sure helper clients are running with 'I'm a High-Level Helper' enabled"); } else { log.Information($"[HelperManager] Inviting {availableHelpers.Count} AUTO-DISCOVERED helper(s)..."); DisbandParty(); await Task.Delay(500); foreach (var (name, worldId) in availableHelpers) { 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() { return new List<(string, ushort)>(availableHelpers); } 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.Information("[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) { needsToLeaveParty = true; log.Information("[HelperManager] Currently in party, 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(); } 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; } }