qstbak/QuestionableCompanion/QuestionableCompanion.Services/HelperManager.cs
2025-12-07 10:54:53 +10:00

695 lines
21 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.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<World> worldSheet = Plugin.DataManager.GetExcelSheet<World>();
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<World> excelSheet = Plugin.DataManager.GetExcelSheet<World>();
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;
}
}