431 lines
12 KiB
C#
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;
|
|
}
|
|
}
|