qstbak/QuestionableCompanion/QuestionableCompanion.Services/HelperManager.cs
2025-12-04 04:39:08 +10:00

444 lines
13 KiB
C#

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;
}
}