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

431 lines
12 KiB
C#

using System;
using System.Threading.Tasks;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Group;
using Newtonsoft.Json.Linq;
namespace QuestionableCompanion.Services;
public class DungeonAutomationService : IDisposable
{
private readonly ICondition condition;
private readonly IPluginLog log;
private readonly IClientState clientState;
private readonly ICommandManager commandManager;
private readonly IFramework framework;
private readonly IGameGui gameGui;
private readonly Configuration config;
private readonly HelperManager helperManager;
private readonly MemoryHelper memoryHelper;
private readonly QuestionableIPC questionableIPC;
private readonly CrossProcessIPC crossProcessIPC;
private readonly MultiClientIPC multiClientIPC;
private bool isWaitingForParty;
private DateTime partyInviteTime = DateTime.MinValue;
private int inviteAttempts;
private bool isInvitingHelpers;
private DateTime helperInviteTime = DateTime.MinValue;
private bool isInDuty;
private bool hasStoppedAD;
private DateTime dutyEntryTime = DateTime.MinValue;
private bool pendingAutomationStop;
private DateTime lastDutyExitTime = DateTime.MinValue;
private DateTime lastDutyEntryTime = DateTime.MinValue;
private bool expectingDutyEntry;
private bool isAutomationActive;
private int originalDutyMode;
private Func<bool>? isRotationActiveChecker;
private bool hasSentAtY;
public bool IsWaitingForParty => isWaitingForParty;
public int CurrentPartySize { get; private set; } = 1;
public bool IsInAutoDutyDungeon => isAutomationActive;
public void SetRotationActiveChecker(Func<bool> checker)
{
isRotationActiveChecker = checker;
}
private bool CanExecuteAutomation()
{
if (config.IsHighLevelHelper)
{
return true;
}
if (config.IsQuester)
{
Func<bool>? func = isRotationActiveChecker;
if (func == null || !func())
{
return false;
}
return true;
}
return false;
}
public DungeonAutomationService(ICondition condition, IPluginLog log, IClientState clientState, ICommandManager commandManager, IFramework framework, IGameGui gameGui, Configuration config, HelperManager helperManager, MemoryHelper memoryHelper, QuestionableIPC questionableIPC, CrossProcessIPC crossProcessIPC, MultiClientIPC multiClientIPC)
{
this.condition = condition;
this.log = log;
this.clientState = clientState;
this.commandManager = commandManager;
this.framework = framework;
this.gameGui = gameGui;
this.config = config;
this.helperManager = helperManager;
this.memoryHelper = memoryHelper;
this.questionableIPC = questionableIPC;
this.crossProcessIPC = crossProcessIPC;
this.multiClientIPC = multiClientIPC;
condition.ConditionChange += OnConditionChanged;
log.Information("[DungeonAutomation] Service initialized with ConditionChange event");
log.Information($"[DungeonAutomation] Config - Required Party Size: {config.AutoDutyPartySize}");
log.Information($"[DungeonAutomation] Config - Party Wait Time: {config.AutoDutyMaxWaitForParty}s");
log.Information($"[DungeonAutomation] Config - Dungeon Automation Enabled: {config.EnableAutoDutyUnsynced}");
SetDutyModeBasedOnConfig();
}
public void StartDungeonAutomation()
{
if (!isAutomationActive)
{
if (!CanExecuteAutomation())
{
log.Information("[DungeonAutomation] Start request ignored - validation failed (Check Role/Rotation)");
return;
}
log.Information("[DungeonAutomation] ========================================");
log.Information("[DungeonAutomation] === STARTING DUNGEON AUTOMATION ===");
log.Information("[DungeonAutomation] ========================================");
isAutomationActive = true;
expectingDutyEntry = true;
log.Information("[DungeonAutomation] Inviting helpers via HelperManager...");
helperManager.InviteHelpers();
isInvitingHelpers = true;
helperInviteTime = DateTime.Now;
inviteAttempts = 0;
}
}
public void SetDutyModeBasedOnConfig()
{
if (config.EnableAutoDutyUnsynced)
{
questionableIPC.SetDefaultDutyMode(2);
log.Information("[DungeonAutomation] Set Duty Mode to Unsync Party (2) - Automation Enabled");
}
else
{
questionableIPC.SetDefaultDutyMode(0);
log.Information("[DungeonAutomation] Set Duty Mode to Support (0) - Automation Disabled");
}
}
public void StopDungeonAutomation()
{
if (isAutomationActive)
{
log.Information("[DungeonAutomation] ========================================");
log.Information("[DungeonAutomation] === STOPPING DUNGEON AUTOMATION ===");
log.Information("[DungeonAutomation] ========================================");
isAutomationActive = false;
Reset();
}
}
private void UpdateHelperInvite()
{
double timeSinceInvite = (DateTime.Now - helperInviteTime).TotalSeconds;
try
{
if (timeSinceInvite >= 2.0)
{
isInvitingHelpers = false;
isWaitingForParty = true;
partyInviteTime = DateTime.Now;
log.Information("[DungeonAutomation] Helper invites sent, waiting for party...");
}
}
catch (Exception ex)
{
log.Error("[DungeonAutomation] Error in helper invite: " + ex.Message);
isInvitingHelpers = false;
}
}
public void Update()
{
if (!CanExecuteAutomation() && !isAutomationActive)
{
return;
}
if (config.EnableAutoDutyUnsynced && !isAutomationActive)
{
CheckWaitForPartyTask();
}
if (!hasStoppedAD && dutyEntryTime != DateTime.MinValue && (DateTime.Now - dutyEntryTime).TotalSeconds >= 1.0)
{
try
{
commandManager.ProcessCommand("/ad stop");
log.Information("[DungeonAutomation] /ad stop (1s after duty entry)");
hasStoppedAD = true;
dutyEntryTime = DateTime.MinValue;
}
catch (Exception ex)
{
log.Error("[DungeonAutomation] Failed to stop AD: " + ex.Message);
}
}
if (isInvitingHelpers)
{
UpdateHelperInvite();
}
else if (pendingAutomationStop && (DateTime.Now - dutyEntryTime).TotalSeconds >= 5.0)
{
log.Information("[DungeonAutomation] 5s delay complete - stopping automation now");
StopDungeonAutomation();
pendingAutomationStop = false;
}
else if (isWaitingForParty)
{
UpdatePartySize();
if (CurrentPartySize >= config.AutoDutyPartySize)
{
log.Information("[DungeonAutomation] ========================================");
log.Information("[DungeonAutomation] === PARTY FULL ===");
log.Information("[DungeonAutomation] ========================================");
log.Information($"[DungeonAutomation] Party Size: {CurrentPartySize}/{config.AutoDutyPartySize}");
isWaitingForParty = false;
partyInviteTime = DateTime.MinValue;
inviteAttempts = 0;
log.Information("[DungeonAutomation] Party full - ready for dungeon!");
}
else if ((DateTime.Now - partyInviteTime).TotalSeconds >= (double)config.AutoDutyMaxWaitForParty)
{
log.Warning($"[DungeonAutomation] Party not full after {config.AutoDutyMaxWaitForParty}s - retrying invite (Attempt #{inviteAttempts + 1})");
log.Information($"[DungeonAutomation] Current Party Size: {CurrentPartySize}/{config.AutoDutyPartySize}");
log.Information("[DungeonAutomation] Retrying helper invites...");
helperManager.InviteHelpers();
partyInviteTime = DateTime.Now;
}
}
}
private void CheckWaitForPartyTask()
{
if (questionableIPC.GetCurrentTask() is JObject jObject)
{
JToken taskNameToken = jObject["TaskName"];
if (taskNameToken != null && taskNameToken.ToString() == "WaitForParty")
{
StartDungeonAutomation();
}
}
}
private unsafe void UpdatePartySize()
{
try
{
int partySize = 0;
GroupManager* groupManager = GroupManager.Instance();
if (groupManager != null)
{
GroupManager.Group* group = groupManager->GetGroup();
if (group != null)
{
partySize = group->MemberCount;
}
}
if (partySize == 0)
{
partySize = 1;
}
if (partySize != CurrentPartySize)
{
CurrentPartySize = partySize;
log.Information($"[DungeonAutomation] Party Size updated: {CurrentPartySize}/{config.AutoDutyPartySize}");
}
}
catch (Exception ex)
{
log.Error("[DungeonAutomation] Error updating party size: " + ex.Message);
}
}
private void OnConditionChanged(ConditionFlag flag, bool value)
{
if (flag == ConditionFlag.BoundByDuty)
{
if (value && !isInDuty)
{
isInDuty = true;
OnDutyEntered();
}
else if (!value && isInDuty)
{
isInDuty = false;
OnDutyExited();
}
}
}
public void OnDutyEntered()
{
if ((DateTime.Now - lastDutyEntryTime).TotalSeconds < 5.0)
{
log.Debug("[DungeonAutomation] OnDutyEntered called too soon - ignoring spam");
return;
}
lastDutyEntryTime = DateTime.Now;
log.Debug("[DungeonAutomation] Entered duty");
if (!CanExecuteAutomation())
{
log.Debug("[DungeonAutomation] OnDutyEntered ignored - validation failed");
}
else if (expectingDutyEntry)
{
log.Information("[DungeonAutomation] Duty started by DungeonAutomation - enabling automation commands");
expectingDutyEntry = false;
hasStoppedAD = false;
dutyEntryTime = DateTime.Now;
if (!hasSentAtY)
{
commandManager.ProcessCommand("/at y");
log.Information("[DungeonAutomation] Sent /at y (duty entered)");
hasSentAtY = true;
}
}
else
{
log.Information("[DungeonAutomation] Duty NOT started by DungeonAutomation (Solo Duty/Quest Battle) - skipping automation commands");
}
}
public void OnDutyExited()
{
if ((DateTime.Now - lastDutyExitTime).TotalSeconds < 2.0)
{
log.Debug("[DungeonAutomation] OnDutyExited called too soon - ignoring spam");
return;
}
lastDutyExitTime = DateTime.Now;
log.Information("[DungeonAutomation] Exited duty");
if (!CanExecuteAutomation() && !isAutomationActive)
{
log.Information("[DungeonAutomation] OnDutyExited ignored - validation failed");
}
else if (isAutomationActive)
{
commandManager.ProcessCommand("/at n");
log.Information("[DungeonAutomation] Sent /at n (duty exited)");
hasSentAtY = false;
log.Information("[DungeonAutomation] Waiting 8s, then disband + restart quest");
Task.Run(async delegate
{
await EnsureSoloPartyAsync();
});
StopDungeonAutomation();
}
else
{
log.Information("[DungeonAutomation] Exited non-automated duty - no cleanup needed");
}
}
private async Task EnsureSoloPartyAsync()
{
TimeSpan timeout = TimeSpan.FromSeconds(60L);
DateTime start = DateTime.Now;
while (CurrentPartySize > 1 && DateTime.Now - start < timeout)
{
await framework.RunOnFrameworkThread(delegate
{
commandManager.ProcessCommand("/leave");
});
log.Information("[DungeonAutomation] Forced /leave sent, rechecking party size...");
await Task.Delay(1500);
UpdatePartySize();
}
if (CurrentPartySize > 1)
{
log.Warning("[DungeonAutomation] Still not solo after leave spam!");
}
else
{
log.Information("[DungeonAutomation] Party reduced to solo after duty exit.");
}
}
public void DisbandParty()
{
try
{
if (!CanExecuteAutomation())
{
log.Information("[DungeonAutomation] DisbandParty ignored - validation failed");
return;
}
log.Information("[DungeonAutomation] Disbanding party");
framework.RunOnFrameworkThread(delegate
{
memoryHelper.SendChatMessage("/leave");
log.Information("[DungeonAutomation] /leave command sent via UIModule");
});
}
catch (Exception ex)
{
log.Error("[DungeonAutomation] Failed to disband party: " + ex.Message);
}
}
public void Reset()
{
isWaitingForParty = false;
partyInviteTime = DateTime.MinValue;
inviteAttempts = 0;
CurrentPartySize = 1;
isInvitingHelpers = false;
helperInviteTime = DateTime.MinValue;
isAutomationActive = false;
log.Information("[DungeonAutomation] State reset");
}
public void Dispose()
{
Reset();
condition.ConditionChange -= OnConditionChanged;
}
}