diff --git a/LLib/LLib.ImGui/LWindow.cs b/LLib/LLib.ImGui/LWindow.cs index 16df614..a355121 100644 --- a/LLib/LLib.ImGui/LWindow.cs +++ b/LLib/LLib.ImGui/LWindow.cs @@ -33,7 +33,7 @@ public abstract class LWindow : Window } } - protected bool IsPinned + protected new bool IsPinned { get { @@ -45,7 +45,7 @@ public abstract class LWindow : Window } } - protected bool IsClickthrough + protected new bool IsClickthrough { get { diff --git a/NotificationMasterAPI/NotificationMasterAPI.csproj b/NotificationMasterAPI/NotificationMasterAPI.csproj new file mode 100644 index 0000000..d516cfc --- /dev/null +++ b/NotificationMasterAPI/NotificationMasterAPI.csproj @@ -0,0 +1,19 @@ + + + + NotificationMasterAPI + False + netcoreapp9.0 + + + 12.0 + True + + + + + + ..\..\..\..\..\ffxiv\alyssile-xivl\addon\Hooks\dev\Dalamud.dll + + + \ No newline at end of file diff --git a/NotificationMasterAPI/NotificationMasterAPI/Data.cs b/NotificationMasterAPI/NotificationMasterAPI/Data.cs new file mode 100644 index 0000000..3623dde --- /dev/null +++ b/NotificationMasterAPI/NotificationMasterAPI/Data.cs @@ -0,0 +1,6 @@ +namespace NotificationMasterAPI; + +public static class Data +{ + public const string MFAudioFormats = "*.3g2;*.3gp;*.3gp2;*.3gpp;*.asf;*.wma;*.wmv;*.aac;*.adts;*.avi;*.mp3;*.m4a;*.m4v;*.mov;*.mp4;*.sami;*.smi;*.wav;*.aiff"; +} diff --git a/NotificationMasterAPI/NotificationMasterAPI/NMAPINames.cs b/NotificationMasterAPI/NotificationMasterAPI/NMAPINames.cs new file mode 100644 index 0000000..4ff0134 --- /dev/null +++ b/NotificationMasterAPI/NotificationMasterAPI/NMAPINames.cs @@ -0,0 +1,16 @@ +namespace NotificationMasterAPI; + +public static class NMAPINames +{ + public const string DisplayToastNotification = "NotificationMasterAPI.DisplayToastNotification"; + + public const string FlashTaskbarIcon = "NotificationMasterAPI.FlashTaskbarIcon"; + + public const string PlaySound = "NotificationMasterAPI.PlaySound"; + + public const string BringGameForeground = "NotificationMasterAPI.BringGameForeground"; + + public const string StopSound = "NotificationMasterAPI.StopSound"; + + public const string Active = "NotificationMasterAPI.Active"; +} diff --git a/NotificationMasterAPI/NotificationMasterAPI/NotificationMasterApi.cs b/NotificationMasterAPI/NotificationMasterAPI/NotificationMasterApi.cs new file mode 100644 index 0000000..6b9235c --- /dev/null +++ b/NotificationMasterAPI/NotificationMasterAPI/NotificationMasterApi.cs @@ -0,0 +1,146 @@ +using System; +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc.Exceptions; + +namespace NotificationMasterAPI; + +public class NotificationMasterApi +{ + private IDalamudPluginInterface PluginInterface; + + /// + /// Creates an instance of NotificationMaster API. You do not need to check if NotificationMaster plugin is installed. + /// + /// Plugin interface reference + public NotificationMasterApi(IDalamudPluginInterface dalamudPluginInterface) + { + PluginInterface = dalamudPluginInterface; + } + + private void Validate() + { + if (PluginInterface == null) + { + throw new NullReferenceException("NotificationMaster API was called before it was initialized"); + } + } + + /// + /// Checks if IPC is ready. You DO NOT need to call this method before invoking any of API functions unless you specifically want to check if plugin is installed and ready to accept requests. + /// + /// + public bool IsIPCReady() + { + Validate(); + try + { + PluginInterface.GetIpcSubscriber("NotificationMasterAPI.Active").InvokeAction(); + return true; + } + catch (IpcNotReadyError) + { + } + return false; + } + + /// + /// Displays tray notification. This function does not throws an exception or displays an error if NotificationMaster is not installed. + /// + /// Text of tray notification + /// Whether operation succeed. + public bool DisplayTrayNotification(string text) + { + return DisplayTrayNotification(null, text); + } + + /// + /// Displays tray notification. This function does not throws an exception or displays an error if NotificationMaster is not installed. + /// + /// Title of tray notification + /// Text of tray notification + /// Whether operation succeed. + public bool DisplayTrayNotification(string? title, string text) + { + Validate(); + try + { + return PluginInterface.GetIpcSubscriber("NotificationMasterAPI.DisplayToastNotification").InvokeFunc(PluginInterface.InternalName, title, text); + } + catch (IpcNotReadyError) + { + } + return false; + } + + /// + /// Flashes game's taskbar icon. This function does not throws an exception or displays an error if NotificationMaster is not installed. + /// + /// Whether operation succeeded + public bool FlashTaskbarIcon() + { + Validate(); + try + { + return PluginInterface.GetIpcSubscriber("NotificationMasterAPI.FlashTaskbarIcon").InvokeFunc(PluginInterface.InternalName); + } + catch (IpcNotReadyError) + { + } + return false; + } + + /// + /// Attempts to bring game's window foreground. Due to Windows inconsistencies, it's not guaranteed to work. This function does not throws an exception or displays an error if NotificationMaster is not installed. + /// + /// Whether operation succeeded + public bool TryBringGameForeground() + { + Validate(); + try + { + return PluginInterface.GetIpcSubscriber("NotificationMasterAPI.BringGameForeground").InvokeFunc(PluginInterface.InternalName); + } + catch (IpcNotReadyError) + { + } + return false; + } + + /// + /// Begins to play a sound file. If another sound file is already playing, stops previous file and begins playing specified. This function does not throws an exception or displays an error if NotificationMaster is not installed. + /// + /// Path to local file. Can not be web URL. See for supported formats. + /// Volume between 0.0 and 1.0 + /// Whether to repeat sound file. + /// Whether to stop file once game is focused. + /// Whether operation succeeded + public bool PlaySound(string pathOnDisk, float volume = 1f, bool repeat = false, bool stopOnGameFocus = true) + { + Validate(); + try + { + return PluginInterface.GetIpcSubscriber("NotificationMasterAPI.PlaySound").InvokeFunc(PluginInterface.InternalName, pathOnDisk, volume, repeat, stopOnGameFocus); + } + catch (IpcNotReadyError) + { + } + return false; + } + + /// + /// Stops playing sound. This function does not throws an exception or displays an error if NotificationMaster is not installed. + /// + /// Whether operation succeeded + public bool StopSound() + { + Validate(); + try + { + return PluginInterface.GetIpcSubscriber("NotificationMasterAPI.StopSound").InvokeFunc(PluginInterface.InternalName); + } + catch (IpcNotReadyError) + { + } + return false; + } +} diff --git a/QuestPaths/Questionable.QuestPaths/AssemblyQuestLoader.cs b/QuestPaths/Questionable.QuestPaths/AssemblyQuestLoader.cs index e8d89ee..edc88ec 100644 --- a/QuestPaths/Questionable.QuestPaths/AssemblyQuestLoader.cs +++ b/QuestPaths/Questionable.QuestPaths/AssemblyQuestLoader.cs @@ -16330,7 +16330,7 @@ public static class AssemblyQuestLoader index = 0; span[index] = "liza"; questRoot29.Author = list253; - index = 3; + index = 4; List list254 = new List(index); CollectionsMarshal.SetCount(list254, index); span2 = CollectionsMarshal.AsSpan(list254); @@ -16382,6 +16382,11 @@ public static class AssemblyQuestLoader obj165.Steps = list256; reference191 = obj165; num++; + span2[num] = new QuestSequence + { + Sequence = 2 + }; + num++; ref QuestSequence reference192 = ref span2[num]; QuestSequence obj166 = new QuestSequence { @@ -87745,7 +87750,7 @@ public static class AssemblyQuestLoader index = 0; span[index] = "plogon_enjoyer"; questRoot21.Author = list193; - index = 3; + index = 4; List list194 = new List(index); CollectionsMarshal.SetCount(list194, index); span2 = CollectionsMarshal.AsSpan(list194); @@ -87781,6 +87786,11 @@ public static class AssemblyQuestLoader obj143.Steps = list196; reference152 = obj143; num++; + span2[num] = new QuestSequence + { + Sequence = 2 + }; + num++; ref QuestSequence reference153 = ref span2[num]; QuestSequence obj144 = new QuestSequence { @@ -106084,7 +106094,7 @@ public static class AssemblyQuestLoader index = 0; span[index] = "JerryWester"; questRoot8.Author = list78; - index = 6; + index = 7; List list79 = new List(index); CollectionsMarshal.SetCount(list79, index); span2 = CollectionsMarshal.AsSpan(list79); @@ -106192,6 +106202,11 @@ public static class AssemblyQuestLoader obj57.Steps = list84; reference67 = obj57; num++; + span2[num] = new QuestSequence + { + Sequence = 5 + }; + num++; ref QuestSequence reference68 = ref span2[num]; QuestSequence obj58 = new QuestSequence { @@ -121129,7 +121144,7 @@ public static class AssemblyQuestLoader index = 0; span[index] = "JerryWester"; questRoot18.Author = list136; - index = 5; + index = 6; List list137 = new List(index); CollectionsMarshal.SetCount(list137, index); span2 = CollectionsMarshal.AsSpan(list137); @@ -121215,6 +121230,11 @@ public static class AssemblyQuestLoader obj96.Steps = list141; reference105 = obj96; num++; + span2[num] = new QuestSequence + { + Sequence = 4 + }; + num++; ref QuestSequence reference106 = ref span2[num]; QuestSequence obj97 = new QuestSequence { @@ -123973,7 +123993,7 @@ public static class AssemblyQuestLoader index = 0; span[index] = "JerryWester"; questRoot44.Author = list325; - index = 4; + index = 5; List list326 = new List(index); CollectionsMarshal.SetCount(list326, index); span2 = CollectionsMarshal.AsSpan(list326); @@ -124040,6 +124060,11 @@ public static class AssemblyQuestLoader obj211.Steps = list329; reference239 = obj211; num++; + span2[num] = new QuestSequence + { + Sequence = 3 + }; + num++; ref QuestSequence reference241 = ref span2[num]; QuestSequence obj212 = new QuestSequence { @@ -125565,7 +125590,7 @@ public static class AssemblyQuestLoader index = 0; span[index] = "JerryWester"; questRoot11.Author = list66; - index = 6; + index = 7; List list67 = new List(index); CollectionsMarshal.SetCount(list67, index); span2 = CollectionsMarshal.AsSpan(list67); @@ -125684,6 +125709,11 @@ public static class AssemblyQuestLoader obj51.Steps = list73; reference51 = obj51; num++; + span2[num] = new QuestSequence + { + Sequence = 5 + }; + num++; ref QuestSequence reference52 = ref span2[num]; QuestSequence obj52 = new QuestSequence { @@ -165091,7 +165121,7 @@ public static class AssemblyQuestLoader index = 0; span[index] = "JerryWester"; questRoot15.Author = list127; - index = 5; + index = 6; List list128 = new List(index); CollectionsMarshal.SetCount(list128, index); span2 = CollectionsMarshal.AsSpan(list128); @@ -165174,6 +165204,11 @@ public static class AssemblyQuestLoader obj91.Steps = list132; reference100 = obj91; num++; + span2[num] = new QuestSequence + { + Sequence = 4 + }; + num++; ref QuestSequence reference101 = ref span2[num]; QuestSequence obj92 = new QuestSequence { @@ -166531,7 +166566,7 @@ public static class AssemblyQuestLoader index = 0; span[index] = "JerryWester"; questRoot27.Author = list219; - index = 7; + index = 8; List list220 = new List(index); CollectionsMarshal.SetCount(list220, index); span2 = CollectionsMarshal.AsSpan(list220); @@ -166662,6 +166697,11 @@ public static class AssemblyQuestLoader obj156.Steps = list226; reference168 = obj156; num++; + span2[num] = new QuestSequence + { + Sequence = 6 + }; + num++; ref QuestSequence reference170 = ref span2[num]; QuestSequence obj158 = new QuestSequence { @@ -179658,7 +179698,7 @@ public static class AssemblyQuestLoader index = 0; span[index] = "JerryWester"; questRoot5.Author = list25; - index = 8; + index = 9; List list26 = new List(index); CollectionsMarshal.SetCount(list26, index); span2 = CollectionsMarshal.AsSpan(list26); @@ -179793,6 +179833,11 @@ public static class AssemblyQuestLoader obj23.Steps = list33; reference23 = obj23; num++; + span2[num] = new QuestSequence + { + Sequence = 7 + }; + num++; ref QuestSequence reference24 = ref span2[num]; QuestSequence obj24 = new QuestSequence { @@ -187760,7 +187805,7 @@ public static class AssemblyQuestLoader index = 0; span[index] = "JerryWester"; questRoot20.Author = list123; - index = 6; + index = 7; List list124 = new List(index); CollectionsMarshal.SetCount(list124, index); span2 = CollectionsMarshal.AsSpan(list124); @@ -187876,6 +187921,11 @@ public static class AssemblyQuestLoader obj84.Steps = list130; reference90 = obj84; num++; + span2[num] = new QuestSequence + { + Sequence = 5 + }; + num++; ref QuestSequence reference91 = ref span2[num]; QuestSequence obj85 = new QuestSequence { @@ -188384,7 +188434,7 @@ public static class AssemblyQuestLoader index = 0; span[index] = "JerryWester"; questRoot25.Author = list160; - index = 6; + index = 7; List list161 = new List(index); CollectionsMarshal.SetCount(list161, index); span2 = CollectionsMarshal.AsSpan(list161); @@ -188495,6 +188545,11 @@ public static class AssemblyQuestLoader obj108.Steps = list166; reference116 = obj108; num++; + span2[num] = new QuestSequence + { + Sequence = 5 + }; + num++; ref QuestSequence reference117 = ref span2[num]; QuestSequence obj109 = new QuestSequence { @@ -195213,7 +195268,7 @@ public static class AssemblyQuestLoader index = 0; span[index] = "JerryWester"; questRoot25.Author = list216; - index = 6; + index = 7; List list217 = new List(index); CollectionsMarshal.SetCount(list217, index); span2 = CollectionsMarshal.AsSpan(list217); @@ -195338,6 +195393,11 @@ public static class AssemblyQuestLoader obj136.Steps = list222; reference166 = obj136; num++; + span2[num] = new QuestSequence + { + Sequence = 5 + }; + num++; ref QuestSequence reference167 = ref span2[num]; QuestSequence obj137 = new QuestSequence { @@ -197491,7 +197551,7 @@ public static class AssemblyQuestLoader index = 0; span[index] = "JerryWester"; questRoot45.Author = list369; - index = 8; + index = 9; List list370 = new List(index); CollectionsMarshal.SetCount(list370, index); span2 = CollectionsMarshal.AsSpan(list370); @@ -197656,6 +197716,11 @@ public static class AssemblyQuestLoader obj238.Steps = list380; reference280 = obj238; num++; + span2[num] = new QuestSequence + { + Sequence = 7 + }; + num++; ref QuestSequence reference282 = ref span2[num]; QuestSequence obj240 = new QuestSequence { @@ -198205,7 +198270,7 @@ public static class AssemblyQuestLoader int index = 0; span[index] = "JerryWester"; questRoot.Author = list; - index = 6; + index = 7; List list2 = new List(index); CollectionsMarshal.SetCount(list2, index); Span span2 = CollectionsMarshal.AsSpan(list2); @@ -198329,6 +198394,11 @@ public static class AssemblyQuestLoader obj5.Steps = list7; reference5 = obj5; num++; + span2[num] = new QuestSequence + { + Sequence = 5 + }; + num++; ref QuestSequence reference7 = ref span2[num]; QuestSequence obj7 = new QuestSequence { @@ -200079,7 +200149,7 @@ public static class AssemblyQuestLoader index = 0; span[index] = "liza"; questRoot11.Author = list111; - index = 5; + index = 6; List list112 = new List(index); CollectionsMarshal.SetCount(list112, index); span2 = CollectionsMarshal.AsSpan(list112); @@ -200170,6 +200240,11 @@ public static class AssemblyQuestLoader obj81.Steps = list116; reference94 = obj81; num++; + span2[num] = new QuestSequence + { + Sequence = 4 + }; + num++; ref QuestSequence reference95 = ref span2[num]; QuestSequence obj82 = new QuestSequence { @@ -201909,7 +201984,7 @@ public static class AssemblyQuestLoader index = 0; span[index] = "liza"; questRoot21.Author = list221; - index = 3; + index = 4; List list222 = new List(index); CollectionsMarshal.SetCount(list222, index); span2 = CollectionsMarshal.AsSpan(list222); @@ -201966,6 +202041,11 @@ public static class AssemblyQuestLoader obj160.Steps = list225; reference181 = obj160; num++; + span2[num] = new QuestSequence + { + Sequence = 2 + }; + num++; ref QuestSequence reference182 = ref span2[num]; QuestSequence obj161 = new QuestSequence { @@ -215089,7 +215169,7 @@ public static class AssemblyQuestLoader int index = 0; span[index] = "liza"; questRoot.Author = list; - index = 3; + index = 4; List list2 = new List(index); CollectionsMarshal.SetCount(list2, index); Span span2 = CollectionsMarshal.AsSpan(list2); @@ -215140,6 +215220,11 @@ public static class AssemblyQuestLoader obj2.Steps = list4; reference2 = obj2; num++; + span2[num] = new QuestSequence + { + Sequence = 2 + }; + num++; ref QuestSequence reference3 = ref span2[num]; QuestSequence obj3 = new QuestSequence { @@ -222552,7 +222637,7 @@ public static class AssemblyQuestLoader index = 0; span[index] = "JerryWester"; questRoot9.Author = list66; - index = 6; + index = 7; List list67 = new List(index); CollectionsMarshal.SetCount(list67, index); span2 = CollectionsMarshal.AsSpan(list67); @@ -222659,6 +222744,11 @@ public static class AssemblyQuestLoader obj52.Steps = list72; reference54 = obj52; num++; + span2[num] = new QuestSequence + { + Sequence = 5 + }; + num++; ref QuestSequence reference55 = ref span2[num]; QuestSequence obj53 = new QuestSequence { @@ -249500,7 +249590,7 @@ public static class AssemblyQuestLoader index = 0; span[index] = "JerryWester"; questRoot36.Author = list374; - index = 4; + index = 5; List list375 = new List(index); CollectionsMarshal.SetCount(list375, index); span2 = CollectionsMarshal.AsSpan(list375); @@ -249558,6 +249648,11 @@ public static class AssemblyQuestLoader obj187.Steps = list378; reference263 = obj187; num++; + span2[num] = new QuestSequence + { + Sequence = 3 + }; + num++; ref QuestSequence reference264 = ref span2[num]; QuestSequence obj188 = new QuestSequence { @@ -249835,7 +249930,7 @@ public static class AssemblyQuestLoader index = 0; span[index] = "JerryWester"; questRoot39.Author = list396; - index = 6; + index = 7; List list397 = new List(index); CollectionsMarshal.SetCount(list397, index); span2 = CollectionsMarshal.AsSpan(list397); @@ -249939,6 +250034,11 @@ public static class AssemblyQuestLoader obj204.Steps = list402; reference281 = obj204; num++; + span2[num] = new QuestSequence + { + Sequence = 5 + }; + num++; ref QuestSequence reference283 = ref span2[num]; QuestSequence obj206 = new QuestSequence { @@ -267329,7 +267429,7 @@ public static class AssemblyQuestLoader int index = 0; span[index] = "liza"; questRoot.Author = list; - index = 6; + index = 7; List list2 = new List(index); CollectionsMarshal.SetCount(list2, index); Span span2 = CollectionsMarshal.AsSpan(list2); @@ -267442,6 +267542,11 @@ public static class AssemblyQuestLoader obj5.Steps = list7; reference5 = obj5; num++; + span2[num] = new QuestSequence + { + Sequence = 5 + }; + num++; ref QuestSequence reference6 = ref span2[num]; QuestSequence obj6 = new QuestSequence { @@ -269112,7 +269217,7 @@ public static class AssemblyQuestLoader index = 0; span[index] = "liza"; questRoot13.Author = list104; - index = 4; + index = 5; List list105 = new List(index); CollectionsMarshal.SetCount(list105, index); span2 = CollectionsMarshal.AsSpan(list105); @@ -269171,6 +269276,11 @@ public static class AssemblyQuestLoader obj75.Steps = list108; reference80 = obj75; num++; + span2[num] = new QuestSequence + { + Sequence = 3 + }; + num++; ref QuestSequence reference81 = ref span2[num]; QuestSequence obj76 = new QuestSequence { @@ -273186,7 +273296,7 @@ public static class AssemblyQuestLoader index = 0; span[index] = "liza"; questRoot41.Author = list355; - index = 4; + index = 5; List list356 = new List(index); CollectionsMarshal.SetCount(list356, index); span2 = CollectionsMarshal.AsSpan(list356); @@ -273253,6 +273363,11 @@ public static class AssemblyQuestLoader obj255.Steps = list359; reference275 = obj255; num++; + span2[num] = new QuestSequence + { + Sequence = 3 + }; + num++; ref QuestSequence reference276 = ref span2[num]; QuestSequence obj256 = new QuestSequence { @@ -284706,7 +284821,7 @@ public static class AssemblyQuestLoader index = 0; span[index] = "liza"; questRoot26.Author = list215; - index = 3; + index = 4; List list216 = new List(index); CollectionsMarshal.SetCount(list216, index); span2 = CollectionsMarshal.AsSpan(list216); @@ -284772,6 +284887,11 @@ public static class AssemblyQuestLoader obj143.Steps = list218; reference156 = obj143; num++; + span2[num] = new QuestSequence + { + Sequence = 2 + }; + num++; ref QuestSequence reference158 = ref span2[num]; QuestSequence obj146 = new QuestSequence { @@ -357538,7 +357658,7 @@ public static class AssemblyQuestLoader span[index] = "liza"; questRoot66.Author = list226; QuestRoot questRoot67 = questRoot; - index = 4; + index = 5; list2 = new List(index); CollectionsMarshal.SetCount(list2, index); span2 = CollectionsMarshal.AsSpan(list2); @@ -357598,6 +357718,11 @@ public static class AssemblyQuestLoader questSequence81.Steps = list228; reference277 = questSequence; num++; + span2[num] = new QuestSequence + { + Sequence = 3 + }; + num++; ref QuestSequence reference278 = ref span2[num]; QuestSequence obj159 = new QuestSequence { @@ -358335,7 +358460,7 @@ public static class AssemblyQuestLoader span[index] = "liza"; questRoot80.Author = list265; QuestRoot questRoot81 = questRoot; - index = 3; + index = 4; list2 = new List(index); CollectionsMarshal.SetCount(list2, index); span2 = CollectionsMarshal.AsSpan(list2); @@ -358376,6 +358501,11 @@ public static class AssemblyQuestLoader questSequence91.Steps = list267; reference317 = questSequence; num++; + span2[num] = new QuestSequence + { + Sequence = 2 + }; + num++; ref QuestSequence reference318 = ref span2[num]; QuestSequence obj186 = new QuestSequence { @@ -359087,10 +359217,11 @@ public static class AssemblyQuestLoader reference355 = obj208; num++; ref QuestSequence reference356 = ref span2[num]; - QuestSequence obj209 = new QuestSequence + questSequence = new QuestSequence { Sequence = 5 }; + QuestSequence questSequence102 = questSequence; num2 = 1; List list299 = new List(num2); CollectionsMarshal.SetCount(list299, num2); @@ -359100,14 +359231,15 @@ public static class AssemblyQuestLoader { StopDistance = 0.25f }; - obj209.Steps = list299; - reference356 = obj209; + questSequence102.Steps = list299; + reference356 = questSequence; num++; ref QuestSequence reference357 = ref span2[num]; - QuestSequence obj210 = new QuestSequence + questSequence = new QuestSequence { Sequence = 6 }; + QuestSequence questSequence103 = questSequence; index2 = 1; List list300 = new List(index2); CollectionsMarshal.SetCount(list300, index2); @@ -359117,11 +359249,11 @@ public static class AssemblyQuestLoader { StopDistance = 0.25f }; - obj210.Steps = list300; - reference357 = obj210; + questSequence103.Steps = list300; + reference357 = questSequence; num++; ref QuestSequence reference358 = ref span2[num]; - QuestSequence obj211 = new QuestSequence + QuestSequence obj209 = new QuestSequence { Sequence = 7 }; @@ -359131,15 +359263,15 @@ public static class AssemblyQuestLoader span3 = CollectionsMarshal.AsSpan(list301); index2 = 0; span3[index2] = new QuestStep(EInteractionType.Interact, 1037985u, new Vector3(481.8036f, 66.16195f, -108.537415f), 956); - obj211.Steps = list301; - reference358 = obj211; + obj209.Steps = list301; + reference358 = obj209; num++; ref QuestSequence reference359 = ref span2[num]; questSequence = new QuestSequence { Sequence = byte.MaxValue }; - QuestSequence questSequence102 = questSequence; + QuestSequence questSequence104 = questSequence; index2 = 1; list4 = new List(index2); CollectionsMarshal.SetCount(list4, index2); @@ -359160,7 +359292,7 @@ public static class AssemblyQuestLoader }; questStep52.DialogueChoices = list302; reference360 = questStep52; - questSequence102.Steps = list4; + questSequence104.Steps = list4; reference359 = questSequence; questRoot91.QuestSequence = list2; AddQuest(questId45, questRoot); @@ -359181,7 +359313,7 @@ public static class AssemblyQuestLoader span2 = CollectionsMarshal.AsSpan(list2); num = 0; ref QuestSequence reference361 = ref span2[num]; - QuestSequence obj212 = new QuestSequence + QuestSequence obj210 = new QuestSequence { Sequence = 0 }; @@ -359191,15 +359323,15 @@ public static class AssemblyQuestLoader span3 = CollectionsMarshal.AsSpan(list304); index2 = 0; span3[index2] = new QuestStep(EInteractionType.AcceptQuest, 1040401u, new Vector3(374.83777f, 79.691376f, 298.08484f), 956); - obj212.Steps = list304; - reference361 = obj212; + obj210.Steps = list304; + reference361 = obj210; num++; ref QuestSequence reference362 = ref span2[num]; questSequence = new QuestSequence { Sequence = 1 }; - QuestSequence questSequence103 = questSequence; + QuestSequence questSequence105 = questSequence; index2 = 3; list4 = new List(index2); CollectionsMarshal.SetCount(list4, index2); @@ -359218,11 +359350,11 @@ public static class AssemblyQuestLoader }; num2++; span3[num2] = new QuestStep(EInteractionType.Interact, 1039687u, new Vector3(-18.875488f, -31.53043f, -76.98181f), 956); - questSequence103.Steps = list4; + questSequence105.Steps = list4; reference362 = questSequence; num++; ref QuestSequence reference363 = ref span2[num]; - QuestSequence obj213 = new QuestSequence + QuestSequence obj211 = new QuestSequence { Sequence = 2 }; @@ -359232,15 +359364,15 @@ public static class AssemblyQuestLoader span3 = CollectionsMarshal.AsSpan(list305); index2 = 0; span3[index2] = new QuestStep(EInteractionType.Interact, 1040409u, new Vector3(-99.076904f, -28.516306f, -60.013794f), 956); - obj213.Steps = list305; - reference363 = obj213; + obj211.Steps = list305; + reference363 = obj211; num++; ref QuestSequence reference364 = ref span2[num]; questSequence = new QuestSequence { Sequence = 3 }; - QuestSequence questSequence104 = questSequence; + QuestSequence questSequence106 = questSequence; index2 = 8; list4 = new List(index2); CollectionsMarshal.SetCount(list4, index2); @@ -359448,11 +359580,11 @@ public static class AssemblyQuestLoader span7[index3] = null; questStep60.CompletionQuestVariablesFlags = list75; reference372 = questStep; - questSequence104.Steps = list4; + questSequence106.Steps = list4; reference364 = questSequence; num++; ref QuestSequence reference373 = ref span2[num]; - QuestSequence obj214 = new QuestSequence + QuestSequence obj212 = new QuestSequence { Sequence = 4 }; @@ -359462,14 +359594,15 @@ public static class AssemblyQuestLoader span3 = CollectionsMarshal.AsSpan(list306); index2 = 0; span3[index2] = new QuestStep(EInteractionType.Interact, 1040436u, new Vector3(-78.26355f, -29.53f, -58.854065f), 956); - obj214.Steps = list306; - reference373 = obj214; + obj212.Steps = list306; + reference373 = obj212; num++; ref QuestSequence reference374 = ref span2[num]; - QuestSequence obj215 = new QuestSequence + questSequence = new QuestSequence { Sequence = 5 }; + QuestSequence questSequence107 = questSequence; index2 = 1; List list307 = new List(index2); CollectionsMarshal.SetCount(list307, index2); @@ -359479,14 +359612,15 @@ public static class AssemblyQuestLoader { StopDistance = 5f }; - obj215.Steps = list307; - reference374 = obj215; + questSequence107.Steps = list307; + reference374 = questSequence; num++; ref QuestSequence reference375 = ref span2[num]; - QuestSequence obj216 = new QuestSequence + questSequence = new QuestSequence { Sequence = byte.MaxValue }; + QuestSequence questSequence108 = questSequence; num2 = 1; List list308 = new List(num2); CollectionsMarshal.SetCount(list308, num2); @@ -359496,8 +359630,8 @@ public static class AssemblyQuestLoader { StopDistance = 5f }; - obj216.Steps = list308; - reference375 = obj216; + questSequence108.Steps = list308; + reference375 = questSequence; questRoot93.QuestSequence = list2; AddQuest(questId46, questRoot); QuestId questId47 = new QuestId(4446); @@ -359517,10 +359651,11 @@ public static class AssemblyQuestLoader span2 = CollectionsMarshal.AsSpan(list2); num = 0; ref QuestSequence reference376 = ref span2[num]; - QuestSequence obj217 = new QuestSequence + questSequence = new QuestSequence { Sequence = 0 }; + QuestSequence questSequence109 = questSequence; index2 = 1; List list310 = new List(index2); CollectionsMarshal.SetCount(list310, index2); @@ -359530,15 +359665,15 @@ public static class AssemblyQuestLoader { StopDistance = 5f }; - obj217.Steps = list310; - reference376 = obj217; + questSequence109.Steps = list310; + reference376 = questSequence; num++; ref QuestSequence reference377 = ref span2[num]; questSequence = new QuestSequence { Sequence = 1 }; - QuestSequence questSequence105 = questSequence; + QuestSequence questSequence110 = questSequence; num2 = 4; list4 = new List(num2); CollectionsMarshal.SetCount(list4, num2); @@ -359588,11 +359723,11 @@ public static class AssemblyQuestLoader reference379 = questStep; index2++; span3[index2] = new QuestStep(EInteractionType.Interact, 1040444u, new Vector3(-424.24664f, -31.831255f, -38.895264f), 956); - questSequence105.Steps = list4; + questSequence110.Steps = list4; reference377 = questSequence; num++; ref QuestSequence reference380 = ref span2[num]; - QuestSequence obj218 = new QuestSequence + QuestSequence obj213 = new QuestSequence { Sequence = 2 }; @@ -359602,11 +359737,11 @@ public static class AssemblyQuestLoader span3 = CollectionsMarshal.AsSpan(list313); num2 = 0; span3[num2] = new QuestStep(EInteractionType.Interact, 2012223u, new Vector3(-427.2984f, -31.418396f, -38.19336f), 956); - obj218.Steps = list313; - reference380 = obj218; + obj213.Steps = list313; + reference380 = obj213; num++; ref QuestSequence reference381 = ref span2[num]; - QuestSequence obj219 = new QuestSequence + QuestSequence obj214 = new QuestSequence { Sequence = 3 }; @@ -359616,15 +359751,15 @@ public static class AssemblyQuestLoader span3 = CollectionsMarshal.AsSpan(list314); index2 = 0; span3[index2] = new QuestStep(EInteractionType.Interact, 1040446u, new Vector3(-697.4136f, -31.621387f, 164.11072f), 956); - obj219.Steps = list314; - reference381 = obj219; + obj214.Steps = list314; + reference381 = obj214; num++; ref QuestSequence reference382 = ref span2[num]; questSequence = new QuestSequence { Sequence = 4 }; - QuestSequence questSequence106 = questSequence; + QuestSequence questSequence111 = questSequence; index2 = 2; list4 = new List(index2); CollectionsMarshal.SetCount(list4, index2); @@ -359637,11 +359772,11 @@ public static class AssemblyQuestLoader }; num2++; span3[num2] = new QuestStep(EInteractionType.Interact, 1040447u, new Vector3(-710.0176f, -31.53043f, 322.59094f), 956); - questSequence106.Steps = list4; + questSequence111.Steps = list4; reference382 = questSequence; num++; ref QuestSequence reference383 = ref span2[num]; - QuestSequence obj220 = new QuestSequence + QuestSequence obj215 = new QuestSequence { Sequence = 5 }; @@ -359651,11 +359786,11 @@ public static class AssemblyQuestLoader span3 = CollectionsMarshal.AsSpan(list315); index2 = 0; span3[index2] = new QuestStep(EInteractionType.Interact, 1040450u, new Vector3(-754.4213f, -29.529999f, 389.70007f), 956); - obj220.Steps = list315; - reference383 = obj220; + obj215.Steps = list315; + reference383 = obj215; num++; ref QuestSequence reference384 = ref span2[num]; - QuestSequence obj221 = new QuestSequence + QuestSequence obj216 = new QuestSequence { Sequence = byte.MaxValue }; @@ -359665,8 +359800,8 @@ public static class AssemblyQuestLoader span3 = CollectionsMarshal.AsSpan(list316); num2 = 0; span3[num2] = new QuestStep(EInteractionType.CompleteQuest, 1040447u, new Vector3(-710.0176f, -31.53043f, 322.59094f), 956); - obj221.Steps = list316; - reference384 = obj221; + obj216.Steps = list316; + reference384 = obj216; questRoot95.QuestSequence = list2; AddQuest(questId47, questRoot); QuestId questId48 = new QuestId(4447); @@ -359686,10 +359821,11 @@ public static class AssemblyQuestLoader span2 = CollectionsMarshal.AsSpan(list2); num = 0; ref QuestSequence reference385 = ref span2[num]; - QuestSequence obj222 = new QuestSequence + questSequence = new QuestSequence { Sequence = 0 }; + QuestSequence questSequence112 = questSequence; num2 = 1; List list318 = new List(num2); CollectionsMarshal.SetCount(list318, num2); @@ -359699,11 +359835,11 @@ public static class AssemblyQuestLoader { StopDistance = 5f }; - obj222.Steps = list318; - reference385 = obj222; + questSequence112.Steps = list318; + reference385 = questSequence; num++; ref QuestSequence reference386 = ref span2[num]; - QuestSequence obj223 = new QuestSequence + QuestSequence obj217 = new QuestSequence { Sequence = 1 }; @@ -359713,15 +359849,15 @@ public static class AssemblyQuestLoader span3 = CollectionsMarshal.AsSpan(list319); num2 = 0; span3[num2] = new QuestStep(EInteractionType.Interact, 1040454u, new Vector3(-613.18384f, -19.786552f, 626.8557f), 956); - obj223.Steps = list319; - reference386 = obj223; + obj217.Steps = list319; + reference386 = obj217; num++; ref QuestSequence reference387 = ref span2[num]; questSequence = new QuestSequence { Sequence = 2 }; - QuestSequence questSequence107 = questSequence; + QuestSequence questSequence113 = questSequence; num2 = 1; list4 = new List(num2); CollectionsMarshal.SetCount(list4, num2); @@ -359745,11 +359881,11 @@ public static class AssemblyQuestLoader span6[num3] = 13980u; questStep63.KillEnemyDataIds = list320; reference388 = questStep; - questSequence107.Steps = list4; + questSequence113.Steps = list4; reference387 = questSequence; num++; ref QuestSequence reference389 = ref span2[num]; - QuestSequence obj224 = new QuestSequence + QuestSequence obj218 = new QuestSequence { Sequence = 3 }; @@ -359759,15 +359895,15 @@ public static class AssemblyQuestLoader span3 = CollectionsMarshal.AsSpan(list321); num2 = 0; span3[num2] = new QuestStep(EInteractionType.Interact, 2012442u, new Vector3(-594.5678f, -26.596497f, 609.09436f), 956); - obj224.Steps = list321; - reference389 = obj224; + obj218.Steps = list321; + reference389 = obj218; num++; ref QuestSequence reference390 = ref span2[num]; questSequence = new QuestSequence { Sequence = byte.MaxValue }; - QuestSequence questSequence108 = questSequence; + QuestSequence questSequence114 = questSequence; num2 = 8; list4 = new List(num2); CollectionsMarshal.SetCount(list4, num2); @@ -359815,7 +359951,7 @@ public static class AssemblyQuestLoader }; index2++; span3[index2] = new QuestStep(EInteractionType.CompleteQuest, 1040455u, new Vector3(10.147156f, -30.155546f, 619.62305f), 956); - questSequence108.Steps = list4; + questSequence114.Steps = list4; reference390 = questSequence; questRoot97.QuestSequence = list2; AddQuest(questId48, questRoot); @@ -359836,7 +359972,7 @@ public static class AssemblyQuestLoader span2 = CollectionsMarshal.AsSpan(list2); num = 0; ref QuestSequence reference391 = ref span2[num]; - QuestSequence obj225 = new QuestSequence + QuestSequence obj219 = new QuestSequence { Sequence = 0 }; @@ -359846,15 +359982,15 @@ public static class AssemblyQuestLoader span3 = CollectionsMarshal.AsSpan(list323); num2 = 0; span3[num2] = new QuestStep(EInteractionType.AcceptQuest, 1040455u, new Vector3(10.147156f, -30.155546f, 619.62305f), 956); - obj225.Steps = list323; - reference391 = obj225; + obj219.Steps = list323; + reference391 = obj219; num++; ref QuestSequence reference392 = ref span2[num]; questSequence = new QuestSequence { Sequence = 1 }; - QuestSequence questSequence109 = questSequence; + QuestSequence questSequence115 = questSequence; num2 = 2; list4 = new List(num2); CollectionsMarshal.SetCount(list4, num2); @@ -359880,11 +360016,11 @@ public static class AssemblyQuestLoader }; questStep64.DialogueChoices = list324; reference393 = questStep64; - questSequence109.Steps = list4; + questSequence115.Steps = list4; reference392 = questSequence; num++; ref QuestSequence reference394 = ref span2[num]; - QuestSequence obj226 = new QuestSequence + QuestSequence obj220 = new QuestSequence { Sequence = 2 }; @@ -359894,15 +360030,15 @@ public static class AssemblyQuestLoader span3 = CollectionsMarshal.AsSpan(list325); num2 = 0; span3[num2] = new QuestStep(EInteractionType.Interact, 1040457u, new Vector3(36.51477f, -16.246998f, 129.47266f), 962); - obj226.Steps = list325; - reference394 = obj226; + obj220.Steps = list325; + reference394 = obj220; num++; ref QuestSequence reference395 = ref span2[num]; questSequence = new QuestSequence { Sequence = 3 }; - QuestSequence questSequence110 = questSequence; + QuestSequence questSequence116 = questSequence; num2 = 1; List list326 = new List(num2); CollectionsMarshal.SetCount(list326, num2); @@ -359912,7 +360048,7 @@ public static class AssemblyQuestLoader { AetheryteShortcut = EAetheryteLocation.LabyrinthosAporia }; - questSequence110.Steps = list326; + questSequence116.Steps = list326; reference395 = questSequence; num++; ref QuestSequence reference396 = ref span2[num]; @@ -359920,7 +360056,7 @@ public static class AssemblyQuestLoader { Sequence = 4 }; - QuestSequence questSequence111 = questSequence; + QuestSequence questSequence117 = questSequence; index2 = 1; list4 = new List(index2); CollectionsMarshal.SetCount(list4, index2); @@ -359940,7 +360076,7 @@ public static class AssemblyQuestLoader }; questStep65.DialogueChoices = list327; reference397 = questStep65; - questSequence111.Steps = list4; + questSequence117.Steps = list4; reference396 = questSequence; num++; ref QuestSequence reference398 = ref span2[num]; @@ -359948,7 +360084,7 @@ public static class AssemblyQuestLoader { Sequence = 5 }; - QuestSequence questSequence112 = questSequence; + QuestSequence questSequence118 = questSequence; num2 = 1; List list328 = new List(num2); CollectionsMarshal.SetCount(list328, num2); @@ -359958,11 +360094,11 @@ public static class AssemblyQuestLoader { StopDistance = 7f }; - questSequence112.Steps = list328; + questSequence118.Steps = list328; reference398 = questSequence; num++; ref QuestSequence reference399 = ref span2[num]; - QuestSequence obj227 = new QuestSequence + QuestSequence obj221 = new QuestSequence { Sequence = 6 }; @@ -359975,11 +360111,11 @@ public static class AssemblyQuestLoader { RestartNavigationIfCancelled = false }; - obj227.Steps = list329; - reference399 = obj227; + obj221.Steps = list329; + reference399 = obj221; num++; ref QuestSequence reference400 = ref span2[num]; - QuestSequence obj228 = new QuestSequence + QuestSequence obj222 = new QuestSequence { Sequence = byte.MaxValue }; @@ -359989,8 +360125,8 @@ public static class AssemblyQuestLoader span3 = CollectionsMarshal.AsSpan(list330); index2 = 0; span3[index2] = new QuestStep(EInteractionType.CompleteQuest, 1040478u, new Vector3(-406.9734f, -220.18355f, 304.1886f), 956); - obj228.Steps = list330; - reference400 = obj228; + obj222.Steps = list330; + reference400 = obj222; questRoot99.QuestSequence = list2; AddQuest(questId49, questRoot); QuestId questId50 = new QuestId(4449); @@ -360010,7 +360146,7 @@ public static class AssemblyQuestLoader span2 = CollectionsMarshal.AsSpan(list2); num = 0; ref QuestSequence reference401 = ref span2[num]; - QuestSequence obj229 = new QuestSequence + QuestSequence obj223 = new QuestSequence { Sequence = 0 }; @@ -360020,11 +360156,11 @@ public static class AssemblyQuestLoader span3 = CollectionsMarshal.AsSpan(list332); num2 = 0; span3[num2] = new QuestStep(EInteractionType.AcceptQuest, 1040478u, new Vector3(-406.9734f, -220.18355f, 304.1886f), 956); - obj229.Steps = list332; - reference401 = obj229; + obj223.Steps = list332; + reference401 = obj223; num++; ref QuestSequence reference402 = ref span2[num]; - QuestSequence obj230 = new QuestSequence + QuestSequence obj224 = new QuestSequence { Sequence = 1 }; @@ -360034,15 +360170,15 @@ public static class AssemblyQuestLoader span3 = CollectionsMarshal.AsSpan(list333); index2 = 0; span3[index2] = new QuestStep(EInteractionType.Interact, 1040487u, new Vector3(-234.6991f, -224.38274f, 349.01953f), 956); - obj230.Steps = list333; - reference402 = obj230; + obj224.Steps = list333; + reference402 = obj224; num++; ref QuestSequence reference403 = ref span2[num]; questSequence = new QuestSequence { Sequence = 2 }; - QuestSequence questSequence113 = questSequence; + QuestSequence questSequence119 = questSequence; index2 = 1; list4 = new List(index2); CollectionsMarshal.SetCount(list4, index2); @@ -360050,7 +360186,7 @@ public static class AssemblyQuestLoader num2 = 0; ref QuestStep reference404 = ref span3[num2]; QuestStep questStep66 = new QuestStep(EInteractionType.Duty, null, null, 956); - DutyOptions obj231 = new DutyOptions + DutyOptions obj225 = new DutyOptions { ContentFinderConditionId = 786u }; @@ -360060,10 +360196,10 @@ public static class AssemblyQuestLoader span = CollectionsMarshal.AsSpan(list334); index3 = 0; span[index3] = "No VBM module"; - obj231.Notes = list334; - questStep66.DutyOptions = obj231; + obj225.Notes = list334; + questStep66.DutyOptions = obj225; reference404 = questStep66; - questSequence113.Steps = list4; + questSequence119.Steps = list4; reference403 = questSequence; num++; span2[num] = new QuestSequence @@ -360076,7 +360212,7 @@ public static class AssemblyQuestLoader { Sequence = 4 }; - QuestSequence questSequence114 = questSequence; + QuestSequence questSequence120 = questSequence; num2 = 1; List list335 = new List(num2); CollectionsMarshal.SetCount(list335, num2); @@ -360090,7 +360226,7 @@ public static class AssemblyQuestLoader ContentFinderConditionId = 790u } }; - questSequence114.Steps = list335; + questSequence120.Steps = list335; reference405 = questSequence; num++; span2[num] = new QuestSequence @@ -360099,7 +360235,7 @@ public static class AssemblyQuestLoader }; num++; ref QuestSequence reference406 = ref span2[num]; - QuestSequence obj232 = new QuestSequence + QuestSequence obj226 = new QuestSequence { Sequence = 6 }; @@ -360109,22 +360245,22 @@ public static class AssemblyQuestLoader span3 = CollectionsMarshal.AsSpan(list336); num2 = 0; span3[num2] = new QuestStep(EInteractionType.Interact, 1040503u, new Vector3(-221.4237f, -224.3827f, 364.40063f), 956); - obj232.Steps = list336; - reference406 = obj232; + obj226.Steps = list336; + reference406 = obj226; num++; ref QuestSequence reference407 = ref span2[num]; questSequence = new QuestSequence { Sequence = byte.MaxValue }; - QuestSequence questSequence115 = questSequence; + QuestSequence questSequence121 = questSequence; num2 = 1; list4 = new List(num2); CollectionsMarshal.SetCount(list4, num2); span3 = CollectionsMarshal.AsSpan(list4); index2 = 0; ref QuestStep reference408 = ref span3[index2]; - QuestStep obj233 = new QuestStep(EInteractionType.CompleteQuest, 1038588u, new Vector3(-101.76245f, 4.357494f, 0.7476196f), 962) + QuestStep obj227 = new QuestStep(EInteractionType.CompleteQuest, 1038588u, new Vector3(-101.76245f, 4.357494f, 0.7476196f), 962) { StopDistance = 5f, AethernetShortcut = new AethernetShortcut @@ -360143,9 +360279,9 @@ public static class AssemblyQuestLoader Type = EDialogChoiceType.YesNo, Prompt = new ExcelRef("TEXT_AKTKMF111_04449_Q4_000_344") }; - obj233.DialogueChoices = list337; - reference408 = obj233; - questSequence115.Steps = list4; + obj227.DialogueChoices = list337; + reference408 = obj227; + questSequence121.Steps = list4; reference407 = questSequence; questRoot101.QuestSequence = list2; AddQuest(questId50, questRoot); @@ -411967,7 +412103,7 @@ public static class AssemblyQuestLoader index = 0; span[index] = "liza"; questRoot3.Author = list15; - index = 3; + index = 4; List list16 = new List(index); CollectionsMarshal.SetCount(list16, index); span2 = CollectionsMarshal.AsSpan(list16); @@ -412012,6 +412148,11 @@ public static class AssemblyQuestLoader obj12.Steps = list18; reference12 = obj12; num++; + span2[num] = new QuestSequence + { + Sequence = 2 + }; + num++; ref QuestSequence reference13 = ref span2[num]; QuestSequence obj13 = new QuestSequence { @@ -413006,7 +413147,7 @@ public static class AssemblyQuestLoader index = 0; span[index] = "liza"; questRoot9.Author = list79; - index = 5; + index = 6; List list80 = new List(index); CollectionsMarshal.SetCount(list80, index); span2 = CollectionsMarshal.AsSpan(list80); @@ -413079,6 +413220,11 @@ public static class AssemblyQuestLoader obj55.Steps = list84; reference66 = obj55; num++; + span2[num] = new QuestSequence + { + Sequence = 4 + }; + num++; ref QuestSequence reference67 = ref span2[num]; QuestSequence obj56 = new QuestSequence { diff --git a/Questionable/Questionable.Controller.Steps.Interactions/Interact.cs b/Questionable/Questionable.Controller.Steps.Interactions/Interact.cs index 40fa3db..defd9ac 100644 --- a/Questionable/Questionable.Controller.Steps.Interactions/Interact.cs +++ b/Questionable/Questionable.Controller.Steps.Interactions/Interact.cs @@ -173,6 +173,10 @@ internal static class Interact protected override bool Start() { InteractionType = base.Task.InteractionType; + _interactionState = EInteractionState.None; + _needsUnmount = false; + delayedFinalCheck = false; + _continueAt = DateTime.MinValue; IGameObject gameObject = gameFunctions.FindObjectByDataId(base.Task.DataId); if (gameObject == null) { @@ -259,6 +263,7 @@ internal static class Interact { if (base.ProgressContext.WasInterrupted()) { + logger.LogDebug("Interaction with {DataId} was interrupted", base.Task.DataId); return ETaskResult.StillRunning; } if (base.ProgressContext.WasSuccessful() || _interactionState == EInteractionState.InteractionConfirmed) diff --git a/Questionable/Questionable.Controller.Steps.Shared/WaitAtEnd.cs b/Questionable/Questionable.Controller.Steps.Shared/WaitAtEnd.cs index 3266500..bfbbae0 100644 --- a/Questionable/Questionable.Controller.Steps.Shared/WaitAtEnd.cs +++ b/Questionable/Questionable.Controller.Steps.Shared/WaitAtEnd.cs @@ -196,15 +196,40 @@ internal static class WaitAtEnd } } - internal sealed class WaitNextStepOrSequenceExecutor : TaskExecutor + internal sealed class WaitNextStepOrSequenceExecutor(QuestController questController) : TaskExecutor() { + private ElementId? _questId; + + private byte _initialSequence; + + private int _initialStep; + protected override bool Start() { + QuestController.QuestProgress currentQuest = questController.CurrentQuest; + if (currentQuest != null) + { + _questId = currentQuest.Quest.Id; + _initialSequence = currentQuest.Sequence; + _initialStep = currentQuest.Step; + } return true; } public override ETaskResult Update() { + if (_questId != null) + { + QuestController.QuestProgress currentQuest = questController.CurrentQuest; + if (currentQuest == null || currentQuest.Quest.Id != _questId) + { + return ETaskResult.TaskComplete; + } + if (currentQuest.Sequence != _initialSequence || currentQuest.Step != _initialStep) + { + return ETaskResult.TaskComplete; + } + } return ETaskResult.StillRunning; } diff --git a/Questionable/Questionable.Controller.Steps/TaskExecutor.cs b/Questionable/Questionable.Controller.Steps/TaskExecutor.cs index bc18729..ab27e90 100644 --- a/Questionable/Questionable.Controller.Steps/TaskExecutor.cs +++ b/Questionable/Questionable.Controller.Steps/TaskExecutor.cs @@ -33,6 +33,7 @@ internal abstract class TaskExecutor : ITaskExecutor where T : class, ITask if (task is T task2) { Task = task2; + ProgressContext = null; return Start(); } throw new TaskException($"Unable to cast {task.GetType()} to {typeof(T)}"); diff --git a/Questionable/Questionable.Controller/InterruptHandler.cs b/Questionable/Questionable.Controller/InterruptHandler.cs index 800c811..f336ec8 100644 --- a/Questionable/Questionable.Controller/InterruptHandler.cs +++ b/Questionable/Questionable.Controller/InterruptHandler.cs @@ -15,11 +15,6 @@ internal sealed class InterruptHandler : IDisposable { private unsafe delegate void ProcessActionEffect(uint sourceId, Character* sourceCharacter, Vector3* pos, EffectHeader* effectHeader, EffectEntry* effectArray, ulong* effectTail); - private static class Signatures - { - internal const string ActionEffect = "40 ?? 56 57 41 ?? 41 ?? 41 ?? 48 ?? ?? ?? ?? ?? ?? ?? 48"; - } - [StructLayout(LayoutKind.Explicit)] private struct EffectEntry { @@ -150,7 +145,7 @@ internal sealed class InterruptHandler : IDisposable _objectTable = objectTable; _territoryData = territoryData; _logger = logger; - _processActionEffectHook = gameInteropProvider.HookFromSignature("40 ?? 56 57 41 ?? 41 ?? 41 ?? 48 ?? ?? ?? ?? ?? ?? ?? 48", HandleProcessActionEffect); + _processActionEffectHook = gameInteropProvider.HookFromAddress(ActionEffectHandler.Addresses.Receive.Value, HandleProcessActionEffect); _processActionEffectHook.Enable(); } diff --git a/Questionable/Questionable.Controller/MovementController.cs b/Questionable/Questionable.Controller/MovementController.cs index 4371bec..5e68569 100644 --- a/Questionable/Questionable.Controller/MovementController.cs +++ b/Questionable/Questionable.Controller/MovementController.cs @@ -85,6 +85,8 @@ internal sealed class MovementController : IDisposable private readonly AetheryteData _aetheryteData; + private readonly Configuration _configuration; + private readonly ILogger _logger; private CancellationTokenSource? _cancellationTokenSource; @@ -93,6 +95,14 @@ internal sealed class MovementController : IDisposable private long _pathfindStartTime; + private Vector3? _lastKnownPosition; + + private long _lastPositionUpdateTime; + + private Vector3? _expectedPosition; + + private bool _isTrackingPlayerInput; + public bool IsNavmeshReady { get @@ -146,7 +156,9 @@ internal sealed class MovementController : IDisposable public int NumQueuedPathfindRequests => _navmeshIpc.NumQueuedPathfindRequests; - public MovementController(NavmeshIpc navmeshIpc, IClientState clientState, IObjectTable objectTable, GameFunctions gameFunctions, ChatFunctions chatFunctions, ICondition condition, MovementOverrideController movementOverrideController, AetheryteData aetheryteData, ILogger logger) + public event EventHandler? PlayerInputDetected; + + public MovementController(NavmeshIpc navmeshIpc, IClientState clientState, IObjectTable objectTable, GameFunctions gameFunctions, ChatFunctions chatFunctions, ICondition condition, MovementOverrideController movementOverrideController, AetheryteData aetheryteData, Configuration configuration, ILogger logger) { _navmeshIpc = navmeshIpc; _clientState = clientState; @@ -156,11 +168,19 @@ internal sealed class MovementController : IDisposable _condition = condition; _movementOverrideController = movementOverrideController; _aetheryteData = aetheryteData; + _configuration = configuration; _logger = logger; } public unsafe void Update() { + if (IsPathRunning && _isTrackingPlayerInput && DetectPlayerInputInterference()) + { + _logger.LogInformation("Player input detected during automatic movement, raising event to stop automation"); + this.PlayerInputDetected?.Invoke(this, EventArgs.Empty); + Stop(); + return; + } if (_pathfindTask != null && Destination != null) { if (!_pathfindTask.IsCompleted && Environment.TickCount64 - _pathfindStartTime > 30000 && _navmeshIpc.NumQueuedPathfindRequests > 5) @@ -188,6 +208,11 @@ internal sealed class MovementController : IDisposable if (Destination.IsFlying && Destination.Land) { _logger.LogWarning("Adjusted destination failed, trying tolerance-based pathfinding"); + if (!IsNavmeshReady) + { + _logger.LogWarning("Navmesh not ready for tolerance-based pathfinding"); + return; + } _cancellationTokenSource = new CancellationTokenSource(); _cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(30L)); Vector3 vector2 = _objectTable[0]?.Position ?? Vector3.Zero; @@ -218,6 +243,11 @@ internal sealed class MovementController : IDisposable (list, _) = tuple; if (tuple.Item2 && Destination.ShouldRecalculateNavmesh()) { + if (!IsNavmeshReady) + { + _logger.LogWarning("Navmesh not ready for recalculation"); + return; + } Destination.NavmeshCalculations++; Destination.PartialRoute.AddRange(list); _logger.LogInformation("Running navmesh recalculation with fudged point ({From} to {To})", list.Last(), Destination.Position); @@ -232,6 +262,7 @@ internal sealed class MovementController : IDisposable _logger.LogInformation("Navigating via route: [{Route}]", string.Join(" → ", _pathfindTask.Result.Select((Vector3 x) => x.ToString("G", CultureInfo.InvariantCulture)))); _navmeshIpc.MoveTo(list, Destination.IsFlying); MovementStartedAt = DateTime.Now; + StartPlayerInputTracking(); ResetPathfinding(); } else if (_pathfindTask.IsCompleted) @@ -321,6 +352,72 @@ internal sealed class MovementController : IDisposable } } + private void StartPlayerInputTracking() + { + IGameObject gameObject = _objectTable[0]; + if (gameObject != null) + { + _lastKnownPosition = gameObject.Position; + _expectedPosition = gameObject.Position; + _lastPositionUpdateTime = Environment.TickCount64; + _isTrackingPlayerInput = true; + } + } + + private bool DetectPlayerInputInterference() + { + if (!_configuration.General.StopOnPlayerInput) + { + return false; + } + if (!_isTrackingPlayerInput || !_lastKnownPosition.HasValue) + { + return false; + } + IGameObject gameObject = _objectTable[0]; + if (gameObject == null) + { + return false; + } + Vector3 position = gameObject.Position; + long tickCount = Environment.TickCount64; + if (tickCount - _lastPositionUpdateTime < 100) + { + return false; + } + List waypoints = _navmeshIpc.GetWaypoints(); + if (waypoints.Count > 0) + { + _expectedPosition = waypoints[0]; + } + if (_expectedPosition.HasValue) + { + Vector3 vector = Vector3.Normalize(_expectedPosition.Value - _lastKnownPosition.Value); + Vector3 value = position - _lastKnownPosition.Value; + if (value.Length() > 0.1f) + { + Vector3 vector2 = Vector3.Normalize(value); + float num = Vector3.Dot(vector, vector2); + if (num < 0.7f) + { + _logger.LogDebug("Player movement detected: alignment={Alignment:F2}, actual={Actual}, expected={Expected}", num, value.ToString("G", CultureInfo.InvariantCulture), vector.ToString("G", CultureInfo.InvariantCulture)); + return true; + } + } + } + _lastKnownPosition = position; + _lastPositionUpdateTime = tickCount; + return false; + } + + private void StopPlayerInputTracking() + { + _isTrackingPlayerInput = false; + _lastKnownPosition = null; + _expectedPosition = null; + _lastPositionUpdateTime = 0L; + } + private void Restart(DestinationData destination) { Stop(); @@ -364,6 +461,11 @@ internal sealed class MovementController : IDisposable public void NavigateTo(EMovementType type, uint? dataId, Vector3 to, bool fly, bool sprint, float? stopDistance = null, float? verticalStopDistance = null, bool land = false) { + if (!IsNavmeshReady) + { + _logger.LogWarning("Navmesh not ready, cannot start navigation to {Position}", to.ToString("G", CultureInfo.InvariantCulture)); + return; + } fly |= _condition[ConditionFlag.Diving]; if (fly && land) { @@ -404,6 +506,11 @@ internal sealed class MovementController : IDisposable public void NavigateTo(EMovementType type, uint? dataId, List to, bool fly, bool sprint, float? stopDistance, float? verticalStopDistance = null, bool land = false) { + if (!IsNavmeshReady) + { + _logger.LogWarning("Navmesh not ready, cannot start navigation to {Position}", to.Last().ToString("G", CultureInfo.InvariantCulture)); + return; + } fly |= _condition[ConditionFlag.Diving]; if (fly && land && to.Count > 0) { @@ -416,6 +523,7 @@ internal sealed class MovementController : IDisposable _logger.LogInformation("Moving to {Destination}", Destination); _navmeshIpc.MoveTo(to, fly); MovementStartedAt = DateTime.Now; + StartPlayerInputTracking(); } public void ResetPathfinding() @@ -436,6 +544,11 @@ internal sealed class MovementController : IDisposable private Vector3? TryFindAccessibleDestination(Vector3 target, bool flying, bool landing) { + if (!IsNavmeshReady) + { + _logger.LogWarning("Navmesh not ready, cannot find accessible destination"); + return null; + } float[] array = ((!(flying && landing)) ? ((!flying) ? new float[3] { 1f, 3f, 5f } : new float[3] { 2f, 5f, 10f }) : new float[3] { 5f, 10f, 15f }); float[] array2 = ((!flying) ? new float[3] { 1f, 2f, 3f } : new float[3] { 3f, 5f, 10f }); for (int i = 0; i < array.Length; i++) @@ -504,17 +617,52 @@ internal sealed class MovementController : IDisposable if (Math.Abs((double)num - Destination.LastWaypoint.Distance2DAtLastUpdate) < 0.5) { int navmeshCalculations = Destination.NavmeshCalculations; - if (navmeshCalculations % 6 == 1) + switch (navmeshCalculations) { - _logger.LogWarning("Jumping to try and resolve navmesh problem (n = {Calculations})", navmeshCalculations); + case 1: + case 7: + _logger.LogWarning("Jumping to try and resolve navmesh problem (n = {Calculations}) at {Position}", navmeshCalculations, Destination.Position.ToString("G", CultureInfo.InvariantCulture)); ActionManager.Instance()->UseAction(ActionType.GeneralAction, 2u, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null); Destination.NavmeshCalculations++; Destination.LastWaypoint.UpdatedAt = Environment.TickCount64; - } - else - { - _logger.LogWarning("Recalculating navmesh (n = {Calculations})", navmeshCalculations); + break; + case 5: + _logger.LogWarning("Reloading navmesh (n = {Calculations}) at {Position}", navmeshCalculations, Destination.Position.ToString("G", CultureInfo.InvariantCulture)); + _navmeshIpc.Reload(); + Destination.LastWaypoint.UpdatedAt = Environment.TickCount64; + break; + case 6: + if (!IsNavmeshReady) + { + _logger.LogWarning("Navmesh not ready after reload (n = {Calculations})", navmeshCalculations); + return false; + } + _logger.LogInformation("Navmesh ready after reload, restarting navigation (n = {Calculations})", navmeshCalculations); Restart(Destination); + break; + case 8: + _logger.LogWarning("Rebuilding navmesh (n = {Calculations}) at {Position}", navmeshCalculations, Destination.Position.ToString("G", CultureInfo.InvariantCulture)); + _navmeshIpc.Rebuild(); + Destination.LastWaypoint.UpdatedAt = Environment.TickCount64; + break; + case 9: + if (!IsNavmeshReady) + { + _logger.LogWarning("Navmesh not ready after rebuild (n = {Calculations})", navmeshCalculations); + return false; + } + _logger.LogInformation("Navmesh ready after rebuild, restarting navigation (n = {Calculations})", navmeshCalculations); + Restart(Destination); + break; + default: + if (!IsNavmeshReady) + { + _logger.LogWarning("Navmesh not ready for recalculation (n = {Calculations})", navmeshCalculations); + return false; + } + _logger.LogWarning("Recalculating navmesh (n = {Calculations}) at {Position}", navmeshCalculations, Destination.Position.ToString("G", CultureInfo.InvariantCulture)); + Restart(Destination); + break; } Destination.NavmeshCalculations = navmeshCalculations + 1; return true; @@ -570,6 +718,7 @@ internal sealed class MovementController : IDisposable public void Stop() { + StopPlayerInputTracking(); _navmeshIpc.Stop(); ResetPathfinding(); Destination = null; diff --git a/Questionable/Questionable.Controller/QuestController.cs b/Questionable/Questionable.Controller/QuestController.cs index 19def79..8cf8137 100644 --- a/Questionable/Questionable.Controller/QuestController.cs +++ b/Questionable/Questionable.Controller/QuestController.cs @@ -257,6 +257,7 @@ internal sealed class QuestController : MiniTaskController _toastGui.Toast += OnNormalToast; _condition.ConditionChange += OnConditionChange; _clientState.Logout += OnLogout; + _movementController.PlayerInputDetected += OnPlayerInputDetected; } public void Reload() @@ -683,55 +684,71 @@ internal sealed class QuestController : MiniTaskController { DebugState = "No quest active"; Stop("No quest active"); - return; } - if (_gameFunctions.IsOccupied() && !_gameFunctions.IsOccupiedWithCustomDeliveryNpc(questProgress.Quest)) + else if (questProgress.Step == 255) + { + DebugState = $"Waiting for sequence update (current: {questProgress.Sequence})"; + if (!_taskQueue.AllTasksComplete) + { + DebugState = "Step 255 - processing interrupted tasks"; + } + else + { + if (this.CurrentQuest == null) + { + return; + } + TimeSpan timeSpan = DateTime.Now - this.CurrentQuest.StepProgress.StartedAt; + if (timeSpan > TimeSpan.FromSeconds(3L)) + { + _logger.LogWarning("Step 255 with no tasks for {WaitTime:F1}s, retrying step to ensure completion (quest: {QuestId}, sequence: {Sequence})", timeSpan.TotalSeconds, questProgress.Quest.Id, questProgress.Sequence); + QuestSequence questSequence = questProgress.Quest.FindSequence(questProgress.Sequence); + if (questSequence != null && questSequence.Steps.Count > 0) + { + this.CurrentQuest.SetStep(questSequence.Steps.Count - 1); + CheckNextTasks("Retry last step at 255"); + } + } + } + } + else if (_gameFunctions.IsOccupied() && !_gameFunctions.IsOccupiedWithCustomDeliveryNpc(questProgress.Quest)) { DebugState = "Occupied"; - return; } - if (_movementController.IsPathfinding) + else if (_movementController.IsPathfinding) { DebugState = "Pathfinding is running"; - return; } - if (_movementController.IsPathRunning) + else if (_movementController.IsPathRunning) { DebugState = "Path is running"; - return; } - if (DateTime.Now < _safeAnimationEnd) + else if (DateTime.Now < _safeAnimationEnd) { DebugState = "Waiting for Animation"; - return; } - if (questProgress.Sequence != b) + else if (questProgress.Sequence != b) { questProgress.SetSequence(b); CheckNextTasks($"New sequence {questProgress == _startedQuest}/{_questFunctions.GetCurrentQuestInternal(allowNewMsq: true)}"); } - QuestSequence questSequence = questProgress.Quest.FindSequence(questProgress.Sequence); - if (questSequence == null) - { - DebugState = $"Sequence {questProgress.Sequence} not found"; - Stop("Unknown sequence"); - } - else if (questProgress.Step == 255) - { - DebugState = "Step completed"; - if (!_taskQueue.AllTasksComplete) - { - CheckNextTasks("Step complete"); - } - } - else if (questSequence.Steps.Count > 0 && questProgress.Step >= questSequence.Steps.Count) - { - DebugState = "Step not found"; - Stop("Unknown step"); - } else { - DebugState = null; + QuestSequence questSequence2 = questProgress.Quest.FindSequence(questProgress.Sequence); + if (questSequence2 == null) + { + DebugState = $"Sequence {questProgress.Sequence} not found"; + Stop("Unknown sequence"); + } + else if (questSequence2.Steps.Count > 0 && questProgress.Step >= questSequence2.Steps.Count) + { + DebugState = "Step not found"; + Stop("Unknown step"); + } + else + { + DebugState = null; + } } } } @@ -778,15 +795,21 @@ internal sealed class QuestController : MiniTaskController _logger.LogWarning("Ignoring 'increase step count' for different sequence (expected {ExpectedSequence}, but we are at {CurrentSequence}", sequence, questSequence.Sequence); } _logger.LogInformation("Increasing step count from {CurrentValue}", CurrentQuest.Step); - if (CurrentQuest.Step + 1 < questSequence.Steps.Count) - { - CurrentQuest.SetStep(CurrentQuest.Step + 1); - } - else + bool num = CurrentQuest.Step + 1 >= questSequence.Steps.Count; + if (num) { CurrentQuest.SetStep(255); } + else + { + CurrentQuest.SetStep(CurrentQuest.Step + 1); + } ResetAutoRefreshState(); + if (num) + { + _logger.LogInformation("Completed last step in sequence, waiting for game to update sequence"); + return; + } } using (_logger.BeginScope("IncStepCt")) { @@ -1291,12 +1314,23 @@ internal sealed class QuestController : MiniTaskController } } + private void OnPlayerInputDetected(object? sender, EventArgs e) + { + if (AutomationType != EAutomationType.Manual && IsRunning) + { + _logger.LogInformation("Player input detected during movement, stopping quest automation"); + _chatGui.Print("Player input detected - stopping quest automation.", "Questionable", 576); + Stop("Player input detected"); + } + } + public override void Dispose() { _toastGui.ErrorToast -= base.OnErrorToast; _toastGui.Toast -= OnNormalToast; _condition.ConditionChange -= OnConditionChange; _clientState.Logout -= OnLogout; + _movementController.PlayerInputDetected -= OnPlayerInputDetected; base.Dispose(); } } diff --git a/Questionable/Questionable.Data/ChangelogData.cs b/Questionable/Questionable.Data/ChangelogData.cs index 7ff0c7f..ba17fc2 100644 --- a/Questionable/Questionable.Data/ChangelogData.cs +++ b/Questionable/Questionable.Data/ChangelogData.cs @@ -11,76 +11,80 @@ internal static class ChangelogData static ChangelogData() { - int num = 43; + int num = 44; List list = new List(num); CollectionsMarshal.SetCount(list, num); Span span = CollectionsMarshal.AsSpan(list); int num2 = 0; ref ChangelogEntry reference = ref span[num2]; - DateOnly releaseDate = new DateOnly(2025, 11, 29); + DateOnly releaseDate = new DateOnly(2025, 12, 6); int num3 = 2; List list2 = new List(num3); CollectionsMarshal.SetCount(list2, num3); Span span2 = CollectionsMarshal.AsSpan(list2); int num4 = 0; ref ChangeEntry reference2 = ref span2[num4]; - int num5 = 3; + int num5 = 4; List list3 = new List(num5); CollectionsMarshal.SetCount(list3, num5); Span span3 = CollectionsMarshal.AsSpan(list3); int num6 = 0; - span3[num6] = "Movement update with automatic retrying if character can't reach target position"; + span3[num6] = "Added reloading and rebuilding to movement system"; num6++; - span3[num6] = "Added Hunt mob data"; + span3[num6] = "Improved interrupts and refresh states to allow continuation of questing"; num6++; - span3[num6] = "Refactored commands"; + span3[num6] = "Added player input detection to stop automation when manually moving character"; + num6++; + span3[num6] = "Added various missing quest sequences"; reference2 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list3); num4++; ref ChangeEntry reference3 = ref span2[num4]; - num6 = 3; + num6 = 1; List list4 = new List(num6); CollectionsMarshal.SetCount(list4, num6); span3 = CollectionsMarshal.AsSpan(list4); num5 = 0; - span3[num5] = "Fixed quest (Way of the Archer)"; - num5++; - span3[num5] = "Fixed quest (Spirithold Broken)"; - num5++; - span3[num5] = "Fixed quest (It's Probably Not Pirates)"; - reference3 = new ChangeEntry(EChangeCategory.Fixed, "Bug fixes", list4); - reference = new ChangelogEntry("7.38.8", releaseDate, list2); + span3[num5] = "Fixed reset task state to prevent stuck interactions after interruption"; + reference3 = new ChangeEntry(EChangeCategory.Fixed, "Fixes", list4); + reference = new ChangelogEntry("7.38.9", releaseDate, list2); num2++; ref ChangelogEntry reference4 = ref span[num2]; - DateOnly releaseDate2 = new DateOnly(2025, 11, 25); + DateOnly releaseDate2 = new DateOnly(2025, 11, 29); num4 = 2; List list5 = new List(num4); CollectionsMarshal.SetCount(list5, num4); span2 = CollectionsMarshal.AsSpan(list5); num3 = 0; ref ChangeEntry reference5 = ref span2[num3]; - num5 = 2; + num5 = 3; List list6 = new List(num5); CollectionsMarshal.SetCount(list6, num5); span3 = CollectionsMarshal.AsSpan(list6); num6 = 0; - span3[num6] = "Added individual sequence stop condition for each quest"; + span3[num6] = "Movement update with automatic retrying if character can't reach target position"; num6++; - span3[num6] = "Added Trials to Duties tab in config"; - reference5 = new ChangeEntry(EChangeCategory.Added, "Major features", list6); + span3[num6] = "Added Hunt mob data"; + num6++; + span3[num6] = "Refactored commands"; + reference5 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list6); num3++; ref ChangeEntry reference6 = ref span2[num3]; - num6 = 1; + num6 = 3; List list7 = new List(num6); CollectionsMarshal.SetCount(list7, num6); span3 = CollectionsMarshal.AsSpan(list7); num5 = 0; - span3[num5] = "Added IPC for stop conditions: GetQuestSequenceStopCondition, SetQuestSequenceStopCondition, RemoveQuestSequenceStopCondition, GetAllQuestSequenceStopConditions"; - reference6 = new ChangeEntry(EChangeCategory.Added, "IPC changes", list7); - reference4 = new ChangelogEntry("7.38.7", releaseDate2, list5); + span3[num5] = "Fixed quest (Way of the Archer)"; + num5++; + span3[num5] = "Fixed quest (Spirithold Broken)"; + num5++; + span3[num5] = "Fixed quest (It's Probably Not Pirates)"; + reference6 = new ChangeEntry(EChangeCategory.Fixed, "Bug fixes", list7); + reference4 = new ChangelogEntry("7.38.8", releaseDate2, list5); num2++; ref ChangelogEntry reference7 = ref span[num2]; DateOnly releaseDate3 = new DateOnly(2025, 11, 25); - num3 = 3; + num3 = 2; List list8 = new List(num3); CollectionsMarshal.SetCount(list8, num3); span2 = CollectionsMarshal.AsSpan(list8); @@ -91,10 +95,10 @@ internal static class ChangelogData CollectionsMarshal.SetCount(list9, num5); span3 = CollectionsMarshal.AsSpan(list9); num6 = 0; - span3[num6] = "Updated Allied Society journal text"; + span3[num6] = "Added individual sequence stop condition for each quest"; num6++; - span3[num6] = "Improved Allied Society rank handling"; - reference8 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list9); + span3[num6] = "Added Trials to Duties tab in config"; + reference8 = new ChangeEntry(EChangeCategory.Added, "Major features", list9); num4++; ref ChangeEntry reference9 = ref span2[num4]; num6 = 1; @@ -102,34 +106,36 @@ internal static class ChangelogData CollectionsMarshal.SetCount(list10, num6); span3 = CollectionsMarshal.AsSpan(list10); num5 = 0; - span3[num5] = "Added IPC for Allied Society: AddAlliedSocietyOptimalQuests, GetAlliedSocietyOptimalQuests"; + span3[num5] = "Added IPC for stop conditions: GetQuestSequenceStopCondition, SetQuestSequenceStopCondition, RemoveQuestSequenceStopCondition, GetAllQuestSequenceStopConditions"; reference9 = new ChangeEntry(EChangeCategory.Added, "IPC changes", list10); - num4++; - ref ChangeEntry reference10 = ref span2[num4]; - num5 = 1; - List list11 = new List(num5); - CollectionsMarshal.SetCount(list11, num5); - span3 = CollectionsMarshal.AsSpan(list11); - num6 = 0; - span3[num6] = "Fixed quest (We Come in Peace)"; - reference10 = new ChangeEntry(EChangeCategory.Fixed, "Bug fixes", list11); - reference7 = new ChangelogEntry("7.38.6", releaseDate3, list8); + reference7 = new ChangelogEntry("7.38.7", releaseDate3, list8); num2++; - ref ChangelogEntry reference11 = ref span[num2]; - DateOnly releaseDate4 = new DateOnly(2025, 11, 24); - num4 = 2; - List list12 = new List(num4); - CollectionsMarshal.SetCount(list12, num4); - span2 = CollectionsMarshal.AsSpan(list12); + ref ChangelogEntry reference10 = ref span[num2]; + DateOnly releaseDate4 = new DateOnly(2025, 11, 25); + num4 = 3; + List list11 = new List(num4); + CollectionsMarshal.SetCount(list11, num4); + span2 = CollectionsMarshal.AsSpan(list11); num3 = 0; + ref ChangeEntry reference11 = ref span2[num3]; + num5 = 2; + List list12 = new List(num5); + CollectionsMarshal.SetCount(list12, num5); + span3 = CollectionsMarshal.AsSpan(list12); + num6 = 0; + span3[num6] = "Updated Allied Society journal text"; + num6++; + span3[num6] = "Improved Allied Society rank handling"; + reference11 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list12); + num3++; ref ChangeEntry reference12 = ref span2[num3]; num6 = 1; List list13 = new List(num6); CollectionsMarshal.SetCount(list13, num6); span3 = CollectionsMarshal.AsSpan(list13); num5 = 0; - span3[num5] = "Added Allied Society daily allowance tracker with bulk quest adding buttons"; - reference12 = new ChangeEntry(EChangeCategory.Added, "Major features", list13); + span3[num5] = "Added IPC for Allied Society: AddAlliedSocietyOptimalQuests, GetAlliedSocietyOptimalQuests"; + reference12 = new ChangeEntry(EChangeCategory.Added, "IPC changes", list13); num3++; ref ChangeEntry reference13 = ref span2[num3]; num5 = 1; @@ -137,12 +143,12 @@ internal static class ChangelogData CollectionsMarshal.SetCount(list14, num5); span3 = CollectionsMarshal.AsSpan(list14); num6 = 0; - span3[num6] = "Added IPC for Allied Society: GetRemainingAllowances, GetTimeUntilReset, GetAvailableQuestIds, GetAllAvailableQuestCounts, IsMaxRank, GetCurrentRank, GetSocietiesWithAvailableQuests"; - reference13 = new ChangeEntry(EChangeCategory.Added, "IPC changes", list14); - reference11 = new ChangelogEntry("7.38.5", releaseDate4, list12); + span3[num6] = "Fixed quest (We Come in Peace)"; + reference13 = new ChangeEntry(EChangeCategory.Fixed, "Bug fixes", list14); + reference10 = new ChangelogEntry("7.38.6", releaseDate4, list11); num2++; ref ChangelogEntry reference14 = ref span[num2]; - DateOnly releaseDate5 = new DateOnly(2025, 11, 23); + DateOnly releaseDate5 = new DateOnly(2025, 11, 24); num3 = 2; List list15 = new List(num3); CollectionsMarshal.SetCount(list15, num3); @@ -154,15 +160,41 @@ internal static class ChangelogData CollectionsMarshal.SetCount(list16, num6); span3 = CollectionsMarshal.AsSpan(list16); num5 = 0; - span3[num5] = "Explicitly declare support for BMR singleplayer duty (The Rematch)"; - reference15 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list16); + span3[num5] = "Added Allied Society daily allowance tracker with bulk quest adding buttons"; + reference15 = new ChangeEntry(EChangeCategory.Added, "Major features", list16); num4++; ref ChangeEntry reference16 = ref span2[num4]; - num5 = 8; + num5 = 1; List list17 = new List(num5); CollectionsMarshal.SetCount(list17, num5); span3 = CollectionsMarshal.AsSpan(list17); num6 = 0; + span3[num6] = "Added IPC for Allied Society: GetRemainingAllowances, GetTimeUntilReset, GetAvailableQuestIds, GetAllAvailableQuestCounts, IsMaxRank, GetCurrentRank, GetSocietiesWithAvailableQuests"; + reference16 = new ChangeEntry(EChangeCategory.Added, "IPC changes", list17); + reference14 = new ChangelogEntry("7.38.5", releaseDate5, list15); + num2++; + ref ChangelogEntry reference17 = ref span[num2]; + DateOnly releaseDate6 = new DateOnly(2025, 11, 23); + num4 = 2; + List list18 = new List(num4); + CollectionsMarshal.SetCount(list18, num4); + span2 = CollectionsMarshal.AsSpan(list18); + num3 = 0; + ref ChangeEntry reference18 = ref span2[num3]; + num6 = 1; + List list19 = new List(num6); + CollectionsMarshal.SetCount(list19, num6); + span3 = CollectionsMarshal.AsSpan(list19); + num5 = 0; + span3[num5] = "Explicitly declare support for BMR singleplayer duty (The Rematch)"; + reference18 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list19); + num3++; + ref ChangeEntry reference19 = ref span2[num3]; + num5 = 8; + List list20 = new List(num5); + CollectionsMarshal.SetCount(list20, num5); + span3 = CollectionsMarshal.AsSpan(list20); + num6 = 0; span3[num6] = "Fixed quest (Microbrewing) to not get stuck near ramp"; num6++; span3[num6] = "Fixed quest (The Illuminated Land) where pathing would kill the player due to fall damage"; @@ -178,69 +210,37 @@ internal static class ChangelogData span3[num6] = "Fixed quest (Poisoned Hearts) where incorrect pathing caused the player to die"; num6++; span3[num6] = "Fixed quests (Savage Snares) and (An Apple a Day) not detecting kills"; - reference16 = new ChangeEntry(EChangeCategory.Fixed, "Bug fixes", list17); - reference14 = new ChangelogEntry("7.38.4", releaseDate5, list15); + reference19 = new ChangeEntry(EChangeCategory.Fixed, "Bug fixes", list20); + reference17 = new ChangelogEntry("7.38.4", releaseDate6, list18); num2++; - ref ChangelogEntry reference17 = ref span[num2]; - DateOnly releaseDate6 = new DateOnly(2025, 11, 23); - num4 = 3; - List list18 = new List(num4); - CollectionsMarshal.SetCount(list18, num4); - span2 = CollectionsMarshal.AsSpan(list18); - num3 = 0; - ref ChangeEntry reference18 = ref span2[num3]; + ref ChangelogEntry reference20 = ref span[num2]; + DateOnly releaseDate7 = new DateOnly(2025, 11, 23); + num3 = 3; + List list21 = new List(num3); + CollectionsMarshal.SetCount(list21, num3); + span2 = CollectionsMarshal.AsSpan(list21); + num4 = 0; + ref ChangeEntry reference21 = ref span2[num4]; num6 = 2; - List list19 = new List(num6); - CollectionsMarshal.SetCount(list19, num6); - span3 = CollectionsMarshal.AsSpan(list19); + List list22 = new List(num6); + CollectionsMarshal.SetCount(list22, num6); + span3 = CollectionsMarshal.AsSpan(list22); num5 = 0; span3[num5] = "Added RequireHq to crafting InteractionType"; num5++; span3[num5] = "Mark GC quests as Locked if rank not achieved"; - reference18 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list19); - num3++; - ref ChangeEntry reference19 = ref span2[num3]; - num5 = 2; - List list20 = new List(num5); - CollectionsMarshal.SetCount(list20, num5); - span3 = CollectionsMarshal.AsSpan(list20); - num6 = 0; - span3[num6] = "Added IPC for stop conditions: GetStopConditionsEnabled, SetStopConditionsEnabled, GetStopQuestList, AddStopQuest, RemoveStopQuest, ClearStopQuests, GetLevelStopCondition, SetLevelStopCondition, GetSequenceStopCondition, SetSequenceStopCondition"; - num6++; - span3[num6] = "Added IPC for priority quests: GetPriorityQuests, RemovePriorityQuest, ReorderPriorityQuest, GetAvailablePresets, GetPresetQuests, AddPresetToPriority, IsPresetAvailable, IsQuestInPriority, GetQuestPriorityIndex, HasAvailablePriorityQuests"; - reference19 = new ChangeEntry(EChangeCategory.Added, "IPC changes", list20); - num3++; - ref ChangeEntry reference20 = ref span2[num3]; - num6 = 3; - List list21 = new List(num6); - CollectionsMarshal.SetCount(list21, num6); - span3 = CollectionsMarshal.AsSpan(list21); - num5 = 0; - span3[num5] = "Fixed line breaks not working in dialog strings"; - num5++; - span3[num5] = "Fixed quest (Labor of Love)"; - num5++; - span3[num5] = "Fixed quest (Sea of Sorrow)"; - reference20 = new ChangeEntry(EChangeCategory.Fixed, "Bug fixes", list21); - reference17 = new ChangelogEntry("7.38.3", releaseDate6, list18); - num2++; - ref ChangelogEntry reference21 = ref span[num2]; - DateOnly releaseDate7 = new DateOnly(2025, 11, 18); - num3 = 3; - List list22 = new List(num3); - CollectionsMarshal.SetCount(list22, num3); - span2 = CollectionsMarshal.AsSpan(list22); - num4 = 0; + reference21 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list22); + num4++; ref ChangeEntry reference22 = ref span2[num4]; num5 = 2; List list23 = new List(num5); CollectionsMarshal.SetCount(list23, num5); span3 = CollectionsMarshal.AsSpan(list23); num6 = 0; - span3[num6] = "Auto Duty unsync options for each duty (Duty Support, Unsync Solo, Unsync Party)"; + span3[num6] = "Added IPC for stop conditions: GetStopConditionsEnabled, SetStopConditionsEnabled, GetStopQuestList, AddStopQuest, RemoveStopQuest, ClearStopQuests, GetLevelStopCondition, SetLevelStopCondition, GetSequenceStopCondition, SetSequenceStopCondition"; num6++; - span3[num6] = "Added Auto Duty unsync options to quest schema and updated quests using old unsync method"; - reference22 = new ChangeEntry(EChangeCategory.Added, "Major features", list23); + span3[num6] = "Added IPC for priority quests: GetPriorityQuests, RemovePriorityQuest, ReorderPriorityQuest, GetAvailablePresets, GetPresetQuests, AddPresetToPriority, IsPresetAvailable, IsQuestInPriority, GetQuestPriorityIndex, HasAvailablePriorityQuests"; + reference22 = new ChangeEntry(EChangeCategory.Added, "IPC changes", list23); num4++; ref ChangeEntry reference23 = ref span2[num4]; num6 = 3; @@ -248,15 +248,13 @@ internal static class ChangelogData CollectionsMarshal.SetCount(list24, num6); span3 = CollectionsMarshal.AsSpan(list24); num5 = 0; - span3[num5] = "Added IPC for duty sync handling: GetDefaultDutyMode, SetDefaultDutyMode"; + span3[num5] = "Fixed line breaks not working in dialog strings"; num5++; - span3[num5] = "Added IPC for duty mode overrides: GetDutyModeOverride, SetDutyModeOverride"; + span3[num5] = "Fixed quest (Labor of Love)"; num5++; - span3[num5] = "Added IPC for clearing overrides: ClearDutyModeOverride, ClearAllDutyModeOverrides"; - reference23 = new ChangeEntry(EChangeCategory.Added, "IPC changes", list24); - num4++; - span2[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed quest (Constant Cravings)"); - reference21 = new ChangelogEntry("7.38.2", releaseDate7, list22); + span3[num5] = "Fixed quest (Sea of Sorrow)"; + reference23 = new ChangeEntry(EChangeCategory.Fixed, "Bug fixes", list24); + reference20 = new ChangelogEntry("7.38.3", releaseDate7, list21); num2++; ref ChangelogEntry reference24 = ref span[num2]; DateOnly releaseDate8 = new DateOnly(2025, 11, 18); @@ -266,13 +264,15 @@ internal static class ChangelogData span2 = CollectionsMarshal.AsSpan(list25); num3 = 0; ref ChangeEntry reference25 = ref span2[num3]; - num5 = 1; + num5 = 2; List list26 = new List(num5); CollectionsMarshal.SetCount(list26, num5); span3 = CollectionsMarshal.AsSpan(list26); num6 = 0; - span3[num6] = "Added new fields to quest schema"; - reference25 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list26); + span3[num6] = "Auto Duty unsync options for each duty (Duty Support, Unsync Solo, Unsync Party)"; + num6++; + span3[num6] = "Added Auto Duty unsync options to quest schema and updated quests using old unsync method"; + reference25 = new ChangeEntry(EChangeCategory.Added, "Major features", list26); num3++; ref ChangeEntry reference26 = ref span2[num3]; num6 = 3; @@ -280,390 +280,422 @@ internal static class ChangelogData CollectionsMarshal.SetCount(list27, num6); span3 = CollectionsMarshal.AsSpan(list27); num5 = 0; + span3[num5] = "Added IPC for duty sync handling: GetDefaultDutyMode, SetDefaultDutyMode"; + num5++; + span3[num5] = "Added IPC for duty mode overrides: GetDutyModeOverride, SetDutyModeOverride"; + num5++; + span3[num5] = "Added IPC for clearing overrides: ClearDutyModeOverride, ClearAllDutyModeOverrides"; + reference26 = new ChangeEntry(EChangeCategory.Added, "IPC changes", list27); + num3++; + span2[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed quest (Constant Cravings)"); + reference24 = new ChangelogEntry("7.38.2", releaseDate8, list25); + num2++; + ref ChangelogEntry reference27 = ref span[num2]; + DateOnly releaseDate9 = new DateOnly(2025, 11, 18); + num3 = 3; + List list28 = new List(num3); + CollectionsMarshal.SetCount(list28, num3); + span2 = CollectionsMarshal.AsSpan(list28); + num4 = 0; + ref ChangeEntry reference28 = ref span2[num4]; + num5 = 1; + List list29 = new List(num5); + CollectionsMarshal.SetCount(list29, num5); + span3 = CollectionsMarshal.AsSpan(list29); + num6 = 0; + span3[num6] = "Added new fields to quest schema"; + reference28 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list29); + num4++; + ref ChangeEntry reference29 = ref span2[num4]; + num6 = 3; + List list30 = new List(num6); + CollectionsMarshal.SetCount(list30, num6); + span3 = CollectionsMarshal.AsSpan(list30); + num5 = 0; span3[num5] = "A Faerie Tale Come True"; num5++; span3[num5] = "Constant Cravings"; num5++; span3[num5] = "A Bridge Too Full"; - reference26 = new ChangeEntry(EChangeCategory.QuestUpdates, "Added new quest paths", list27); - num3++; - ref ChangeEntry reference27 = ref span2[num3]; + reference29 = new ChangeEntry(EChangeCategory.QuestUpdates, "Added new quest paths", list30); + num4++; + ref ChangeEntry reference30 = ref span2[num4]; num5 = 3; - List list28 = new List(num5); - CollectionsMarshal.SetCount(list28, num5); - span3 = CollectionsMarshal.AsSpan(list28); + List list31 = new List(num5); + CollectionsMarshal.SetCount(list31, num5); + span3 = CollectionsMarshal.AsSpan(list31); num6 = 0; span3[num6] = "Fixed various quest schemas"; num6++; span3[num6] = "Fixed changelog bullet point encoding"; num6++; span3[num6] = "Fixed item use to wait until item is used before next action"; - reference27 = new ChangeEntry(EChangeCategory.Fixed, "Bug fixes", list28); - reference24 = new ChangelogEntry("7.38.1", releaseDate8, list25); + reference30 = new ChangeEntry(EChangeCategory.Fixed, "Bug fixes", list31); + reference27 = new ChangelogEntry("7.38.1", releaseDate9, list28); num2++; - ref ChangelogEntry reference28 = ref span[num2]; - DateOnly releaseDate9 = new DateOnly(2025, 11, 17); - num3 = 5; - List list29 = new List(num3); - CollectionsMarshal.SetCount(list29, num3); - span2 = CollectionsMarshal.AsSpan(list29); - num4 = 0; - ref ChangeEntry reference29 = ref span2[num4]; + ref ChangelogEntry reference31 = ref span[num2]; + DateOnly releaseDate10 = new DateOnly(2025, 11, 17); + num4 = 5; + List list32 = new List(num4); + CollectionsMarshal.SetCount(list32, num4); + span2 = CollectionsMarshal.AsSpan(list32); + num3 = 0; + ref ChangeEntry reference32 = ref span2[num3]; num6 = 2; - List list30 = new List(num6); - CollectionsMarshal.SetCount(list30, num6); - span3 = CollectionsMarshal.AsSpan(list30); + List list33 = new List(num6); + CollectionsMarshal.SetCount(list33, num6); + span3 = CollectionsMarshal.AsSpan(list33); num5 = 0; span3[num5] = "Quest sequence window to show expected sequences in each quest (with quest searching)"; num5++; span3[num5] = "Changelog"; - reference29 = new ChangeEntry(EChangeCategory.Added, "Major features", list30); - num4++; - ref ChangeEntry reference30 = ref span2[num4]; + reference32 = new ChangeEntry(EChangeCategory.Added, "Major features", list33); + num3++; + ref ChangeEntry reference33 = ref span2[num3]; num5 = 2; - List list31 = new List(num5); - CollectionsMarshal.SetCount(list31, num5); - span3 = CollectionsMarshal.AsSpan(list31); + List list34 = new List(num5); + CollectionsMarshal.SetCount(list34, num5); + span3 = CollectionsMarshal.AsSpan(list34); num6 = 0; span3[num6] = "Updated quest schemas"; num6++; span3[num6] = "Added search bar to preferred mounts and capitalization to mirror game mount names"; - reference30 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list31); - num4++; - ref ChangeEntry reference31 = ref span2[num4]; + reference33 = new ChangeEntry(EChangeCategory.Changed, "Improvements", list34); + num3++; + ref ChangeEntry reference34 = ref span2[num3]; num6 = 3; - List list32 = new List(num6); - CollectionsMarshal.SetCount(list32, num6); - span3 = CollectionsMarshal.AsSpan(list32); + List list35 = new List(num6); + CollectionsMarshal.SetCount(list35, num6); + span3 = CollectionsMarshal.AsSpan(list35); num5 = 0; span3[num5] = "Renamed IsQuestCompleted → IsQuestComplete"; num5++; span3[num5] = "Renamed IsQuestAvailable → IsReadyToAcceptQuest"; num5++; span3[num5] = "Added GetCurrentTask IPC"; - reference31 = new ChangeEntry(EChangeCategory.Changed, "IPC changes", list32); - num4++; - span2[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added all Hildibrand quests"); - num4++; - span2[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed credits/cutscenes playback"); - reference28 = new ChangelogEntry("7.38.0", releaseDate9, list29); - num2++; - ref ChangelogEntry reference32 = ref span[num2]; - DateOnly releaseDate10 = new DateOnly(2025, 11, 8); - num4 = 1; - List list33 = new List(num4); - CollectionsMarshal.SetCount(list33, num4); - span2 = CollectionsMarshal.AsSpan(list33); - num3 = 0; - span2[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added Fall Guys quest (Just Crowning Around)"); - reference32 = new ChangelogEntry("6.38", releaseDate10, list33); - num2++; - ref ChangelogEntry reference33 = ref span[num2]; - DateOnly releaseDate11 = new DateOnly(2025, 11, 8); - num3 = 1; - List list34 = new List(num3); - CollectionsMarshal.SetCount(list34, num3); - span2 = CollectionsMarshal.AsSpan(list34); - num4 = 0; - span2[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added Cosmic Exploration and various unlock quests"); - reference33 = new ChangelogEntry("6.37", releaseDate11, list34); - num2++; - ref ChangelogEntry reference34 = ref span[num2]; - DateOnly releaseDate12 = new DateOnly(2025, 11, 2); - num4 = 1; - List list35 = new List(num4); - CollectionsMarshal.SetCount(list35, num4); - span2 = CollectionsMarshal.AsSpan(list35); - num3 = 0; - span2[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy Rank 6 quest (With High Spirits)"); - reference34 = new ChangelogEntry("6.36", releaseDate12, list35); + reference34 = new ChangeEntry(EChangeCategory.Changed, "IPC changes", list35); + num3++; + span2[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added all Hildibrand quests"); + num3++; + span2[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed credits/cutscenes playback"); + reference31 = new ChangelogEntry("7.38.0", releaseDate10, list32); num2++; ref ChangelogEntry reference35 = ref span[num2]; - DateOnly releaseDate13 = new DateOnly(2025, 10, 28); + DateOnly releaseDate11 = new DateOnly(2025, 11, 8); num3 = 1; List list36 = new List(num3); CollectionsMarshal.SetCount(list36, num3); span2 = CollectionsMarshal.AsSpan(list36); num4 = 0; - span2[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed level 3 MSQ handling if character started on non-XP buff world"); - reference35 = new ChangelogEntry("6.35", releaseDate13, list36); + span2[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added Fall Guys quest (Just Crowning Around)"); + reference35 = new ChangelogEntry("6.38", releaseDate11, list36); num2++; ref ChangelogEntry reference36 = ref span[num2]; - DateOnly releaseDate14 = new DateOnly(2025, 10, 23); - num4 = 2; + DateOnly releaseDate12 = new DateOnly(2025, 11, 8); + num4 = 1; List list37 = new List(num4); CollectionsMarshal.SetCount(list37, num4); span2 = CollectionsMarshal.AsSpan(list37); num3 = 0; - span2[num3] = new ChangeEntry(EChangeCategory.Added, "Added clear priority quests on logout and on completion config settings"); - num3++; - span2[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed priority quest importing to respect import order"); - reference36 = new ChangelogEntry("6.34", releaseDate14, list37); + span2[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added Cosmic Exploration and various unlock quests"); + reference36 = new ChangelogEntry("6.37", releaseDate12, list37); num2++; ref ChangelogEntry reference37 = ref span[num2]; - DateOnly releaseDate15 = new DateOnly(2025, 10, 23); + DateOnly releaseDate13 = new DateOnly(2025, 11, 2); num3 = 1; List list38 = new List(num3); CollectionsMarshal.SetCount(list38, num3); span2 = CollectionsMarshal.AsSpan(list38); num4 = 0; - span2[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed RSR combat module"); - reference37 = new ChangelogEntry("6.33", releaseDate15, list38); + span2[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy Rank 6 quest (With High Spirits)"); + reference37 = new ChangelogEntry("6.36", releaseDate13, list38); num2++; ref ChangelogEntry reference38 = ref span[num2]; - DateOnly releaseDate16 = new DateOnly(2025, 10, 23); + DateOnly releaseDate14 = new DateOnly(2025, 10, 28); num4 = 1; List list39 = new List(num4); CollectionsMarshal.SetCount(list39, num4); span2 = CollectionsMarshal.AsSpan(list39); num3 = 0; - span2[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy Rank 5 quest (Forged in Corn)"); - reference38 = new ChangelogEntry("6.32", releaseDate16, list39); + span2[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed level 3 MSQ handling if character started on non-XP buff world"); + reference38 = new ChangelogEntry("6.35", releaseDate14, list39); num2++; ref ChangelogEntry reference39 = ref span[num2]; - DateOnly releaseDate17 = new DateOnly(2025, 10, 21); - num3 = 1; + DateOnly releaseDate15 = new DateOnly(2025, 10, 23); + num3 = 2; List list40 = new List(num3); CollectionsMarshal.SetCount(list40, num3); span2 = CollectionsMarshal.AsSpan(list40); num4 = 0; - span2[num4] = new ChangeEntry(EChangeCategory.Changed, "Added checks for moogle and allied society quests when using add all available quests"); - reference39 = new ChangelogEntry("6.31", releaseDate17, list40); + span2[num4] = new ChangeEntry(EChangeCategory.Added, "Added clear priority quests on logout and on completion config settings"); + num4++; + span2[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed priority quest importing to respect import order"); + reference39 = new ChangelogEntry("6.34", releaseDate15, list40); num2++; ref ChangelogEntry reference40 = ref span[num2]; - DateOnly releaseDate18 = new DateOnly(2025, 10, 21); + DateOnly releaseDate16 = new DateOnly(2025, 10, 23); num4 = 1; List list41 = new List(num4); CollectionsMarshal.SetCount(list41, num4); span2 = CollectionsMarshal.AsSpan(list41); num3 = 0; - span2[num3] = new ChangeEntry(EChangeCategory.Added, "Added button to journal that allows adding all available quests to priority"); - reference40 = new ChangelogEntry("6.30", releaseDate18, list41); + span2[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed RSR combat module"); + reference40 = new ChangelogEntry("6.33", releaseDate16, list41); num2++; ref ChangelogEntry reference41 = ref span[num2]; - DateOnly releaseDate19 = new DateOnly(2025, 10, 20); - num3 = 2; + DateOnly releaseDate17 = new DateOnly(2025, 10, 23); + num3 = 1; List list42 = new List(num3); CollectionsMarshal.SetCount(list42, num3); span2 = CollectionsMarshal.AsSpan(list42); num4 = 0; - ref ChangeEntry reference42 = ref span2[num4]; + span2[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy Rank 5 quest (Forged in Corn)"); + reference41 = new ChangelogEntry("6.32", releaseDate17, list42); + num2++; + ref ChangelogEntry reference42 = ref span[num2]; + DateOnly releaseDate18 = new DateOnly(2025, 10, 21); + num4 = 1; + List list43 = new List(num4); + CollectionsMarshal.SetCount(list43, num4); + span2 = CollectionsMarshal.AsSpan(list43); + num3 = 0; + span2[num3] = new ChangeEntry(EChangeCategory.Changed, "Added checks for moogle and allied society quests when using add all available quests"); + reference42 = new ChangelogEntry("6.31", releaseDate18, list43); + num2++; + ref ChangelogEntry reference43 = ref span[num2]; + DateOnly releaseDate19 = new DateOnly(2025, 10, 21); + num3 = 1; + List list44 = new List(num3); + CollectionsMarshal.SetCount(list44, num3); + span2 = CollectionsMarshal.AsSpan(list44); + num4 = 0; + span2[num4] = new ChangeEntry(EChangeCategory.Added, "Added button to journal that allows adding all available quests to priority"); + reference43 = new ChangelogEntry("6.30", releaseDate19, list44); + num2++; + ref ChangelogEntry reference44 = ref span[num2]; + DateOnly releaseDate20 = new DateOnly(2025, 10, 20); + num4 = 2; + List list45 = new List(num4); + CollectionsMarshal.SetCount(list45, num4); + span2 = CollectionsMarshal.AsSpan(list45); + num3 = 0; + ref ChangeEntry reference45 = ref span2[num3]; num5 = 2; - List list43 = new List(num5); - CollectionsMarshal.SetCount(list43, num5); - span3 = CollectionsMarshal.AsSpan(list43); + List list46 = new List(num5); + CollectionsMarshal.SetCount(list46, num5); + span3 = CollectionsMarshal.AsSpan(list46); num6 = 0; span3[num6] = "Added item count to combat handling rework"; num6++; span3[num6] = "Updated Pandora conflicting features"; - reference42 = new ChangeEntry(EChangeCategory.Changed, "Combat handling improvements", list43); - num4++; - span2[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed quest to purchase Gysahl Greens if not in inventory"); - reference41 = new ChangelogEntry("6.29", releaseDate19, list42); - num2++; - ref ChangelogEntry reference43 = ref span[num2]; - DateOnly releaseDate20 = new DateOnly(2025, 10, 19); - num4 = 1; - List list44 = new List(num4); - CollectionsMarshal.SetCount(list44, num4); - span2 = CollectionsMarshal.AsSpan(list44); - num3 = 0; - span2[num3] = new ChangeEntry(EChangeCategory.Changed, "Reworked kill count combat handling - combat and enemy kills are now processed instantly"); - reference43 = new ChangelogEntry("6.28", releaseDate20, list44); - num2++; - ref ChangelogEntry reference44 = ref span[num2]; - DateOnly releaseDate21 = new DateOnly(2025, 10, 18); - num3 = 2; - List list45 = new List(num3); - CollectionsMarshal.SetCount(list45, num3); - span2 = CollectionsMarshal.AsSpan(list45); - num4 = 0; - span2[num4] = new ChangeEntry(EChangeCategory.Changed, "Improved Aether Current checking logic"); - num4++; - span2[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed Chocobo Taxi Stand CheckSkip error and Patch 7.3 Fantasia unlock quest date/time"); - reference44 = new ChangelogEntry("6.27", releaseDate21, list45); - num2++; - ref ChangelogEntry reference45 = ref span[num2]; - DateOnly releaseDate22 = new DateOnly(2025, 10, 18); - num4 = 1; - List list46 = new List(num4); - CollectionsMarshal.SetCount(list46, num4); - span2 = CollectionsMarshal.AsSpan(list46); - num3 = 0; - span2[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 4 quests"); - reference45 = new ChangelogEntry("6.26", releaseDate22, list46); + reference45 = new ChangeEntry(EChangeCategory.Changed, "Combat handling improvements", list46); + num3++; + span2[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed quest to purchase Gysahl Greens if not in inventory"); + reference44 = new ChangelogEntry("6.29", releaseDate20, list45); num2++; ref ChangelogEntry reference46 = ref span[num2]; - DateOnly releaseDate23 = new DateOnly(2025, 10, 17); + DateOnly releaseDate21 = new DateOnly(2025, 10, 19); num3 = 1; List list47 = new List(num3); CollectionsMarshal.SetCount(list47, num3); span2 = CollectionsMarshal.AsSpan(list47); num4 = 0; - span2[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added All Saints' Wake 2025 quests and 7.35 Yok Huy rank 4 quests"); - reference46 = new ChangelogEntry("6.25", releaseDate23, list47); + span2[num4] = new ChangeEntry(EChangeCategory.Changed, "Reworked kill count combat handling - combat and enemy kills are now processed instantly"); + reference46 = new ChangelogEntry("6.28", releaseDate21, list47); num2++; ref ChangelogEntry reference47 = ref span[num2]; - DateOnly releaseDate24 = new DateOnly(2025, 10, 16); - num4 = 1; + DateOnly releaseDate22 = new DateOnly(2025, 10, 18); + num4 = 2; List list48 = new List(num4); CollectionsMarshal.SetCount(list48, num4); span2 = CollectionsMarshal.AsSpan(list48); num3 = 0; - span2[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 4 quests and Deep Dungeon quest"); - reference47 = new ChangelogEntry("6.24", releaseDate24, list48); + span2[num3] = new ChangeEntry(EChangeCategory.Changed, "Improved Aether Current checking logic"); + num3++; + span2[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed Chocobo Taxi Stand CheckSkip error and Patch 7.3 Fantasia unlock quest date/time"); + reference47 = new ChangelogEntry("6.27", releaseDate22, list48); num2++; ref ChangelogEntry reference48 = ref span[num2]; - DateOnly releaseDate25 = new DateOnly(2025, 10, 13); + DateOnly releaseDate23 = new DateOnly(2025, 10, 18); num3 = 1; List list49 = new List(num3); CollectionsMarshal.SetCount(list49, num3); span2 = CollectionsMarshal.AsSpan(list49); num4 = 0; - span2[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 3 quest (Larder Logistics)"); - reference48 = new ChangelogEntry("6.23", releaseDate25, list49); + span2[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 4 quests"); + reference48 = new ChangelogEntry("6.26", releaseDate23, list49); num2++; ref ChangelogEntry reference49 = ref span[num2]; - DateOnly releaseDate26 = new DateOnly(2025, 10, 12); - num4 = 3; + DateOnly releaseDate24 = new DateOnly(2025, 10, 17); + num4 = 1; List list50 = new List(num4); CollectionsMarshal.SetCount(list50, num4); span2 = CollectionsMarshal.AsSpan(list50); num3 = 0; - span2[num3] = new ChangeEntry(EChangeCategory.Changed, "Prevent disabled or Locked quests from being started as 'Start as next quest'"); - num3++; - span2[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 3 quests"); - num3++; - span2[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed Yok Huy quest and journal quest chain priority issues"); - reference49 = new ChangelogEntry("6.22", releaseDate26, list50); + span2[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added All Saints' Wake 2025 quests and 7.35 Yok Huy rank 4 quests"); + reference49 = new ChangelogEntry("6.25", releaseDate24, list50); num2++; ref ChangelogEntry reference50 = ref span[num2]; - DateOnly releaseDate27 = new DateOnly(2025, 10, 12); - num3 = 2; + DateOnly releaseDate25 = new DateOnly(2025, 10, 16); + num3 = 1; List list51 = new List(num3); CollectionsMarshal.SetCount(list51, num3); span2 = CollectionsMarshal.AsSpan(list51); num4 = 0; - span2[num4] = new ChangeEntry(EChangeCategory.Added, "Added expansion abbreviation to journal window"); - num4++; - span2[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 3 quests"); - reference50 = new ChangelogEntry("6.21", releaseDate27, list51); + span2[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 4 quests and Deep Dungeon quest"); + reference50 = new ChangelogEntry("6.24", releaseDate25, list51); num2++; ref ChangelogEntry reference51 = ref span[num2]; - DateOnly releaseDate28 = new DateOnly(2025, 10, 10); - num4 = 2; + DateOnly releaseDate26 = new DateOnly(2025, 10, 13); + num4 = 1; List list52 = new List(num4); CollectionsMarshal.SetCount(list52, num4); span2 = CollectionsMarshal.AsSpan(list52); num3 = 0; - span2[num3] = new ChangeEntry(EChangeCategory.Changed, "Allow completed repeatable quests to be used with 'Add quest and requirements to priority' feature"); - num3++; - span2[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 1 quest (A Work of Cart)"); - reference51 = new ChangelogEntry("6.20", releaseDate28, list52); + span2[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 3 quest (Larder Logistics)"); + reference51 = new ChangelogEntry("6.23", releaseDate26, list52); num2++; ref ChangelogEntry reference52 = ref span[num2]; - DateOnly releaseDate29 = new DateOnly(2025, 10, 9); + DateOnly releaseDate27 = new DateOnly(2025, 10, 12); num3 = 3; List list53 = new List(num3); CollectionsMarshal.SetCount(list53, num3); span2 = CollectionsMarshal.AsSpan(list53); num4 = 0; - span2[num4] = new ChangeEntry(EChangeCategory.Added, "Added config to batch Allied Society quest turn-ins"); + span2[num4] = new ChangeEntry(EChangeCategory.Changed, "Prevent disabled or Locked quests from being started as 'Start as next quest'"); num4++; - span2[num4] = new ChangeEntry(EChangeCategory.Changed, "Repeatable quests now show correct availability state in journal"); + span2[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 3 quests"); num4++; - span2[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 2 quests"); - reference52 = new ChangelogEntry("6.19", releaseDate29, list53); + span2[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed Yok Huy quest and journal quest chain priority issues"); + reference52 = new ChangelogEntry("6.22", releaseDate27, list53); num2++; ref ChangelogEntry reference53 = ref span[num2]; - DateOnly releaseDate30 = new DateOnly(2025, 10, 9); + DateOnly releaseDate28 = new DateOnly(2025, 10, 12); num4 = 2; List list54 = new List(num4); CollectionsMarshal.SetCount(list54, num4); span2 = CollectionsMarshal.AsSpan(list54); num3 = 0; - span2[num3] = new ChangeEntry(EChangeCategory.Changed, "Show once completed quests with improved state display"); + span2[num3] = new ChangeEntry(EChangeCategory.Added, "Added expansion abbreviation to journal window"); num3++; - span2[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy daily quest and improvements to various Yok Huy quests"); - reference53 = new ChangelogEntry("6.18", releaseDate30, list54); + span2[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 3 quests"); + reference53 = new ChangelogEntry("6.21", releaseDate28, list54); num2++; ref ChangelogEntry reference54 = ref span[num2]; - DateOnly releaseDate31 = new DateOnly(2025, 10, 8); - num3 = 1; + DateOnly releaseDate29 = new DateOnly(2025, 10, 10); + num3 = 2; List list55 = new List(num3); CollectionsMarshal.SetCount(list55, num3); span2 = CollectionsMarshal.AsSpan(list55); num4 = 0; - span2[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 1 and rank 2 quests"); - reference54 = new ChangelogEntry("6.17", releaseDate31, list55); + span2[num4] = new ChangeEntry(EChangeCategory.Changed, "Allow completed repeatable quests to be used with 'Add quest and requirements to priority' feature"); + num4++; + span2[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 1 quest (A Work of Cart)"); + reference54 = new ChangelogEntry("6.20", releaseDate29, list55); num2++; ref ChangelogEntry reference55 = ref span[num2]; - DateOnly releaseDate32 = new DateOnly(2025, 10, 8); - num4 = 1; + DateOnly releaseDate30 = new DateOnly(2025, 10, 9); + num4 = 3; List list56 = new List(num4); CollectionsMarshal.SetCount(list56, num4); span2 = CollectionsMarshal.AsSpan(list56); num3 = 0; - span2[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Deep Dungeon quest (Faerie Tale)"); - reference55 = new ChangelogEntry("6.16", releaseDate32, list56); + span2[num3] = new ChangeEntry(EChangeCategory.Added, "Added config to batch Allied Society quest turn-ins"); + num3++; + span2[num3] = new ChangeEntry(EChangeCategory.Changed, "Repeatable quests now show correct availability state in journal"); + num3++; + span2[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 2 quests"); + reference55 = new ChangelogEntry("6.19", releaseDate30, list56); num2++; ref ChangelogEntry reference56 = ref span[num2]; - DateOnly releaseDate33 = new DateOnly(2025, 10, 8); + DateOnly releaseDate31 = new DateOnly(2025, 10, 9); num3 = 2; List list57 = new List(num3); CollectionsMarshal.SetCount(list57, num3); span2 = CollectionsMarshal.AsSpan(list57); num4 = 0; - span2[num4] = new ChangeEntry(EChangeCategory.Changed, "Dalamud cleanup"); + span2[num4] = new ChangeEntry(EChangeCategory.Changed, "Show once completed quests with improved state display"); num4++; - span2[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed quest level requirement check log spam"); - reference56 = new ChangelogEntry("6.15", releaseDate33, list57); + span2[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy daily quest and improvements to various Yok Huy quests"); + reference56 = new ChangelogEntry("6.18", releaseDate31, list57); num2++; ref ChangelogEntry reference57 = ref span[num2]; - DateOnly releaseDate34 = new DateOnly(2025, 10, 8); + DateOnly releaseDate32 = new DateOnly(2025, 10, 8); num4 = 1; List list58 = new List(num4); CollectionsMarshal.SetCount(list58, num4); span2 = CollectionsMarshal.AsSpan(list58); num3 = 0; - span2[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed abandoned quest check logic if quest were MSQ"); - reference57 = new ChangelogEntry("6.14", releaseDate34, list58); + span2[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Yok Huy rank 1 and rank 2 quests"); + reference57 = new ChangelogEntry("6.17", releaseDate32, list58); num2++; ref ChangelogEntry reference58 = ref span[num2]; - DateOnly releaseDate35 = new DateOnly(2025, 10, 8); - num3 = 2; + DateOnly releaseDate33 = new DateOnly(2025, 10, 8); + num3 = 1; List list59 = new List(num3); CollectionsMarshal.SetCount(list59, num3); span2 = CollectionsMarshal.AsSpan(list59); num4 = 0; - ref ChangeEntry reference59 = ref span2[num4]; + span2[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Deep Dungeon quest (Faerie Tale)"); + reference58 = new ChangelogEntry("6.16", releaseDate33, list59); + num2++; + ref ChangelogEntry reference59 = ref span[num2]; + DateOnly releaseDate34 = new DateOnly(2025, 10, 8); + num4 = 2; + List list60 = new List(num4); + CollectionsMarshal.SetCount(list60, num4); + span2 = CollectionsMarshal.AsSpan(list60); + num3 = 0; + span2[num3] = new ChangeEntry(EChangeCategory.Changed, "Dalamud cleanup"); + num3++; + span2[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed quest level requirement check log spam"); + reference59 = new ChangelogEntry("6.15", releaseDate34, list60); + num2++; + ref ChangelogEntry reference60 = ref span[num2]; + DateOnly releaseDate35 = new DateOnly(2025, 10, 8); + num3 = 1; + List list61 = new List(num3); + CollectionsMarshal.SetCount(list61, num3); + span2 = CollectionsMarshal.AsSpan(list61); + num4 = 0; + span2[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed abandoned quest check logic if quest were MSQ"); + reference60 = new ChangelogEntry("6.14", releaseDate35, list61); + num2++; + ref ChangelogEntry reference61 = ref span[num2]; + DateOnly releaseDate36 = new DateOnly(2025, 10, 8); + num4 = 2; + List list62 = new List(num4); + CollectionsMarshal.SetCount(list62, num4); + span2 = CollectionsMarshal.AsSpan(list62); + num3 = 0; + ref ChangeEntry reference62 = ref span2[num3]; num6 = 3; - List list60 = new List(num6); - CollectionsMarshal.SetCount(list60, num6); - span3 = CollectionsMarshal.AsSpan(list60); + List list63 = new List(num6); + CollectionsMarshal.SetCount(list63, num6); + span3 = CollectionsMarshal.AsSpan(list63); num5 = 0; span3[num5] = "Context menu option to add required quests and their chain to priority list"; num5++; span3[num5] = "AetheryteShortcut to multiple quests"; num5++; span3[num5] = "Artisan as a recommended plugin/dependency"; - reference59 = new ChangeEntry(EChangeCategory.Added, "Quest improvements", list60); - num4++; - span2[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed abandoned quest check and priority list issues"); - reference58 = new ChangelogEntry("6.13", releaseDate35, list59); + reference62 = new ChangeEntry(EChangeCategory.Added, "Quest improvements", list63); + num3++; + span2[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed abandoned quest check and priority list issues"); + reference61 = new ChangelogEntry("6.13", releaseDate36, list62); num2++; - ref ChangelogEntry reference60 = ref span[num2]; - DateOnly releaseDate36 = new DateOnly(2025, 10, 7); - num4 = 4; - List list61 = new List(num4); - CollectionsMarshal.SetCount(list61, num4); - span2 = CollectionsMarshal.AsSpan(list61); - num3 = 0; - ref ChangeEntry reference61 = ref span2[num3]; + ref ChangelogEntry reference63 = ref span[num2]; + DateOnly releaseDate37 = new DateOnly(2025, 10, 7); + num3 = 4; + List list64 = new List(num3); + CollectionsMarshal.SetCount(list64, num3); + span2 = CollectionsMarshal.AsSpan(list64); + num4 = 0; + ref ChangeEntry reference64 = ref span2[num4]; num5 = 4; - List list62 = new List(num5); - CollectionsMarshal.SetCount(list62, num5); - span3 = CollectionsMarshal.AsSpan(list62); + List list65 = new List(num5); + CollectionsMarshal.SetCount(list65, num5); + span3 = CollectionsMarshal.AsSpan(list65); num6 = 0; span3[num6] = "FATE combat handling with auto level syncing"; num6++; @@ -672,67 +704,67 @@ internal static class ChangelogData span3[num6] = "Update quest tracking when quests are hidden or prioritised in game"; num6++; span3[num6] = "QuestMap as a recommended plugin/dependency"; - reference61 = new ChangeEntry(EChangeCategory.Added, "FATE and quest tracking", list62); - num3++; - ref ChangeEntry reference62 = ref span2[num3]; + reference64 = new ChangeEntry(EChangeCategory.Added, "FATE and quest tracking", list65); + num4++; + ref ChangeEntry reference65 = ref span2[num4]; num6 = 3; - List list63 = new List(num6); - CollectionsMarshal.SetCount(list63, num6); - span3 = CollectionsMarshal.AsSpan(list63); + List list66 = new List(num6); + CollectionsMarshal.SetCount(list66, num6); + span3 = CollectionsMarshal.AsSpan(list66); num5 = 0; span3[num5] = "Always prioritise next quest during teleportation/zone transitions"; num5++; span3[num5] = "Improved accepted quest logic with abandoned quest detection"; num5++; span3[num5] = "Show quests without quest paths as Locked"; - reference62 = new ChangeEntry(EChangeCategory.Changed, "Quest prioritisation improvements", list63); - num3++; - span2[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Deep Dungeon, Hildibrand, Yok Huy, Monster Hunter Wilds Collab, and Doman Enclave quests"); - num3++; - span2[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed accepted/active quest display and Hildibrand quest issues"); - reference60 = new ChangelogEntry("6.12", releaseDate36, list61); + reference65 = new ChangeEntry(EChangeCategory.Changed, "Quest prioritisation improvements", list66); + num4++; + span2[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.35 Deep Dungeon, Hildibrand, Yok Huy, Monster Hunter Wilds Collab, and Doman Enclave quests"); + num4++; + span2[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed accepted/active quest display and Hildibrand quest issues"); + reference63 = new ChangelogEntry("6.12", releaseDate37, list64); num2++; - ref ChangelogEntry reference63 = ref span[num2]; - DateOnly releaseDate37 = new DateOnly(2025, 10, 3); - num3 = 1; - List list64 = new List(num3); - CollectionsMarshal.SetCount(list64, num3); - span2 = CollectionsMarshal.AsSpan(list64); - num4 = 0; - span2[num4] = new ChangeEntry(EChangeCategory.Changed, "Added remaining checks for quest priority to prevent infinite teleport looping"); - reference63 = new ChangelogEntry("6.11", releaseDate37, list64); - num2++; - ref ChangelogEntry reference64 = ref span[num2]; - DateOnly releaseDate38 = new DateOnly(2025, 10, 2); + ref ChangelogEntry reference66 = ref span[num2]; + DateOnly releaseDate38 = new DateOnly(2025, 10, 3); num4 = 1; - List list65 = new List(num4); - CollectionsMarshal.SetCount(list65, num4); - span2 = CollectionsMarshal.AsSpan(list65); + List list67 = new List(num4); + CollectionsMarshal.SetCount(list67, num4); + span2 = CollectionsMarshal.AsSpan(list67); num3 = 0; - ref ChangeEntry reference65 = ref span2[num3]; + span2[num3] = new ChangeEntry(EChangeCategory.Changed, "Added remaining checks for quest priority to prevent infinite teleport looping"); + reference66 = new ChangelogEntry("6.11", releaseDate38, list67); + num2++; + ref ChangelogEntry reference67 = ref span[num2]; + DateOnly releaseDate39 = new DateOnly(2025, 10, 2); + num3 = 1; + List list68 = new List(num3); + CollectionsMarshal.SetCount(list68, num3); + span2 = CollectionsMarshal.AsSpan(list68); + num4 = 0; + ref ChangeEntry reference68 = ref span2[num4]; num5 = 2; - List list66 = new List(num5); - CollectionsMarshal.SetCount(list66, num5); - span3 = CollectionsMarshal.AsSpan(list66); + List list69 = new List(num5); + CollectionsMarshal.SetCount(list69, num5); + span3 = CollectionsMarshal.AsSpan(list69); num6 = 0; span3[num6] = "Don't show quests as available if player doesn't meet level requirements"; num6++; span3[num6] = "Updated 'required for MSQ' text in Crystal Tower quest preset window"; - reference65 = new ChangeEntry(EChangeCategory.Changed, "Quest window improvements", list66); - reference64 = new ChangelogEntry("6.10", releaseDate38, list65); + reference68 = new ChangeEntry(EChangeCategory.Changed, "Quest window improvements", list69); + reference67 = new ChangelogEntry("6.10", releaseDate39, list68); num2++; - ref ChangelogEntry reference66 = ref span[num2]; - DateOnly releaseDate39 = new DateOnly(2025, 9, 21); - num3 = 5; - List list67 = new List(num3); - CollectionsMarshal.SetCount(list67, num3); - span2 = CollectionsMarshal.AsSpan(list67); - num4 = 0; - ref ChangeEntry reference67 = ref span2[num4]; + ref ChangelogEntry reference69 = ref span[num2]; + DateOnly releaseDate40 = new DateOnly(2025, 9, 21); + num4 = 5; + List list70 = new List(num4); + CollectionsMarshal.SetCount(list70, num4); + span2 = CollectionsMarshal.AsSpan(list70); + num3 = 0; + ref ChangeEntry reference70 = ref span2[num3]; num6 = 4; - List list68 = new List(num6); - CollectionsMarshal.SetCount(list68, num6); - span3 = CollectionsMarshal.AsSpan(list68); + List list71 = new List(num6); + CollectionsMarshal.SetCount(list71, num6); + span3 = CollectionsMarshal.AsSpan(list71); num5 = 0; span3[num5] = "Reworked event quest handling - automatically displays when events are active"; num5++; @@ -741,13 +773,13 @@ internal static class ChangelogData span3[num5] = "Reworked Priority Quests tab (Manual Priority and Quest Presets)"; num5++; span3[num5] = "Quest path viewer site (https://wigglymuffin.github.io/FFXIV-Tools/)"; - reference67 = new ChangeEntry(EChangeCategory.Added, "Major system reworks", list68); - num4++; - ref ChangeEntry reference68 = ref span2[num4]; + reference70 = new ChangeEntry(EChangeCategory.Added, "Major system reworks", list71); + num3++; + ref ChangeEntry reference71 = ref span2[num3]; num5 = 4; - List list69 = new List(num5); - CollectionsMarshal.SetCount(list69, num5); - span3 = CollectionsMarshal.AsSpan(list69); + List list72 = new List(num5); + CollectionsMarshal.SetCount(list72, num5); + span3 = CollectionsMarshal.AsSpan(list72); num6 = 0; span3[num6] = "Questionable.IsQuestCompleted"; num6++; @@ -756,13 +788,13 @@ internal static class ChangelogData span3[num6] = "Questionable.IsQuestAccepted"; num6++; span3[num6] = "Questionable.IsQuestUnobtainable"; - reference68 = new ChangeEntry(EChangeCategory.Added, "New IPC commands", list69); - num4++; - ref ChangeEntry reference69 = ref span2[num4]; + reference71 = new ChangeEntry(EChangeCategory.Added, "New IPC commands", list72); + num3++; + ref ChangeEntry reference72 = ref span2[num3]; num6 = 5; - List list70 = new List(num6); - CollectionsMarshal.SetCount(list70, num6); - span3 = CollectionsMarshal.AsSpan(list70); + List list73 = new List(num6); + CollectionsMarshal.SetCount(list73, num6); + span3 = CollectionsMarshal.AsSpan(list73); num5 = 0; span3[num5] = "Improved JSON quest validation with specific error reasons"; num5++; @@ -773,25 +805,25 @@ internal static class ChangelogData span3[num5] = "Improved DialogueChoices regex matching"; num5++; span3[num5] = "Improved refresh checker for all quest states"; - reference69 = new ChangeEntry(EChangeCategory.Changed, "Various improvements", list70); - num4++; - span2[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.31 Occult Crescent quests"); - num4++; - span2[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed cutscene crashes, Single Player Duty triggers, and various quest issues"); - reference66 = new ChangelogEntry("6.9", releaseDate39, list67); + reference72 = new ChangeEntry(EChangeCategory.Changed, "Various improvements", list73); + num3++; + span2[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added 7.31 Occult Crescent quests"); + num3++; + span2[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed cutscene crashes, Single Player Duty triggers, and various quest issues"); + reference69 = new ChangelogEntry("6.9", releaseDate40, list70); num2++; - ref ChangelogEntry reference70 = ref span[num2]; - DateOnly releaseDate40 = new DateOnly(2025, 9, 2); - num4 = 4; - List list71 = new List(num4); - CollectionsMarshal.SetCount(list71, num4); - span2 = CollectionsMarshal.AsSpan(list71); - num3 = 0; - ref ChangeEntry reference71 = ref span2[num3]; + ref ChangelogEntry reference73 = ref span[num2]; + DateOnly releaseDate41 = new DateOnly(2025, 9, 2); + num3 = 4; + List list74 = new List(num3); + CollectionsMarshal.SetCount(list74, num3); + span2 = CollectionsMarshal.AsSpan(list74); + num4 = 0; + ref ChangeEntry reference74 = ref span2[num4]; num5 = 4; - List list72 = new List(num5); - CollectionsMarshal.SetCount(list72, num5); - span3 = CollectionsMarshal.AsSpan(list72); + List list75 = new List(num5); + CollectionsMarshal.SetCount(list75, num5); + span3 = CollectionsMarshal.AsSpan(list75); num6 = 0; span3[num6] = "Help commands and priority quest command"; num6++; @@ -800,72 +832,72 @@ internal static class ChangelogData span3[num6] = "Duty counts and controls in 'Quest Battles' tab"; num6++; span3[num6] = "'Refresh quest timer' setting (WIP)"; - reference71 = new ChangeEntry(EChangeCategory.Added, "Command and UI improvements", list72); - num3++; - span2[num3] = new ChangeEntry(EChangeCategory.Changed, "Improved 'Clear All' buttons to require CTRL being held"); - num3++; - span2[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added Zodiac quests and 7.31 Cosmic/Occult Crescent quests"); - num3++; - span2[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed Fishing for Friendship and Cosmic Exploration quests"); - reference70 = new ChangelogEntry("6.8", releaseDate40, list71); + reference74 = new ChangeEntry(EChangeCategory.Added, "Command and UI improvements", list75); + num4++; + span2[num4] = new ChangeEntry(EChangeCategory.Changed, "Improved 'Clear All' buttons to require CTRL being held"); + num4++; + span2[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added Zodiac quests and 7.31 Cosmic/Occult Crescent quests"); + num4++; + span2[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed Fishing for Friendship and Cosmic Exploration quests"); + reference73 = new ChangelogEntry("6.8", releaseDate41, list74); num2++; - ref ChangelogEntry reference72 = ref span[num2]; - DateOnly releaseDate41 = new DateOnly(2025, 8, 27); - num3 = 4; - List list73 = new List(num3); - CollectionsMarshal.SetCount(list73, num3); - span2 = CollectionsMarshal.AsSpan(list73); - num4 = 0; - ref ChangeEntry reference73 = ref span2[num4]; + ref ChangelogEntry reference75 = ref span[num2]; + DateOnly releaseDate42 = new DateOnly(2025, 8, 27); + num4 = 4; + List list76 = new List(num4); + CollectionsMarshal.SetCount(list76, num4); + span2 = CollectionsMarshal.AsSpan(list76); + num3 = 0; + ref ChangeEntry reference76 = ref span2[num3]; num6 = 2; - List list74 = new List(num6); - CollectionsMarshal.SetCount(list74, num6); - span3 = CollectionsMarshal.AsSpan(list74); + List list77 = new List(num6); + CollectionsMarshal.SetCount(list77, num6); + span3 = CollectionsMarshal.AsSpan(list77); num5 = 0; span3[num5] = "Icon to 'Clear All' button in stop conditions"; num5++; span3[num5] = "Duty counts and 'Enable All' button in 'Duties' tab"; - reference73 = new ChangeEntry(EChangeCategory.Added, "UI improvements", list74); - num4++; - span2[num4] = new ChangeEntry(EChangeCategory.Changed, "Renamed 'Clear' button to 'Clear All' in priority window"); - num4++; - span2[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added Rising 2025 Event Quests"); - num4++; - span2[num4] = new ChangeEntry(EChangeCategory.Fixed, "Fixed clipboard assigning blacklist to whitelist in 'Duties' tab"); - reference72 = new ChangelogEntry("6.7", releaseDate41, list73); + reference76 = new ChangeEntry(EChangeCategory.Added, "UI improvements", list77); + num3++; + span2[num3] = new ChangeEntry(EChangeCategory.Changed, "Renamed 'Clear' button to 'Clear All' in priority window"); + num3++; + span2[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added Rising 2025 Event Quests"); + num3++; + span2[num3] = new ChangeEntry(EChangeCategory.Fixed, "Fixed clipboard assigning blacklist to whitelist in 'Duties' tab"); + reference75 = new ChangelogEntry("6.7", releaseDate42, list76); num2++; - ref ChangelogEntry reference74 = ref span[num2]; - DateOnly releaseDate42 = new DateOnly(2025, 8, 25); - num4 = 2; - List list75 = new List(num4); - CollectionsMarshal.SetCount(list75, num4); - span2 = CollectionsMarshal.AsSpan(list75); - num3 = 0; - ref ChangeEntry reference75 = ref span2[num3]; + ref ChangelogEntry reference77 = ref span[num2]; + DateOnly releaseDate43 = new DateOnly(2025, 8, 25); + num3 = 2; + List list78 = new List(num3); + CollectionsMarshal.SetCount(list78, num3); + span2 = CollectionsMarshal.AsSpan(list78); + num4 = 0; + ref ChangeEntry reference78 = ref span2[num4]; num5 = 2; - List list76 = new List(num5); - CollectionsMarshal.SetCount(list76, num5); - span3 = CollectionsMarshal.AsSpan(list76); + List list79 = new List(num5); + CollectionsMarshal.SetCount(list79, num5); + span3 = CollectionsMarshal.AsSpan(list79); num6 = 0; span3[num6] = "Missing emotes to schema and emote handler"; num6++; span3[num6] = "Improved stop conditions with 'Clear All' button"; - reference75 = new ChangeEntry(EChangeCategory.Added, "Emote support and stop conditions", list76); - num3++; - span2[num3] = new ChangeEntry(EChangeCategory.Changed, "Stop at level functionality"); - reference74 = new ChangelogEntry("6.6", releaseDate42, list75); - num2++; - ref ChangelogEntry reference76 = ref span[num2]; - DateOnly releaseDate43 = new DateOnly(2025, 8, 25); - num3 = 2; - List list77 = new List(num3); - CollectionsMarshal.SetCount(list77, num3); - span2 = CollectionsMarshal.AsSpan(list77); - num4 = 0; - span2[num4] = new ChangeEntry(EChangeCategory.Fixed, "Potential fix to single/solo duties softlocking"); + reference78 = new ChangeEntry(EChangeCategory.Added, "Emote support and stop conditions", list79); num4++; - span2[num4] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added San d'Oria: The Second Walk and various side quests"); - reference76 = new ChangelogEntry("6.5", releaseDate43, list77); + span2[num4] = new ChangeEntry(EChangeCategory.Changed, "Stop at level functionality"); + reference77 = new ChangelogEntry("6.6", releaseDate43, list78); + num2++; + ref ChangelogEntry reference79 = ref span[num2]; + DateOnly releaseDate44 = new DateOnly(2025, 8, 25); + num4 = 2; + List list80 = new List(num4); + CollectionsMarshal.SetCount(list80, num4); + span2 = CollectionsMarshal.AsSpan(list80); + num3 = 0; + span2[num3] = new ChangeEntry(EChangeCategory.Fixed, "Potential fix to single/solo duties softlocking"); + num3++; + span2[num3] = new ChangeEntry(EChangeCategory.QuestUpdates, "Added San d'Oria: The Second Walk and various side quests"); + reference79 = new ChangelogEntry("6.5", releaseDate44, list80); Changelogs = list; } } diff --git a/Questionable/Questionable.External/NavmeshIpc.cs b/Questionable/Questionable.External/NavmeshIpc.cs index 0ed9d8a..0a21170 100644 --- a/Questionable/Questionable.External/NavmeshIpc.cs +++ b/Questionable/Questionable.External/NavmeshIpc.cs @@ -39,6 +39,10 @@ internal sealed class NavmeshIpc private readonly ICallGateSubscriber _buildProgress; + private readonly ICallGateSubscriber _navReload; + + private readonly ICallGateSubscriber _navRebuild; + public bool IsReady { get @@ -115,6 +119,8 @@ internal sealed class NavmeshIpc _queryPointOnFloor = pluginInterface.GetIpcSubscriber("vnavmesh.Query.Mesh.PointOnFloor"); _queryNearestPoint = pluginInterface.GetIpcSubscriber("vnavmesh.Query.Mesh.NearestPoint"); _buildProgress = pluginInterface.GetIpcSubscriber("vnavmesh.Nav.BuildProgress"); + _navReload = pluginInterface.GetIpcSubscriber("vnavmesh.Nav.Reload"); + _navRebuild = pluginInterface.GetIpcSubscriber("vnavmesh.Nav.Rebuild"); } public void Stop() @@ -129,6 +135,30 @@ internal sealed class NavmeshIpc } } + public void Reload() + { + try + { + _navReload.InvokeFunc(); + } + catch (IpcError exception) + { + _logger.LogWarning(exception, "Could not reload navmesh"); + } + } + + public void Rebuild() + { + try + { + _navRebuild.InvokeFunc(); + } + catch (IpcError exception) + { + _logger.LogWarning(exception, "Could not rebuild navmesh"); + } + } + public Task> Pathfind(Vector3 localPlayerPosition, Vector3 targetPosition, bool fly, CancellationToken cancellationToken) { try @@ -147,8 +177,8 @@ internal sealed class NavmeshIpc { try { - _pathSetTolerance.InvokeAction(0.25f); - return _pluginInterface.GetIpcSubscriber>>("vnavmesh.Nav.PathfindWithTolerance").InvokeFunc(localPlayerPosition, targetPosition, fly, tolerance, cancellationToken); + _pathSetTolerance.InvokeAction(tolerance); + return _pluginInterface.GetIpcSubscriber>>("vnavmesh.Nav.PathfindWithTolerance").InvokeFunc(localPlayerPosition, targetPosition, fly, tolerance); } catch (IpcError exception) { diff --git a/Questionable/Questionable.Functions/ChatFunctions.cs b/Questionable/Questionable.Functions/ChatFunctions.cs index 6211668..bd71d4a 100644 --- a/Questionable/Questionable.Functions/ChatFunctions.cs +++ b/Questionable/Questionable.Functions/ChatFunctions.cs @@ -11,6 +11,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Client.System.Memory; using FFXIVClientStructs.FFXIV.Client.System.String; +using FFXIVClientStructs.FFXIV.Client.UI; using Lumina.Excel.Sheets; using Microsoft.Extensions.Logging; using Questionable.Model.Questing; @@ -21,11 +22,6 @@ internal sealed class ChatFunctions { private delegate void ProcessChatBoxDelegate(nint uiModule, nint message, nint unused, byte a4); - private static class Signatures - { - internal const string SendChat = "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B F2 48 8B F9 45 84 C9"; - } - [StructLayout(LayoutKind.Explicit)] private readonly struct ChatPayload : IDisposable { @@ -72,7 +68,7 @@ internal sealed class ChatFunctions _gameFunctions = gameFunctions; _targetManager = targetManager; _logger = logger; - _processChatBox = Marshal.GetDelegateForFunctionPointer(sigScanner.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B F2 48 8B F9 45 84 C9")); + _processChatBox = Marshal.GetDelegateForFunctionPointer(UIModule.Addresses.ProcessChatBoxEntry.Value); _emoteCommands = (from x in dataManager.GetExcelSheet() where x.RowId != 0 where x.TextCommand.IsValid diff --git a/Questionable/Questionable.Functions/GameFunctions.cs b/Questionable/Questionable.Functions/GameFunctions.cs index 9258154..86a8e16 100644 --- a/Questionable/Questionable.Functions/GameFunctions.cs +++ b/Questionable/Questionable.Functions/GameFunctions.cs @@ -14,6 +14,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Control; +using FFXIVClientStructs.FFXIV.Client.Game.Event; using FFXIVClientStructs.FFXIV.Client.Game.Fate; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.UI; @@ -34,11 +35,6 @@ internal sealed class GameFunctions { private delegate void AbandonDutyDelegate(bool a1); - private static class Signatures - { - internal const string AbandonDuty = "E8 ?? ?? ?? ?? 41 B2 01 EB 39"; - } - private readonly QuestFunctions _questFunctions; private readonly IDataManager _dataManager; @@ -74,7 +70,7 @@ internal sealed class GameFunctions _gameGui = gameGui; _configuration = configuration; _logger = logger; - _abandonDuty = Marshal.GetDelegateForFunctionPointer(sigScanner.ScanText("E8 ?? ?? ?? ?? 41 B2 01 EB 39")); + _abandonDuty = Marshal.GetDelegateForFunctionPointer(EventFramework.Addresses.LeaveCurrentContent.Value); _territoryToAetherCurrentCompFlgSet = (from x in dataManager.GetExcelSheet() where x.RowId != 0 where x.AetherCurrentCompFlgSet.RowId != 0 diff --git a/Questionable/Questionable.Functions/GameSignatures.cs b/Questionable/Questionable.Functions/GameSignatures.cs new file mode 100644 index 0000000..19a9aa4 --- /dev/null +++ b/Questionable/Questionable.Functions/GameSignatures.cs @@ -0,0 +1,5 @@ +namespace Questionable.Functions; + +internal static class GameSignatures +{ +} diff --git a/Questionable/Questionable.Windows.ConfigComponents/GeneralConfigComponent.cs b/Questionable/Questionable.Windows.ConfigComponents/GeneralConfigComponent.cs index 1f8492b..fc771c7 100644 --- a/Questionable/Questionable.Windows.ConfigComponents/GeneralConfigComponent.cs +++ b/Questionable/Questionable.Windows.ConfigComponents/GeneralConfigComponent.cs @@ -183,22 +183,28 @@ internal sealed class GeneralConfigComponent : ConfigComponent base.Configuration.General.UseEscToCancelQuesting = v2; Save(); } - bool v3 = base.Configuration.General.ShowIncompleteSeasonalEvents; - if (ImGui.Checkbox("Show details for incomplete seasonal events", ref v3)) + bool v3 = base.Configuration.General.StopOnPlayerInput; + if (ImGui.Checkbox("Stop automation when manually moving character", ref v3)) { - base.Configuration.General.ShowIncompleteSeasonalEvents = v3; + base.Configuration.General.StopOnPlayerInput = v3; Save(); } - bool v4 = base.Configuration.General.HideSeasonalEventsFromJournalProgress; - if (ImGui.Checkbox("Hide Seasonal Events from Journal Progress", ref v4)) + bool v4 = base.Configuration.General.ShowIncompleteSeasonalEvents; + if (ImGui.Checkbox("Show details for incomplete seasonal events", ref v4)) { - base.Configuration.General.HideSeasonalEventsFromJournalProgress = v4; + base.Configuration.General.ShowIncompleteSeasonalEvents = v4; Save(); } - bool v5 = base.Configuration.General.ShowChangelogOnUpdate; - if (ImGui.Checkbox("Show changelog window when plugin updates", ref v5)) + bool v5 = base.Configuration.General.HideSeasonalEventsFromJournalProgress; + if (ImGui.Checkbox("Hide Seasonal Events from Journal Progress", ref v5)) { - base.Configuration.General.ShowChangelogOnUpdate = v5; + base.Configuration.General.HideSeasonalEventsFromJournalProgress = v5; + Save(); + } + bool v6 = base.Configuration.General.ShowChangelogOnUpdate; + if (ImGui.Checkbox("Show changelog window when plugin updates", ref v6)) + { + base.Configuration.General.ShowChangelogOnUpdate = v6; Save(); } } @@ -206,16 +212,16 @@ internal sealed class GeneralConfigComponent : ConfigComponent ImGui.Text("Questing"); using (ImRaii.PushIndent()) { - bool v6 = base.Configuration.General.ConfigureTextAdvance; - if (ImGui.Checkbox("Automatically configure TextAdvance with the recommended settings", ref v6)) + bool v7 = base.Configuration.General.ConfigureTextAdvance; + if (ImGui.Checkbox("Automatically configure TextAdvance with the recommended settings", ref v7)) { - base.Configuration.General.ConfigureTextAdvance = v6; + base.Configuration.General.ConfigureTextAdvance = v7; Save(); } - bool v7 = base.Configuration.General.SkipLowPriorityDuties; - if (ImGui.Checkbox("Unlock certain optional dungeons and raids (instead of waiting for completion)", ref v7)) + bool v8 = base.Configuration.General.SkipLowPriorityDuties; + if (ImGui.Checkbox("Unlock certain optional dungeons and raids (instead of waiting for completion)", ref v8)) { - base.Configuration.General.SkipLowPriorityDuties = v7; + base.Configuration.General.SkipLowPriorityDuties = v8; Save(); } ImGui.SameLine(); @@ -243,10 +249,10 @@ internal sealed class GeneralConfigComponent : ConfigComponent } } ImGui.Spacing(); - bool v8 = base.Configuration.General.AutoStepRefreshEnabled; - if (ImGui.Checkbox("Automatically refresh quest steps when stuck", ref v8)) + bool v9 = base.Configuration.General.AutoStepRefreshEnabled; + if (ImGui.Checkbox("Automatically refresh quest steps when stuck", ref v9)) { - base.Configuration.General.AutoStepRefreshEnabled = v8; + base.Configuration.General.AutoStepRefreshEnabled = v9; Save(); } ImGui.SameLine(); @@ -262,20 +268,20 @@ internal sealed class GeneralConfigComponent : ConfigComponent ImGui.Text("This helps resume automated quest completion when interruptions occur."); } } - using (ImRaii.Disabled(!v8)) + using (ImRaii.Disabled(!v9)) { ImGui.Indent(); - int v9 = base.Configuration.General.AutoStepRefreshDelaySeconds; + int v10 = base.Configuration.General.AutoStepRefreshDelaySeconds; ImGui.SetNextItemWidth(150f); - if (ImGui.SliderInt("Refresh delay (seconds)", ref v9, 10, 180)) + if (ImGui.SliderInt("Refresh delay (seconds)", ref v10, 10, 180)) { - base.Configuration.General.AutoStepRefreshDelaySeconds = v9; + base.Configuration.General.AutoStepRefreshDelaySeconds = v10; Save(); } Vector4 col = new Vector4(0.7f, 0.7f, 0.7f, 1f); ImU8String text2 = new ImU8String(77, 1); text2.AppendLiteral("Quest steps will refresh automatically after "); - text2.AppendFormatted(v9); + text2.AppendFormatted(v10); text2.AppendLiteral(" seconds if no progress is made."); ImGui.TextColored(in col, text2); ImGui.Unindent(); @@ -283,16 +289,16 @@ internal sealed class GeneralConfigComponent : ConfigComponent ImGui.Spacing(); ImGui.Separator(); ImGui.Text("Priority Quest Management"); - bool v10 = base.Configuration.General.ClearPriorityQuestsOnLogout; - if (ImGui.Checkbox("Clear priority quests on character logout", ref v10)) + bool v11 = base.Configuration.General.ClearPriorityQuestsOnLogout; + if (ImGui.Checkbox("Clear priority quests on character logout", ref v11)) { - base.Configuration.General.ClearPriorityQuestsOnLogout = v10; + base.Configuration.General.ClearPriorityQuestsOnLogout = v11; Save(); } - bool v11 = base.Configuration.General.ClearPriorityQuestsOnCompletion; - if (ImGui.Checkbox("Remove priority quests when completed", ref v11)) + bool v12 = base.Configuration.General.ClearPriorityQuestsOnCompletion; + if (ImGui.Checkbox("Remove priority quests when completed", ref v12)) { - base.Configuration.General.ClearPriorityQuestsOnCompletion = v11; + base.Configuration.General.ClearPriorityQuestsOnCompletion = v12; Save(); } ImGui.SameLine(); diff --git a/Questionable/Questionable.Windows.ConfigComponents/PluginConfigComponent.cs b/Questionable/Questionable.Windows.ConfigComponents/PluginConfigComponent.cs index 10ceffd..3a0e311 100644 --- a/Questionable/Questionable.Windows.ConfigComponents/PluginConfigComponent.cs +++ b/Questionable/Questionable.Windows.ConfigComponents/PluginConfigComponent.cs @@ -25,9 +25,9 @@ internal sealed class PluginConfigComponent : ConfigComponent private static readonly IReadOnlyList RequiredPlugins = new global::_003C_003Ez__ReadOnlyArray(new PluginInfo[3] { - new PluginInfo("vnavmesh", "vnavmesh", "vnavmesh handles the navigation within a zone, moving\nyour character to the next quest-related objective.", new Uri("https://github.com/awgil/ffxiv_navmesh/"), new Uri("https://puni.sh/api/repository/veyn")), new PluginInfo("Lifestream", "Lifestream", "Used to travel to aethernet shards in cities.", new Uri("https://github.com/NightmareXIV/Lifestream"), new Uri("https://github.com/NightmareXIV/MyDalamudPlugins/raw/main/pluginmaster.json")), - new PluginInfo("TextAdvance", "TextAdvance", "Automatically accepts and turns in quests, skips cutscenes\nand dialogue.", new Uri("https://github.com/NightmareXIV/TextAdvance"), new Uri("https://github.com/NightmareXIV/MyDalamudPlugins/raw/main/pluginmaster.json")) + new PluginInfo("TextAdvance", "TextAdvance", "Automatically accepts and turns in quests, skips cutscenes\nand dialogue.", new Uri("https://github.com/NightmareXIV/TextAdvance"), new Uri("https://github.com/NightmareXIV/MyDalamudPlugins/raw/main/pluginmaster.json")), + new PluginInfo("vnavmesh", "vnavmesh", "vnavmesh handles the navigation within a zone, moving\nyour character to the next quest-related objective.", new Uri("https://github.com/awgil/ffxiv_navmesh/"), new Uri("https://puni.sh/api/repository/veyn")) }); private static readonly ReadOnlyDictionary CombatPlugins = new Dictionary @@ -36,13 +36,13 @@ internal sealed class PluginConfigComponent : ConfigComponent Questionable.Configuration.ECombatModule.BossMod, new PluginInfo("Boss Mod (VBM)", "BossMod", string.Empty, new Uri("https://github.com/awgil/ffxiv_bossmod"), new Uri("https://puni.sh/api/repository/veyn")) }, - { - Questionable.Configuration.ECombatModule.WrathCombo, - new PluginInfo("Wrath Combo", "WrathCombo", string.Empty, new Uri("https://github.com/PunishXIV/WrathCombo"), new Uri("https://puni.sh/api/plugins")) - }, { Questionable.Configuration.ECombatModule.RotationSolverReborn, new PluginInfo("Rotation Solver Reborn", "RotationSolver", string.Empty, new Uri("https://github.com/FFXIV-CombatReborn/RotationSolverReborn"), new Uri("https://raw.githubusercontent.com/FFXIV-CombatReborn/CombatRebornRepo/main/pluginmaster.json")) + }, + { + Questionable.Configuration.ECombatModule.WrathCombo, + new PluginInfo("Wrath Combo", "WrathCombo", string.Empty, new Uri("https://github.com/PunishXIV/WrathCombo"), new Uri("https://puni.sh/api/plugins")) } }.AsReadOnly(); @@ -66,9 +66,10 @@ internal sealed class PluginConfigComponent : ConfigComponent _pluginInterface = pluginInterface; _uiUtils = uiUtils; _commandManager = commandManager; - PluginInfo[] obj = new PluginInfo[5] + PluginInfo[] obj = new PluginInfo[6] { new PluginInfo("Artisan", "Artisan", "Handles automatic crafting for quests that require\ncrafted items.", new Uri("https://github.com/PunishXIV/Artisan"), new Uri("https://puni.sh/api/plugins")), + new PluginInfo("AutoDuty", "AutoDuty", "Automatically handles dungeon and trial completion during\nMain Scenario Quest progression.", new Uri("https://github.com/erdelf/AutoDuty"), new Uri("https://puni.sh/api/repository/erdelf")), null, null, null, @@ -82,7 +83,8 @@ internal sealed class PluginConfigComponent : ConfigComponent Span span = CollectionsMarshal.AsSpan(list); int index = 0; span[index] = new PluginDetailInfo("'Sniper no sniping' enabled", "Automatically completes sniping tasks introduced in Stormblood", () => automatonIpc.IsAutoSnipeEnabled); - obj[1] = new PluginInfo("CBT (formerly known as Automaton)", "Automaton", "Automaton is a collection of automation-related tweaks.", websiteUri, dalamudRepositoryUri, "/cbt", list); + obj[2] = new PluginInfo("CBT (formerly known as Automaton)", "Automaton", "Automaton is a collection of automation-related tweaks.", websiteUri, dalamudRepositoryUri, "/cbt", list); + obj[3] = new PluginInfo("NotificationMaster", "NotificationMaster", "Sends a configurable out-of-game notification if a quest\nrequires manual actions.", new Uri("https://github.com/NightmareXIV/NotificationMaster"), null); Uri websiteUri2 = new Uri("https://github.com/PunishXIV/PandorasBox"); Uri dalamudRepositoryUri2 = new Uri("https://puni.sh/api/plugins"); index = 1; @@ -91,9 +93,8 @@ internal sealed class PluginConfigComponent : ConfigComponent span = CollectionsMarshal.AsSpan(list2); num = 0; span[num] = new PluginDetailInfo("'Auto Active Time Maneuver' enabled", "Automatically completes active time maneuvers in\nsingle player instances, trials and raids\"", () => pandorasBoxIpc.IsAutoActiveTimeManeuverEnabled); - obj[2] = new PluginInfo("Pandora's Box", "PandorasBox", "Pandora's Box is a collection of tweaks.", websiteUri2, dalamudRepositoryUri2, "/pandora", list2); - obj[3] = new PluginInfo("QuestMap", "QuestMap", "Displays quest objectives and markers on the map for\nbetter navigation and tracking.", new Uri("https://github.com/rreminy/QuestMap"), null); - obj[4] = new PluginInfo("NotificationMaster", "NotificationMaster", "Sends a configurable out-of-game notification if a quest\nrequires manual actions.", new Uri("https://github.com/NightmareXIV/NotificationMaster"), null); + obj[4] = new PluginInfo("Pandora's Box", "PandorasBox", "Pandora's Box is a collection of tweaks.", websiteUri2, dalamudRepositoryUri2, "/pandora", list2); + obj[5] = new PluginInfo("QuestMap", "QuestMap", "Displays quest objectives and markers on the map for\nbetter navigation and tracking.", new Uri("https://github.com/rreminy/QuestMap"), null); _recommendedPlugins = new global::_003C_003Ez__ReadOnlyArray(obj); } @@ -147,8 +148,8 @@ internal sealed class PluginConfigComponent : ConfigComponent _pluginInterface.SavePluginConfig(_configuration); } allRequiredInstalled &= DrawCombatPlugin(Questionable.Configuration.ECombatModule.BossMod, checklistPadding); - allRequiredInstalled &= DrawCombatPlugin(Questionable.Configuration.ECombatModule.WrathCombo, checklistPadding); allRequiredInstalled &= DrawCombatPlugin(Questionable.Configuration.ECombatModule.RotationSolverReborn, checklistPadding); + allRequiredInstalled &= DrawCombatPlugin(Questionable.Configuration.ECombatModule.WrathCombo, checklistPadding); } } ImGui.Spacing(); diff --git a/Questionable/Questionable.Windows.JournalComponents/GatheringJournalComponent.cs b/Questionable/Questionable.Windows.JournalComponents/GatheringJournalComponent.cs index f96ee5f..f9bba1f 100644 --- a/Questionable/Questionable.Windows.JournalComponents/GatheringJournalComponent.cs +++ b/Questionable/Questionable.Windows.JournalComponents/GatheringJournalComponent.cs @@ -8,7 +8,7 @@ using Dalamud.Interface.Colors; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin; using Dalamud.Plugin.Services; -using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game; using LLib.GameData; using Lumina.Excel; using Lumina.Excel.Sheets; @@ -84,12 +84,9 @@ internal sealed class GatheringJournalComponent private string _searchText = string.Empty; - [Signature("48 89 5C 24 ?? 57 48 83 EC 20 8B D9 8B F9")] - private GetIsGatheringItemGatheredDelegate _getIsGatheringItemGathered; - - private bool IsGatheringItemGathered(uint item) + private static bool IsGatheringItemGathered(uint item) { - return _getIsGatheringItemGathered((ushort)item) != 0; + return QuestManager.IsGatheringItemGathered((ushort)item); } public GatheringJournalComponent(IDataManager dataManager, IDalamudPluginInterface pluginInterface, UiUtils uiUtils, IGameInteropProvider gameInteropProvider, GatheringPointRegistry gatheringPointRegistry) diff --git a/Questionable/Questionable/Configuration.cs b/Questionable/Questionable/Configuration.cs index 39cb0a4..47c005a 100644 --- a/Questionable/Questionable/Configuration.cs +++ b/Questionable/Questionable/Configuration.cs @@ -34,7 +34,7 @@ internal sealed class Configuration : IPluginConfiguration public bool AutoStepRefreshEnabled { get; set; } = true; - public int AutoStepRefreshDelaySeconds { get; set; } = 10; + public int AutoStepRefreshDelaySeconds { get; set; } = 60; public bool HideSeasonalEventsFromJournalProgress { get; set; } @@ -43,6 +43,8 @@ internal sealed class Configuration : IPluginConfiguration public bool ClearPriorityQuestsOnCompletion { get; set; } public bool ShowChangelogOnUpdate { get; set; } = true; + + public bool StopOnPlayerInput { get; set; } } internal sealed class StopConfiguration diff --git a/QuestionableCompanion/ChauffeurModeService.cs b/QuestionableCompanion/ChauffeurModeService.cs index 30cd6ce..a715cb8 100644 --- a/QuestionableCompanion/ChauffeurModeService.cs +++ b/QuestionableCompanion/ChauffeurModeService.cs @@ -21,6 +21,7 @@ using Lumina.Excel; using Lumina.Excel.Sheets; using Newtonsoft.Json.Linq; using QuestionableCompanion; +using QuestionableCompanion.Models; using QuestionableCompanion.Services; public class ChauffeurModeService : IDisposable @@ -81,6 +82,8 @@ public class ChauffeurModeService : IDisposable private DateTime? lastZoneChangeTime; + private DateTime lastDutyExitTime = DateTime.MinValue; + private bool isFollowingQuester; private DateTime lastFollowCheck = DateTime.MinValue; @@ -109,6 +112,14 @@ public class ChauffeurModeService : IDisposable public bool IsTransportingQuester => isTransportingQuester; + public void UpdateQuesterPositionFromLAN(float x, float y, float z, uint zoneId, string questerName) + { + lastQuesterPosition = new Vector3(x, y, z); + lastQuesterZone = zoneId; + followingQuesterName = questerName; + discoveredQuesters[questerName] = DateTime.Now; + } + public string? GetHelperStatus(string helperKey) { if (!helperStatuses.TryGetValue(helperKey, out string status)) @@ -118,6 +129,18 @@ public class ChauffeurModeService : IDisposable return status; } + public void StartHelperStatusBroadcast() + { + if (config.IsHighLevelHelper) + { + log.Information("[ChauffeurMode] Starting periodic helper status broadcast (Helper mode enabled)"); + framework.RunOnTick(delegate + { + BroadcastHelperStatusPeriodically(); + }, TimeSpan.FromSeconds(1L)); + } + } + public List GetDiscoveredQuesters() { DateTime now = DateTime.Now; @@ -127,7 +150,19 @@ public class ChauffeurModeService : IDisposable { discoveredQuesters.Remove(stale); } - return discoveredQuesters.Keys.ToList(); + List result = discoveredQuesters.Keys.ToList(); + LANHelperServer lanServer = Plugin.Instance?.GetLANHelperServer(); + if (lanServer != null) + { + foreach (string client in lanServer.GetConnectedClientNames()) + { + if (!result.Contains(client)) + { + result.Add(client); + } + } + } + return result; } public ChauffeurModeService(Configuration config, IPluginLog log, IClientState clientState, ICondition condition, IFramework framework, ICommandManager commandManager, IDataManager dataManager, IPartyList partyList, IObjectTable objectTable, QuestionableIPC questionableIPC, CrossProcessIPC crossProcessIPC, PartyInviteService partyInviteService, PartyInviteAutoAccept partyInviteAutoAccept, IDalamudPluginInterface pluginInterface, MemoryHelper memoryHelper, MovementMonitorService? movementMonitor = null) @@ -158,6 +193,7 @@ public class ChauffeurModeService : IDisposable crossProcessIPC.OnHelperStatusUpdate += OnHelperStatusUpdate; crossProcessIPC.OnQuesterPositionUpdate += OnQuesterPositionUpdate; clientState.TerritoryChanged += OnTerritoryChanged; + condition.ConditionChange += OnConditionChanged; if (config.IsHighLevelHelper) { framework.RunOnTick(delegate @@ -170,6 +206,15 @@ public class ChauffeurModeService : IDisposable log.Information("[ChauffeurMode] Service initialized"); } + private void OnConditionChanged(ConditionFlag flag, bool value) + { + if (flag == ConditionFlag.BoundByDuty && !value) + { + lastDutyExitTime = DateTime.Now; + log.Information("[ChauffeurMode] Left duty - starting 10s grace period for zone checks"); + } + } + private void OnFrameworkUpdate(IFramework framework) { if (config.IsHighLevelHelper && config.EnableHelperFollowing && (DateTime.Now - lastFollowCheck).TotalSeconds >= (double)config.HelperFollowCheckInterval) @@ -202,61 +247,72 @@ public class ChauffeurModeService : IDisposable return; } } - if (objectTable.LocalPlayer == null) + double timeSinceDutyExit = (DateTime.Now - lastDutyExitTime).TotalSeconds; + if (timeSinceDutyExit < 10.0) { - return; + if (timeSinceDutyExit < 1.0 || timeSinceDutyExit > 9.0) + { + log.Debug($"[WaitTerritory] Duty Grace Period: Waiting for stabilization after duty exit (elapsed: {timeSinceDutyExit:F1}s / 10.0s)"); + } } - object task = questionableIPC.GetCurrentTask(); - if (task == null) + else { - return; - } - try - { - if (!(task is JObject jObject)) + if (objectTable.LocalPlayer == null) { return; } - JToken taskNameToken = jObject["TaskName"]; - if (taskNameToken == null) + object task = questionableIPC.GetCurrentTask(); + if (task == null) { return; } - string taskName = taskNameToken.ToString(); - if (string.IsNullOrEmpty(taskName)) + try { - return; - } - Match waitTerritoryMatch = new Regex("Wait\\(territory:\\s*(.+?)\\s*\\((\\d+)\\)\\)").Match(taskName); - if (!waitTerritoryMatch.Success) - { - return; - } - string territoryName = waitTerritoryMatch.Groups[1].Value.Trim(); - uint territoryId = uint.Parse(waitTerritoryMatch.Groups[2].Value); - if (clientState.TerritoryType == territoryId) - { - log.Debug($"[WaitTerritory] Already in target territory {territoryName} ({territoryId}) - skipping teleport"); - return; - } - string mappedName = MapTerritoryName(territoryName); - log.Information($"[WaitTerritory] Wait(territory) detected: {territoryName} ({territoryId}) → Auto-teleporting to {mappedName}"); - framework.RunOnFrameworkThread(delegate - { - try + if (!(task is JObject jObject)) { - string content = "/li " + mappedName; - commandManager.ProcessCommand(content); + return; } - catch (Exception ex2) + JToken taskNameToken = jObject["TaskName"]; + if (taskNameToken == null) { - log.Error("[WaitTerritory] Failed to teleport to " + mappedName + ": " + ex2.Message); + return; } - }); - } - catch (Exception ex) - { - log.Error("[WaitTerritory] Error checking Wait(territory) task: " + ex.Message); + string taskName = taskNameToken.ToString(); + if (string.IsNullOrEmpty(taskName)) + { + return; + } + Match waitTerritoryMatch = new Regex("Wait\\(territory:\\s*(.+?)\\s*\\((\\d+)\\)\\)").Match(taskName); + if (!waitTerritoryMatch.Success) + { + return; + } + string territoryName = waitTerritoryMatch.Groups[1].Value.Trim(); + uint territoryId = uint.Parse(waitTerritoryMatch.Groups[2].Value); + if (clientState.TerritoryType == territoryId) + { + log.Debug($"[WaitTerritory] Already in target territory {territoryName} ({territoryId}) - skipping teleport"); + return; + } + string mappedName = MapTerritoryName(territoryName); + log.Information($"[WaitTerritory] Wait(territory) detected: {territoryName} ({territoryId}) → Auto-teleporting to {mappedName}"); + framework.RunOnFrameworkThread(delegate + { + try + { + string content = "/li " + mappedName; + commandManager.ProcessCommand(content); + } + catch (Exception ex2) + { + log.Error("[WaitTerritory] Failed to teleport to " + mappedName + ": " + ex2.Message); + } + }); + } + catch (Exception ex) + { + log.Error("[WaitTerritory] Error checking Wait(territory) task: " + ex.Message); + } } } @@ -280,6 +336,10 @@ public class ChauffeurModeService : IDisposable return; } } + if ((DateTime.Now - lastDutyExitTime).TotalSeconds < 10.0) + { + return; + } ushort currentZoneId = clientState.TerritoryType; if (BLACKLISTED_ZONES.Contains(currentZoneId)) { @@ -587,6 +647,14 @@ public class ChauffeurModeService : IDisposable log.Information("[ChauffeurMode] ========================================"); log.Information("[ChauffeurMode] === SUMMONING HELPER ==="); log.Information("[ChauffeurMode] ========================================"); + if (config.HelperSelection == HelperSelectionMode.ManualInput) + { + log.Warning("[ChauffeurMode] [QUESTER] Manual Input mode is selected!"); + log.Warning("[ChauffeurMode] [QUESTER] Chauffeur Mode requires IPC communication and cannot work with Manual Input."); + log.Warning("[ChauffeurMode] [QUESTER] Please switch to 'Auto' or 'Dropdown' mode to use Chauffeur."); + log.Warning("[ChauffeurMode] [QUESTER] Walking to destination instead."); + return; + } if (!string.IsNullOrEmpty(config.PreferredHelper)) { string preferredHelper = config.PreferredHelper; @@ -660,7 +728,88 @@ public class ChauffeurModeService : IDisposable log.Information($"[ChauffeurMode] Quester Position: ({questerPos.X:F2}, {questerPos.Y:F2}, {questerPos.Z:F2})"); log.Information($"[ChauffeurMode] Target: ({targetPos.X:F2}, {targetPos.Y:F2}, {targetPos.Z:F2})"); log.Information($"[ChauffeurMode] AttuneAetheryte: {isAttuneAetheryte}"); - crossProcessIPC.SendChauffeurSummonRequest(questerName, questerWorld, zoneId, targetPos, questerPos, isAttuneAetheryte); + bool isLanHelper = false; + string lanIp = null; + log.Information("[ChauffeurMode] Checking if preferred helper '" + config.PreferredHelper + "' is a LAN helper..."); + LANHelperClient lanClient = Plugin.Instance?.GetLANHelperClient(); + if (lanClient != null) + { + IReadOnlyList lanHelpers = lanClient.DiscoveredHelpers; + log.Information($"[ChauffeurMode] Found {lanHelpers.Count} LAN helpers in discovery list"); + if (!string.IsNullOrEmpty(config.PreferredHelper)) + { + foreach (LANHelperInfo helper in lanHelpers) + { + string helperKey = $"{helper.Name}@{helper.WorldId}"; + log.Information("[ChauffeurMode] Checking LAN helper: " + helperKey + " at " + helper.IPAddress); + if (helperKey == config.PreferredHelper) + { + isLanHelper = true; + lanIp = helper.IPAddress; + log.Information("[ChauffeurMode] ✓ MATCHED! This is a LAN helper at " + lanIp); + break; + } + } + if (!isLanHelper) + { + log.Information("[ChauffeurMode] No match found - PreferredHelper '" + config.PreferredHelper + "' not in LAN list"); + } + } + else if (lanHelpers.Any((LANHelperInfo h) => h.Status == LANHelperStatus.Available)) + { + LANHelperInfo firstAvailable = lanHelpers.FirstOrDefault((LANHelperInfo h) => h.Status == LANHelperStatus.Available); + if (firstAvailable != null) + { + isLanHelper = true; + lanIp = firstAvailable.IPAddress; + string autoSelectedKey = $"{firstAvailable.Name}@{firstAvailable.WorldId}"; + log.Information("[ChauffeurMode] AUTO-SELECTED LAN helper: " + autoSelectedKey + " at " + lanIp); + } + } + else if (lanHelpers.Count > 0) + { + LANHelperInfo firstHelper = lanHelpers.First(); + isLanHelper = true; + lanIp = firstHelper.IPAddress; + string autoSelectedKey2 = $"{firstHelper.Name}@{firstHelper.WorldId}"; + log.Information("[ChauffeurMode] AUTO-SELECTED first LAN helper (no Available status): " + autoSelectedKey2 + " at " + lanIp); + } + else + { + log.Information("[ChauffeurMode] No PreferredHelper configured and no LAN helpers available - using local IPC"); + } + } + else + { + log.Warning("[ChauffeurMode] LANHelperClient is null!"); + log.Information("[ChauffeurMode] Falling back to local IPC"); + } + if (isLanHelper && !string.IsNullOrEmpty(lanIp)) + { + log.Information("[ChauffeurMode] Selected helper is on LAN (" + lanIp + ") - Sending LAN Summon Request"); + if (lanClient != null) + { + LANChauffeurSummon summonData = new LANChauffeurSummon + { + QuesterName = questerName, + QuesterWorldId = questerWorld, + ZoneId = zoneId, + TargetX = targetPos.X, + TargetY = targetPos.Y, + TargetZ = targetPos.Z, + QuesterX = questerPos.X, + QuesterY = questerPos.Y, + QuesterZ = questerPos.Z, + IsAttuneAetheryte = isAttuneAetheryte + }; + lanClient.SendChauffeurSummonAsync(lanIp, summonData); + } + } + else + { + log.Information("[ChauffeurMode] Sending local IPC Summon Request"); + crossProcessIPC.SendChauffeurSummonRequest(questerName, questerWorld, zoneId, targetPos, questerPos, isAttuneAetheryte); + } } public bool IsRestrictedZone(uint zoneId) @@ -756,15 +905,25 @@ public class ChauffeurModeService : IDisposable } } } - log.Debug($"[ChauffeurMode] Found {mounts.Count} multi-seater mounts"); } - catch (Exception ex) + catch (Exception) { - log.Error("[ChauffeurMode] Error loading multi-seater mounts: " + ex.Message); } return mounts; } + public void StartHelperWorkflow(string questerName, ushort questerWorld, uint zoneId, Vector3 targetPos, Vector3 questerPos, bool isAttuneAetheryte) + { + log.Information("[ChauffeurMode] ========================================="); + log.Information("[ChauffeurMode] *** StartHelperWorkflow CALLED ***"); + log.Information("[ChauffeurMode] ========================================="); + log.Information($"[ChauffeurMode] Quester: {questerName}@{questerWorld}"); + log.Information($"[ChauffeurMode] Zone: {zoneId}"); + log.Information($"[ChauffeurMode] Target: ({targetPos.X:F2}, {targetPos.Y:F2}, {targetPos.Z:F2})"); + log.Information($"[ChauffeurMode] AttuneAetheryte: {isAttuneAetheryte}"); + OnChauffeurSummonRequest(questerName, questerWorld, zoneId, targetPos, questerPos, isAttuneAetheryte); + } + private void OnChauffeurSummonRequest(string questerName, ushort questerWorld, uint zoneId, Vector3 targetPos, Vector3 questerPos, bool isAttuneAetheryte) { if (!config.ChauffeurModeEnabled) @@ -1274,6 +1433,12 @@ public class ChauffeurModeService : IDisposable log.Information($"[ChauffeurMode] [HELPER] Helper position: ({objectTable.LocalPlayer?.Position.X:F2}, {objectTable.LocalPlayer?.Position.Y:F2}, {objectTable.LocalPlayer?.Position.Z:F2})"); crossProcessIPC.SendChauffeurMountReady(questerName, questerWorld); log.Information("[ChauffeurMode] [HELPER] Mount ready signal sent via IPC"); + LANHelperServer lanServer = Plugin.Instance?.GetLANHelperServer(); + if (lanServer != null) + { + log.Information("[ChauffeurMode] [HELPER] Also sending mount ready via LAN to connected clients"); + lanServer.SendChauffeurMountReady(questerName, questerWorld); + } log.Information("[ChauffeurMode] [WORKFLOW] Waiting 8 seconds for quester to mount..."); await Task.Delay(8000); log.Information($"[ChauffeurMode] [WORKFLOW] Step 6: Transporting to target ({finalTargetPos.X:F2}, {finalTargetPos.Y:F2}, {finalTargetPos.Z:F2})"); @@ -1333,6 +1498,12 @@ public class ChauffeurModeService : IDisposable log.Information("[ChauffeurMode] [HELPER] Transport complete - FLAGS RESET + STATUS AVAILABLE (before notification)"); log.Information($"[ChauffeurMode] [HELPER] Notifying Quester of arrival: {questerName}@{questerWorld}"); crossProcessIPC.SendChauffeurArrived(questerName, questerWorld); + LANHelperServer lanServerArrival = Plugin.Instance?.GetLANHelperServer(); + if (lanServerArrival != null) + { + log.Information("[ChauffeurMode] [HELPER] Also sending arrival via LAN to connected clients"); + lanServerArrival.SendChauffeurArrived(questerName, questerWorld); + } log.Information("[ChauffeurMode] [HELPER] Waiting for quester to restart Questionable and checking for AttuneAetheryte task..."); await Task.Delay(3000); bool isAttuneAetheryteTask = false; @@ -1954,7 +2125,7 @@ public class ChauffeurModeService : IDisposable } } - private unsafe void OnChauffeurMountReady(string questerName, ushort questerWorld) + public unsafe void OnChauffeurMountReady(string questerName, ushort questerWorld) { if (!config.ChauffeurModeEnabled || !config.IsQuester || !isWaitingForHelper) { @@ -2306,7 +2477,7 @@ public class ChauffeurModeService : IDisposable } } - private void OnChauffeurArrived(string questerName, ushort questerWorld) + public void OnChauffeurArrived(string questerName, ushort questerWorld) { if (!config.ChauffeurModeEnabled || !config.IsQuester || !isWaitingForHelper) { @@ -2747,13 +2918,24 @@ public class ChauffeurModeService : IDisposable lastZoneChangeTime = null; } IPlayerCharacter localPlayer = objectTable.LocalPlayer; - if (localPlayer != null) + if (localPlayer == null) { - string questerName = localPlayer.Name.ToString(); - ushort questerWorld = (ushort)localPlayer.HomeWorld.RowId; - ushort currentZone = clientState.TerritoryType; - Vector3 position = localPlayer.Position; - crossProcessIPC.BroadcastQuesterPosition(questerName, questerWorld, currentZone, position); + return; + } + string questerName = localPlayer.Name.ToString(); + ushort questerWorld = (ushort)localPlayer.HomeWorld.RowId; + ushort currentZone = clientState.TerritoryType; + Vector3 position = localPlayer.Position; + crossProcessIPC.BroadcastQuesterPosition(questerName, questerWorld, currentZone, position); + LANHelperClient lanClient = Plugin.Instance?.GetLANHelperClient(); + if (lanClient != null) + { + IReadOnlyList lanHelpers = lanClient.DiscoveredHelpers; + if (lanHelpers.Count > 0) + { + LANHelperInfo firstHelper = lanHelpers.First(); + lanClient.SendFollowCommandAsync(firstHelper.IPAddress, position.X, position.Y, position.Z, currentZone); + } } } diff --git a/QuestionableCompanion/QuestionableCompanion.Models/LANChauffeurResponse.cs b/QuestionableCompanion/QuestionableCompanion.Models/LANChauffeurResponse.cs new file mode 100644 index 0000000..00b49ad --- /dev/null +++ b/QuestionableCompanion/QuestionableCompanion.Models/LANChauffeurResponse.cs @@ -0,0 +1,8 @@ +namespace QuestionableCompanion.Models; + +public class LANChauffeurResponse +{ + public string QuesterName { get; set; } = string.Empty; + + public ushort QuesterWorldId { get; set; } +} diff --git a/QuestionableCompanion/QuestionableCompanion.Models/LANChauffeurSummon.cs b/QuestionableCompanion/QuestionableCompanion.Models/LANChauffeurSummon.cs new file mode 100644 index 0000000..c5a2dc4 --- /dev/null +++ b/QuestionableCompanion/QuestionableCompanion.Models/LANChauffeurSummon.cs @@ -0,0 +1,24 @@ +namespace QuestionableCompanion.Models; + +public class LANChauffeurSummon +{ + public string QuesterName { get; set; } = string.Empty; + + public ushort QuesterWorldId { get; set; } + + public uint ZoneId { get; set; } + + public float TargetX { get; set; } + + public float TargetY { get; set; } + + public float TargetZ { get; set; } + + public float QuesterX { get; set; } + + public float QuesterY { get; set; } + + public float QuesterZ { get; set; } + + public bool IsAttuneAetheryte { get; set; } +} diff --git a/QuestionableCompanion/QuestionableCompanion.Models/LANFollowCommand.cs b/QuestionableCompanion/QuestionableCompanion.Models/LANFollowCommand.cs new file mode 100644 index 0000000..d2310e9 --- /dev/null +++ b/QuestionableCompanion/QuestionableCompanion.Models/LANFollowCommand.cs @@ -0,0 +1,12 @@ +namespace QuestionableCompanion.Models; + +public class LANFollowCommand +{ + public float X { get; set; } + + public float Y { get; set; } + + public float Z { get; set; } + + public uint TerritoryId { get; set; } +} diff --git a/QuestionableCompanion/QuestionableCompanion.Models/LANHeartbeat.cs b/QuestionableCompanion/QuestionableCompanion.Models/LANHeartbeat.cs new file mode 100644 index 0000000..cba7496 --- /dev/null +++ b/QuestionableCompanion/QuestionableCompanion.Models/LANHeartbeat.cs @@ -0,0 +1,10 @@ +namespace QuestionableCompanion.Models; + +public class LANHeartbeat +{ + public string ClientName { get; set; } = string.Empty; + + public ushort ClientWorldId { get; set; } + + public string ClientRole { get; set; } = string.Empty; +} diff --git a/QuestionableCompanion/QuestionableCompanion.Models/LANHelperInfo.cs b/QuestionableCompanion/QuestionableCompanion.Models/LANHelperInfo.cs new file mode 100644 index 0000000..df822d6 --- /dev/null +++ b/QuestionableCompanion/QuestionableCompanion.Models/LANHelperInfo.cs @@ -0,0 +1,16 @@ +using System; + +namespace QuestionableCompanion.Models; + +public class LANHelperInfo +{ + public string Name { get; set; } = string.Empty; + + public ushort WorldId { get; set; } + + public string IPAddress { get; set; } = string.Empty; + + public LANHelperStatus Status { get; set; } + + public DateTime LastSeen { get; set; } = DateTime.Now; +} diff --git a/QuestionableCompanion/QuestionableCompanion.Models/LANHelperRequest.cs b/QuestionableCompanion/QuestionableCompanion.Models/LANHelperRequest.cs new file mode 100644 index 0000000..e7d4499 --- /dev/null +++ b/QuestionableCompanion/QuestionableCompanion.Models/LANHelperRequest.cs @@ -0,0 +1,10 @@ +namespace QuestionableCompanion.Models; + +public class LANHelperRequest +{ + public string QuesterName { get; set; } = string.Empty; + + public ushort QuesterWorldId { get; set; } + + public string DutyName { get; set; } = string.Empty; +} diff --git a/QuestionableCompanion/QuestionableCompanion.Models/LANHelperStatus.cs b/QuestionableCompanion/QuestionableCompanion.Models/LANHelperStatus.cs new file mode 100644 index 0000000..7ec5a82 --- /dev/null +++ b/QuestionableCompanion/QuestionableCompanion.Models/LANHelperStatus.cs @@ -0,0 +1,12 @@ +namespace QuestionableCompanion.Models; + +public enum LANHelperStatus +{ + Available, + Busy, + InParty, + InDuty, + Transporting, + Offline, + Error +} diff --git a/QuestionableCompanion/QuestionableCompanion.Models/LANHelperStatusResponse.cs b/QuestionableCompanion/QuestionableCompanion.Models/LANHelperStatusResponse.cs new file mode 100644 index 0000000..5e762ae --- /dev/null +++ b/QuestionableCompanion/QuestionableCompanion.Models/LANHelperStatusResponse.cs @@ -0,0 +1,12 @@ +namespace QuestionableCompanion.Models; + +public class LANHelperStatusResponse +{ + public string Name { get; set; } = string.Empty; + + public ushort WorldId { get; set; } + + public LANHelperStatus Status { get; set; } + + public string? CurrentActivity { get; set; } +} diff --git a/QuestionableCompanion/QuestionableCompanion.Models/LANMessage.cs b/QuestionableCompanion/QuestionableCompanion.Models/LANMessage.cs new file mode 100644 index 0000000..ea51fb4 --- /dev/null +++ b/QuestionableCompanion/QuestionableCompanion.Models/LANMessage.cs @@ -0,0 +1,35 @@ +using System; +using Newtonsoft.Json; + +namespace QuestionableCompanion.Models; + +public class LANMessage +{ + public LANMessageType Type { get; set; } + + public DateTime Timestamp { get; set; } = DateTime.Now; + + public string? Data { get; set; } + + public LANMessage() + { + } + + public LANMessage(LANMessageType type, object? data = null) + { + Type = type; + if (data != null) + { + Data = JsonConvert.SerializeObject(data); + } + } + + public T? GetData() + { + if (string.IsNullOrEmpty(Data)) + { + return default(T); + } + return JsonConvert.DeserializeObject(Data); + } +} diff --git a/QuestionableCompanion/QuestionableCompanion.Models/LANMessageType.cs b/QuestionableCompanion/QuestionableCompanion.Models/LANMessageType.cs new file mode 100644 index 0000000..3c04d4e --- /dev/null +++ b/QuestionableCompanion/QuestionableCompanion.Models/LANMessageType.cs @@ -0,0 +1,24 @@ +namespace QuestionableCompanion.Models; + +public enum LANMessageType +{ + DISCOVER_REQUEST, + DISCOVER_RESPONSE, + REQUEST_HELPER, + HELPER_STATUS, + INVITE_NOTIFICATION, + INVITE_ACCEPTED, + HELPER_IN_PARTY, + HELPER_READY, + HELPER_IN_DUTY, + DUTY_COMPLETE, + FOLLOW_COMMAND, + FOLLOW_STARTED, + FOLLOW_ARRIVED, + CHAUFFEUR_PICKUP_REQUEST, + CHAUFFEUR_HELPER_READY_FOR_MOUNT, + CHAUFFEUR_HELPER_ARRIVED_DEST, + ERROR, + DISCONNECT, + HEARTBEAT +} diff --git a/QuestionableCompanion/QuestionableCompanion.Services/ARRTrialAutomationService.cs b/QuestionableCompanion/QuestionableCompanion.Services/ARRTrialAutomationService.cs new file mode 100644 index 0000000..223b233 --- /dev/null +++ b/QuestionableCompanion/QuestionableCompanion.Services/ARRTrialAutomationService.cs @@ -0,0 +1,393 @@ +using System; +using System.Threading.Tasks; +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.Text; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.UI; + +namespace QuestionableCompanion.Services; + +public class ARRTrialAutomationService : IDisposable +{ + private readonly IPluginLog log; + + private readonly IFramework framework; + + private readonly ICommandManager commandManager; + + private readonly IChatGui chatGui; + + private readonly Configuration config; + + private readonly QuestionableIPC questionableIPC; + + private readonly SubmarineManager submarineManager; + + private readonly HelperManager helperManager; + + private readonly IPartyList partyList; + + private readonly ICondition condition; + + private readonly MemoryHelper memoryHelper; + + private bool isInDuty; + + private static readonly (uint QuestId, uint TrialId, string ADCommand, string Name)[] Trials = new(uint, uint, string, string)[3] + { + (1048u, 20004u, "/ad run trial 292 1", "Ifrit HM"), + (1157u, 20006u, "/ad run trial 294 1", "Garuda HM"), + (1158u, 20005u, "/ad run trial 293 1", "Titan HM") + }; + + private const uint TRIGGER_QUEST = 89u; + + private const uint TARGET_QUEST = 363u; + + private bool isProcessing; + + private int currentTrialIndex = -1; + + private bool waitingForQuest; + + private bool waitingForParty; + + private bool waitingForTrial; + + private DateTime lastCheckTime = DateTime.MinValue; + + public ARRTrialAutomationService(IPluginLog log, IFramework framework, ICommandManager commandManager, IChatGui chatGui, Configuration config, QuestionableIPC questionableIPC, SubmarineManager submarineManager, HelperManager helperManager, IPartyList partyList, ICondition condition, MemoryHelper memoryHelper) + { + this.log = log; + this.framework = framework; + this.commandManager = commandManager; + this.chatGui = chatGui; + this.config = config; + this.questionableIPC = questionableIPC; + this.submarineManager = submarineManager; + this.helperManager = helperManager; + this.partyList = partyList; + this.condition = condition; + this.memoryHelper = memoryHelper; + framework.Update += OnFrameworkUpdate; + condition.ConditionChange += OnConditionChanged; + log.Information("[ARRTrials] Service initialized"); + } + + private void OnFrameworkUpdate(IFramework framework) + { + if (!isProcessing) + { + return; + } + if (waitingForParty && partyList != null && partyList.Length > 1) + { + if (!((DateTime.Now - lastCheckTime).TotalSeconds < 1.0)) + { + lastCheckTime = DateTime.Now; + log.Information($"[ARRTrials] Party join detected (Size: {partyList.Length}) - Triggering trial..."); + waitingForParty = false; + TriggerCurrentTrial(); + } + } + else if (waitingForQuest && currentTrialIndex >= 0 && currentTrialIndex < Trials.Length && !((DateTime.Now - lastCheckTime).TotalSeconds < 2.0)) + { + lastCheckTime = DateTime.Now; + (uint QuestId, uint TrialId, string ADCommand, string Name) tuple = Trials[currentTrialIndex]; + uint trialId = tuple.TrialId; + string name = tuple.Name; + bool unlocked = IsTrialUnlocked(trialId); + log.Debug($"[ARRTrials] Polling {name} ({trialId}) Unlocked: {unlocked}"); + if (unlocked) + { + log.Information("[ARRTrials] Polling detected " + name + " unlocked - Proceeding..."); + waitingForQuest = false; + helperManager.InviteHelpers(); + waitingForParty = true; + } + } + } + + public bool IsTrialComplete(uint instanceId) + { + return UIState.IsInstanceContentCompleted(instanceId); + } + + public bool IsTrialUnlocked(uint instanceId) + { + return UIState.IsInstanceContentUnlocked(instanceId); + } + + public bool IsTargetQuestAvailableOrComplete() + { + if (QuestManager.IsQuestComplete(363u)) + { + return true; + } + if (questionableIPC.IsReadyToAcceptQuest(363u.ToString())) + { + return true; + } + return false; + } + + public void OnTriggerQuestComplete() + { + if (!config.EnableARRPrimalCheck) + { + log.Debug("[ARRTrials] Feature disabled, skipping check"); + return; + } + log.Information("[ARRTrials] Quest 89 complete, starting ARR Primal check..."); + StartTrialChain(); + } + + public void StartTrialChain() + { + if (isProcessing) + { + log.Debug("[ARRTrials] Already processing trial chain"); + return; + } + isProcessing = true; + submarineManager.SetExternalPause(paused: true); + int startIndex = -1; + for (int i = Trials.Length - 1; i >= 0; i--) + { + if (!IsTrialComplete(Trials[i].TrialId)) + { + for (int j = 0; j <= i; j++) + { + if (!IsTrialComplete(Trials[j].TrialId)) + { + startIndex = j; + break; + } + } + break; + } + } + if (startIndex == -1) + { + log.Information("[ARRTrials] All trials already complete!"); + isProcessing = false; + submarineManager.SetExternalPause(paused: false); + return; + } + currentTrialIndex = startIndex; + log.Information($"[ARRTrials] Starting from trial index {startIndex}: {Trials[startIndex].Name}"); + ProcessCurrentTrial(); + } + + private void ProcessCurrentTrial() + { + if (currentTrialIndex < 0 || currentTrialIndex >= Trials.Length) + { + log.Information("[ARRTrials] Trial chain complete!"); + isProcessing = false; + submarineManager.SetExternalPause(paused: false); + return; + } + var (questId, trialId, _, name) = Trials[currentTrialIndex]; + if (IsTrialComplete(trialId)) + { + log.Information("[ARRTrials] " + name + " already complete, moving to next"); + currentTrialIndex++; + ProcessCurrentTrial(); + } + else if (!QuestManager.IsQuestComplete(questId)) + { + log.Information($"[ARRTrials] Queueing unlock quest {questId} for {name}"); + questionableIPC.AddQuestPriority(questId.ToString()); + framework.RunOnFrameworkThread(delegate + { + commandManager.ProcessCommand("/qst start"); + }); + waitingForQuest = true; + } + else + { + log.Information("[ARRTrials] " + name + " unlocked, inviting helper and triggering trial..."); + helperManager.InviteHelpers(); + waitingForParty = true; + } + } + + public void OnQuestComplete(uint questId) + { + if (!isProcessing || !waitingForQuest) + { + return; + } + for (int i = currentTrialIndex; i < Trials.Length; i++) + { + if (Trials[i].QuestId == questId) + { + log.Information($"[ARRTrials] Unlock quest {questId} completed, triggering trial"); + waitingForQuest = false; + helperManager.InviteHelpers(); + waitingForParty = true; + break; + } + } + } + + public void OnPartyReady() + { + if (isProcessing && waitingForParty) + { + waitingForParty = false; + TriggerCurrentTrial(); + } + } + + private void TriggerCurrentTrial() + { + if (currentTrialIndex >= 0 && currentTrialIndex < Trials.Length) + { + (uint, uint, string, string) tuple = Trials[currentTrialIndex]; + string adCommand = tuple.Item3; + string name = tuple.Item4; + log.Information("[ARRTrials] Triggering " + name + " via AD command"); + framework.RunOnFrameworkThread(delegate + { + chatGui.Print(new XivChatEntry + { + Message = "[QSTCompanion] Triggering " + name + "...", + Type = XivChatType.Echo + }); + commandManager.ProcessCommand("/ad cfg Unsynced true"); + commandManager.ProcessCommand(adCommand); + }); + waitingForTrial = true; + } + } + + public void OnDutyComplete() + { + if (!isProcessing || !waitingForTrial) + { + return; + } + (uint QuestId, uint TrialId, string ADCommand, string Name) tuple = Trials[currentTrialIndex]; + uint trialId = tuple.TrialId; + string name = tuple.Name; + if (IsTrialComplete(trialId)) + { + log.Information("[ARRTrials] " + name + " completed successfully!"); + waitingForTrial = false; + currentTrialIndex++; + framework.RunOnFrameworkThread(delegate + { + ProcessCurrentTrial(); + }); + } + else + { + log.Warning("[ARRTrials] " + name + " NOT complete after verification. Retrying current step..."); + waitingForTrial = false; + framework.RunOnFrameworkThread(delegate + { + ProcessCurrentTrial(); + }); + } + } + + public string GetStatus() + { + if (!isProcessing) + { + return "Idle"; + } + if (currentTrialIndex >= 0 && currentTrialIndex < Trials.Length) + { + string name = Trials[currentTrialIndex].Name; + if (waitingForQuest) + { + return "Waiting for " + name + " unlock quest"; + } + if (waitingForParty) + { + return "Waiting for party (" + name + ")"; + } + if (waitingForTrial) + { + return "In " + name; + } + return "Processing " + name; + } + return "Processing..."; + } + + public void Reset() + { + isProcessing = false; + currentTrialIndex = -1; + waitingForQuest = false; + waitingForParty = false; + waitingForTrial = false; + submarineManager.SetExternalPause(paused: false); + } + + public void Dispose() + { + framework.Update -= OnFrameworkUpdate; + condition.ConditionChange -= OnConditionChanged; + log.Information("[ARRTrials] Service disposed"); + } + + private void OnConditionChanged(ConditionFlag flag, bool value) + { + if (flag == ConditionFlag.BoundByDuty) + { + if (value && !isInDuty) + { + isInDuty = true; + log.Debug("[ARRTrials] Entered duty"); + } + else if (!value && isInDuty) + { + isInDuty = false; + OnDutyExited(); + } + } + } + + private void OnDutyExited() + { + if (!isProcessing || !waitingForTrial) + { + return; + } + log.Information("[ARRTrials] Exited duty - stopping AD and disbanding..."); + framework.RunOnFrameworkThread(delegate + { + commandManager.ProcessCommand("/ad stop"); + }); + Task.Run(async delegate + { + await Task.Delay(2000); + framework.RunOnFrameworkThread(delegate + { + memoryHelper.SendChatMessage("/leave"); + commandManager.ProcessCommand("/ad stop"); + log.Information("[ARRTrials] /leave and safety /ad stop sent"); + }); + log.Information("[ARRTrials] Waiting for completion state check..."); + await Task.Delay(1000); + (uint, uint, string, string) tuple = Trials[currentTrialIndex]; + uint trialId = tuple.Item2; + for (int i = 0; i < 10; i++) + { + if (IsTrialComplete(trialId)) + { + log.Information($"[ARRTrials] Completion verified on attempt {i + 1}"); + break; + } + await Task.Delay(1000); + } + OnDutyComplete(); + }); + } +} diff --git a/QuestionableCompanion/QuestionableCompanion.Services/CombatDutyDetectionService.cs b/QuestionableCompanion/QuestionableCompanion.Services/CombatDutyDetectionService.cs index 87ba6c9..a63b584 100644 --- a/QuestionableCompanion/QuestionableCompanion.Services/CombatDutyDetectionService.cs +++ b/QuestionableCompanion/QuestionableCompanion.Services/CombatDutyDetectionService.cs @@ -7,6 +7,13 @@ namespace QuestionableCompanion.Services; public class CombatDutyDetectionService : IDisposable { + private enum MultiClientRole + { + None, + Quester, + Helper + } + private readonly ICondition condition; private readonly IPluginLog log; @@ -143,7 +150,7 @@ public class CombatDutyDetectionService : IDisposable if (player != null) { float hpPercent = (float)player.CurrentHp / (float)player.MaxHp * 100f; - if (hpPercent <= (float)config.CombatHPThreshold) + if (hpPercent <= (float)config.CombatHPThreshold && CanExecuteCombatAutomation()) { log.Warning($"[CombatDuty] HP at {hpPercent:F1}% (threshold: {config.CombatHPThreshold}%) - enabling combat commands"); EnableCombatCommands(); @@ -231,6 +238,19 @@ public class CombatDutyDetectionService : IDisposable { return; } + if (!CanExecuteCombatAutomation()) + { + switch (GetCurrentMultiClientRole()) + { + case MultiClientRole.None: + log.Debug("[CombatDuty] Combat blocked: Role is 'None' (Config not Helper/Quester)"); + break; + case MultiClientRole.Quester: + log.Debug("[CombatDuty] Combat blocked: Quester outside Solo Duty (Let D.Automation handle invalid content)"); + break; + } + return; + } try { log.Information("[CombatDuty] ========================================"); @@ -358,6 +378,67 @@ public class CombatDutyDetectionService : IDisposable log.Information("[CombatDuty] State reset"); } + private MultiClientRole GetCurrentMultiClientRole() + { + if (config.IsHighLevelHelper) + { + return MultiClientRole.Helper; + } + if (config.IsQuester) + { + return MultiClientRole.Quester; + } + return MultiClientRole.None; + } + + private bool IsInSoloDuty() + { + if (condition[ConditionFlag.BoundByDuty95]) + { + return true; + } + if (IsInDuty && !isInAutoDutyDungeon) + { + return true; + } + return false; + } + + private bool CanExecuteCombatAutomation() + { + switch (GetCurrentMultiClientRole()) + { + case MultiClientRole.None: + if (!IsInDuty) + { + return true; + } + return false; + case MultiClientRole.Helper: + return true; + case MultiClientRole.Quester: + if (!IsInDuty) + { + return true; + } + if (IsInSoloDuty()) + { + return true; + } + if (currentQuestId == 811) + { + return true; + } + if (currentQuestId == 4591) + { + return true; + } + return false; + default: + return false; + } + } + public void Dispose() { if (combatCommandsActive) diff --git a/QuestionableCompanion/QuestionableCompanion.Services/DCTravelService.cs b/QuestionableCompanion/QuestionableCompanion.Services/DCTravelService.cs index 06c09c4..054bcc0 100644 --- a/QuestionableCompanion/QuestionableCompanion.Services/DCTravelService.cs +++ b/QuestionableCompanion/QuestionableCompanion.Services/DCTravelService.cs @@ -48,16 +48,6 @@ public class DCTravelService : IDisposable log.Information("[DCTravel] Config.DCTravelWorld: '" + config.DCTravelWorld + "'"); log.Information($"[DCTravel] State.dcTravelCompleted: {dcTravelCompleted}"); log.Information($"[DCTravel] State.dcTravelInProgress: {dcTravelInProgress}"); - if (dcTravelCompleted) - { - log.Warning("[DCTravel] SKIP: Already completed for this character"); - return false; - } - if (dcTravelInProgress) - { - log.Warning("[DCTravel] SKIP: Travel already in progress"); - return false; - } if (!config.EnableDCTravel) { log.Warning("[DCTravel] SKIP: DC Travel is DISABLED in config"); @@ -78,9 +68,25 @@ public class DCTravelService : IDisposable log.Information("[DCTravel] Target World: '" + config.DCTravelWorld + "'"); if (currentWorld.Equals(config.DCTravelWorld, StringComparison.OrdinalIgnoreCase)) { + if (!dcTravelCompleted) + { + log.Information("[DCTravel] Character is already on target world - marking as completed"); + dcTravelCompleted = true; + } log.Warning("[DCTravel] SKIP: Already on target world '" + config.DCTravelWorld + "'"); return false; } + if (dcTravelCompleted) + { + log.Warning("[DCTravel] State says completed but character is NOT on target world!"); + log.Warning("[DCTravel] Resetting state - will perform DC Travel"); + dcTravelCompleted = false; + } + if (dcTravelInProgress) + { + log.Warning("[DCTravel] SKIP: Travel already in progress"); + return false; + } log.Information("[DCTravel] ========================================"); log.Information("[DCTravel] DC TRAVEL WILL BE PERFORMED!"); log.Information("[DCTravel] ========================================"); diff --git a/QuestionableCompanion/QuestionableCompanion.Services/DungeonAutomationService.cs b/QuestionableCompanion/QuestionableCompanion.Services/DungeonAutomationService.cs index ece2c26..09268a9 100644 --- a/QuestionableCompanion/QuestionableCompanion.Services/DungeonAutomationService.cs +++ b/QuestionableCompanion/QuestionableCompanion.Services/DungeonAutomationService.cs @@ -29,6 +29,10 @@ public class DungeonAutomationService : IDisposable private readonly QuestionableIPC questionableIPC; + private readonly CrossProcessIPC crossProcessIPC; + + private readonly MultiClientIPC multiClientIPC; + private bool isWaitingForParty; private DateTime partyInviteTime = DateTime.MinValue; @@ -55,6 +59,10 @@ public class DungeonAutomationService : IDisposable private bool isAutomationActive; + private int originalDutyMode; + + private Func? isRotationActiveChecker; + private bool hasSentAtY; public bool IsWaitingForParty => isWaitingForParty; @@ -63,7 +71,30 @@ public class DungeonAutomationService : IDisposable public bool IsInAutoDutyDungeon => isAutomationActive; - public DungeonAutomationService(ICondition condition, IPluginLog log, IClientState clientState, ICommandManager commandManager, IFramework framework, IGameGui gameGui, Configuration config, HelperManager helperManager, MemoryHelper memoryHelper, QuestionableIPC questionableIPC) + public void SetRotationActiveChecker(Func checker) + { + isRotationActiveChecker = checker; + } + + private bool CanExecuteAutomation() + { + if (config.IsHighLevelHelper) + { + return true; + } + if (config.IsQuester) + { + Func? 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; @@ -75,6 +106,8 @@ public class DungeonAutomationService : IDisposable 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}"); @@ -87,6 +120,11 @@ public class DungeonAutomationService : IDisposable { 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] ========================================"); @@ -148,6 +186,10 @@ public class DungeonAutomationService : IDisposable public void Update() { + if (!CanExecuteAutomation() && !isAutomationActive) + { + return; + } if (config.EnableAutoDutyUnsynced && !isAutomationActive) { CheckWaitForPartyTask(); @@ -268,8 +310,12 @@ public class DungeonAutomationService : IDisposable return; } lastDutyEntryTime = DateTime.Now; - log.Information("[DungeonAutomation] Entered duty"); - if (expectingDutyEntry) + 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; @@ -297,7 +343,11 @@ public class DungeonAutomationService : IDisposable } lastDutyExitTime = DateTime.Now; log.Information("[DungeonAutomation] Exited duty"); - if (isAutomationActive) + 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)"); @@ -343,6 +393,11 @@ public class DungeonAutomationService : IDisposable { try { + if (!CanExecuteAutomation()) + { + log.Information("[DungeonAutomation] DisbandParty ignored - validation failed"); + return; + } log.Information("[DungeonAutomation] Disbanding party"); framework.RunOnFrameworkThread(delegate { diff --git a/QuestionableCompanion/QuestionableCompanion.Services/ErrorRecoveryService.cs b/QuestionableCompanion/QuestionableCompanion.Services/ErrorRecoveryService.cs new file mode 100644 index 0000000..cc697a3 --- /dev/null +++ b/QuestionableCompanion/QuestionableCompanion.Services/ErrorRecoveryService.cs @@ -0,0 +1,197 @@ +using System; +using System.Runtime.InteropServices; +using Dalamud.Game.NativeWrapper; +using Dalamud.Hooking; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace QuestionableCompanion.Services; + +public class ErrorRecoveryService : IDisposable +{ + private delegate char LobbyErrorHandlerDelegate(long a1, long a2, long a3); + + private readonly IPluginLog log; + + private readonly IGameInteropProvider hookProvider; + + private readonly IClientState clientState; + + private readonly IFramework framework; + + private readonly IGameGui gameGui; + + private readonly AutoRetainerIPC? autoRetainerIPC; + + private Hook? lobbyErrorHandlerHook; + + private DateTime lastDialogClickTime = DateTime.MinValue; + + public bool IsErrorDisconnect { get; private set; } + + public string? LastDisconnectedCharacter { get; private set; } + + public ErrorRecoveryService(IPluginLog log, IGameInteropProvider hookProvider, IClientState clientState, IFramework framework, IGameGui gameGui, AutoRetainerIPC? autoRetainerIPC = null) + { + this.log = log; + this.hookProvider = hookProvider; + this.clientState = clientState; + this.framework = framework; + this.gameGui = gameGui; + this.autoRetainerIPC = autoRetainerIPC; + framework.Update += OnFrameworkUpdate; + InitializeHook(); + } + + private void InitializeHook() + { + try + { + lobbyErrorHandlerHook = hookProvider.HookFromSignature("40 53 48 83 EC 30 48 8B D9 49 8B C8 E8 ?? ?? ?? ?? 8B D0", LobbyErrorHandlerDetour); + if (lobbyErrorHandlerHook != null && lobbyErrorHandlerHook.Address != IntPtr.Zero) + { + lobbyErrorHandlerHook.Enable(); + } + } + catch (Exception) + { + } + } + + private char LobbyErrorHandlerDetour(long a1, long a2, long a3) + { + try + { + nint p3 = new IntPtr(a3); + byte t1 = Marshal.ReadByte(p3); + int num = (((t1 & 0xF) > 0) ? Marshal.ReadInt32(p3 + 8) : 0); + _ = 0; + if (num != 0) + { + try + { + if (autoRetainerIPC != null) + { + string currentChar = autoRetainerIPC.GetCurrentCharacter(); + if (!string.IsNullOrEmpty(currentChar)) + { + LastDisconnectedCharacter = currentChar; + } + } + } + catch (Exception) + { + } + Marshal.WriteInt64(p3 + 8, 16000L); + IsErrorDisconnect = true; + if ((t1 & 0xF) > 0) + { + Marshal.ReadInt32(p3 + 8); + } + else + _ = 0; + } + } + catch (Exception) + { + } + return lobbyErrorHandlerHook.Original(a1, a2, a3); + } + + private unsafe void OnFrameworkUpdate(IFramework framework) + { + try + { + AtkUnitBasePtr dialoguePtr = gameGui.GetAddonByName("Dialogue"); + if (dialoguePtr == IntPtr.Zero) + { + return; + } + AtkUnitBase* dialogueAddon = (AtkUnitBase*)(nint)dialoguePtr; + if (dialogueAddon == null || !dialogueAddon->IsVisible || (DateTime.Now - lastDialogClickTime).TotalMilliseconds < 1000.0) + { + return; + } + AtkTextNode* textNode = dialogueAddon->GetTextNodeById(3u); + if (textNode == null) + { + return; + } + string text = textNode->NodeText.ToString(); + if (string.IsNullOrEmpty(text) || (!text.Contains("server", StringComparison.OrdinalIgnoreCase) && !text.Contains("connection", StringComparison.OrdinalIgnoreCase) && !text.Contains("error", StringComparison.OrdinalIgnoreCase) && !text.Contains("lost", StringComparison.OrdinalIgnoreCase))) + { + return; + } + IsErrorDisconnect = true; + try + { + if (autoRetainerIPC != null) + { + string currentChar = autoRetainerIPC.GetCurrentCharacter(); + if (!string.IsNullOrEmpty(currentChar)) + { + LastDisconnectedCharacter = currentChar; + } + } + } + catch + { + } + try + { + AtkComponentButton* button = dialogueAddon->GetComponentButtonById(4u); + if (button != null) + { + AtkResNode btnRes = button->AtkComponentBase.OwnerNode->AtkResNode; + AtkEvent* evt = btnRes.AtkEventManager.Event; + dialogueAddon->ReceiveEvent(evt->State.EventType, (int)evt->Param, btnRes.AtkEventManager.Event, null); + lastDialogClickTime = DateTime.Now; + } + } + catch (Exception) + { + } + } + catch (Exception) + { + } + } + + public void Reset() + { + IsErrorDisconnect = false; + LastDisconnectedCharacter = null; + } + + public bool RequestRelog() + { + if (autoRetainerIPC == null || !autoRetainerIPC.IsAvailable) + { + log.Warning("[ErrorRecovery] AutoRetainer IPC not available - cannot relog"); + return false; + } + if (string.IsNullOrEmpty(LastDisconnectedCharacter)) + { + log.Warning("[ErrorRecovery] No character to relog to"); + return false; + } + log.Information("[ErrorRecovery] Requesting AutoRetainer relog to: " + LastDisconnectedCharacter); + return autoRetainerIPC.SwitchCharacter(LastDisconnectedCharacter); + } + + public void Dispose() + { + try + { + framework.Update -= OnFrameworkUpdate; + if (lobbyErrorHandlerHook != null) + { + lobbyErrorHandlerHook.Disable(); + lobbyErrorHandlerHook.Dispose(); + } + } + catch (Exception) + { + } + } +} diff --git a/QuestionableCompanion/QuestionableCompanion.Services/ExecutionService.cs b/QuestionableCompanion/QuestionableCompanion.Services/ExecutionService.cs index 661d7a3..f2a6940 100644 --- a/QuestionableCompanion/QuestionableCompanion.Services/ExecutionService.cs +++ b/QuestionableCompanion/QuestionableCompanion.Services/ExecutionService.cs @@ -553,10 +553,17 @@ public class ExecutionService : IDisposable } if (dcTravelService != null && dcTravelService.IsDCTravelCompleted()) { - AddLog(LogLevel.Info, "[DCTravel] Returning to homeworld before character switch..."); - dcTravelService.ReturnToHomeworld(); - Thread.Sleep(2000); - AddLog(LogLevel.Info, "[DCTravel] Returned to homeworld"); + if (config.ReturnToHomeworldOnStopQuest) + { + AddLog(LogLevel.Info, "[DCTravel] Returning to homeworld before character switch..."); + dcTravelService.ReturnToHomeworld(); + Thread.Sleep(2000); + AddLog(LogLevel.Info, "[DCTravel] Returned to homeworld"); + } + else + { + AddLog(LogLevel.Info, "[DCTravel] Skipping return to homeworld (setting disabled)"); + } } if (config.EnableSafeWaitBeforeCharacterSwitch && safeWaitService != null) { diff --git a/QuestionableCompanion/QuestionableCompanion.Services/HelperManager.cs b/QuestionableCompanion/QuestionableCompanion.Services/HelperManager.cs index dc95cc5..3f9e1c9 100644 --- a/QuestionableCompanion/QuestionableCompanion.Services/HelperManager.cs +++ b/QuestionableCompanion/QuestionableCompanion.Services/HelperManager.cs @@ -4,8 +4,12 @@ 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; @@ -33,13 +37,17 @@ public class HelperManager : IDisposable 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) + 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; @@ -51,7 +59,9 @@ public class HelperManager : IDisposable 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; @@ -95,56 +105,236 @@ public class HelperManager : IDisposable 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 worldSheet = Plugin.DataManager.GetExcelSheet(); + 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); - if (availableHelpers.Count == 0) + List<(string Name, ushort WorldId)> helpersToInvite = new List<(string, ushort)>(); + if (configuration.HelperSelection == HelperSelectionMode.Auto) { - 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"); + 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 excelSheet = Plugin.DataManager.GetExcelSheet(); + 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 { - log.Information($"[HelperManager] Inviting {availableHelpers.Count} AUTO-DISCOVERED helper(s)..."); DisbandParty(); await Task.Delay(500); - foreach (var (name, worldId) in availableHelpers) + } + foreach (var (name, worldId) in helpersToInvite) + { + if (string.IsNullOrEmpty(name) || worldId == 0) { - 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) { - log.Warning($"[HelperManager] Invalid helper: {name}@{worldId}"); + 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] 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) + log.Information("[HelperManager] " + name + " is ready! Sending invite..."); + if (partyInviteService.InviteToParty(name, worldId)) { - await Task.Delay(100); - } - if (!helperReadyStatus.GetValueOrDefault((name, worldId), defaultValue: false)) - { - log.Warning("[HelperManager] Timeout waiting for " + name + " to be ready!"); + log.Information("[HelperManager] Successfully invited " + name); } 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); + log.Error("[HelperManager] Failed to invite " + name); } + await Task.Delay(500); } } } @@ -153,7 +343,18 @@ public class HelperManager : IDisposable public List<(string Name, ushort WorldId)> GetAvailableHelpers() { - return new List<(string, ushort)>(availableHelpers); + 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() @@ -211,7 +412,7 @@ public class HelperManager : IDisposable private void OnDutyEnter() { - log.Information("[HelperManager] Entered duty"); + log.Debug("[HelperManager] Entered duty"); if (!configuration.IsHighLevelHelper) { return; @@ -345,14 +546,34 @@ public class HelperManager : IDisposable 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]) + bool requesterInParty = false; + if (partyList != null) { - isInDuty = true; - log.Information("[HelperManager] Currently in duty, notifying quester..."); - crossProcessIPC.NotifyHelperInDuty(localName, localWorldId); + 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); + } } } } @@ -427,6 +648,36 @@ public class HelperManager : IDisposable 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; diff --git a/QuestionableCompanion/QuestionableCompanion.Services/LANHelperClient.cs b/QuestionableCompanion/QuestionableCompanion.Services/LANHelperClient.cs new file mode 100644 index 0000000..b54ba58 --- /dev/null +++ b/QuestionableCompanion/QuestionableCompanion.Services/LANHelperClient.cs @@ -0,0 +1,489 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Plugin.Services; +using Newtonsoft.Json; +using QuestionableCompanion.Models; + +namespace QuestionableCompanion.Services; + +public class LANHelperClient : IDisposable +{ + public class ChauffeurMessageEventArgs : EventArgs + { + public LANMessageType Type { get; } + + public LANChauffeurResponse Data { get; } + + public ChauffeurMessageEventArgs(LANMessageType type, LANChauffeurResponse data) + { + Type = type; + Data = data; + } + } + + private readonly IPluginLog log; + + private readonly IClientState clientState; + + private readonly IFramework framework; + + private readonly Configuration config; + + private readonly Dictionary activeConnections = new Dictionary(); + + private readonly Dictionary discoveredHelpers = new Dictionary(); + + private CancellationTokenSource? cancellationTokenSource; + + private string cachedPlayerName = string.Empty; + + private ushort cachedWorldId; + + public IReadOnlyList DiscoveredHelpers => discoveredHelpers.Values.ToList(); + + public event EventHandler? OnChauffeurMessageReceived; + + public LANHelperClient(IPluginLog log, IClientState clientState, IFramework framework, Configuration config) + { + this.log = log; + this.clientState = clientState; + this.framework = framework; + this.config = config; + framework.Update += OnFrameworkUpdate; + } + + private void OnFrameworkUpdate(IFramework fw) + { + IPlayerCharacter player = clientState.LocalPlayer; + if (player != null) + { + cachedPlayerName = player.Name.ToString(); + cachedWorldId = (ushort)player.HomeWorld.RowId; + } + } + + public async Task Initialize() + { + if (!config.EnableLANHelpers) + { + return; + } + cancellationTokenSource = new CancellationTokenSource(); + log.Information("[LANClient] Initializing LAN Helper Client..."); + foreach (string ip in config.LANHelperIPs) + { + await ConnectToHelperAsync(ip); + } + Task.Run(() => HeartbeatMonitorAsync(cancellationTokenSource.Token)); + } + + private async Task HeartbeatMonitorAsync(CancellationToken cancellationToken) + { + log.Information("[LANClient] Heartbeat monitor started (30s interval)"); + while (!cancellationToken.IsCancellationRequested) + { + try + { + await Task.Delay(30000, cancellationToken); + foreach (string ip in config.LANHelperIPs.ToList()) + { + if (!activeConnections.ContainsKey(ip) || !activeConnections[ip].Connected) + { + log.Debug("[LANClient] Heartbeat: " + ip + " disconnected, reconnecting..."); + await ConnectToHelperAsync(ip); + continue; + } + LANHeartbeat heartbeatData = new LANHeartbeat + { + ClientName = (string.IsNullOrEmpty(cachedPlayerName) ? "Unknown" : cachedPlayerName), + ClientWorldId = cachedWorldId, + ClientRole = (config.IsQuester ? "Quester" : "Helper") + }; + await SendMessageAsync(ip, new LANMessage(LANMessageType.HEARTBEAT, heartbeatData)); + log.Debug($"[LANClient] Heartbeat sent to {ip} (as {heartbeatData.ClientName}@{heartbeatData.ClientWorldId})"); + } + foreach (LANHelperInfo helper in discoveredHelpers.Values.ToList()) + { + if (!string.IsNullOrEmpty(helper.IPAddress)) + { + LANHeartbeat heartbeatData = new LANHeartbeat + { + ClientName = (string.IsNullOrEmpty(cachedPlayerName) ? "Unknown" : cachedPlayerName), + ClientWorldId = cachedWorldId, + ClientRole = (config.IsQuester ? "Quester" : "Helper") + }; + await SendMessageAsync(helper.IPAddress, new LANMessage(LANMessageType.HEARTBEAT, heartbeatData)); + log.Information($"[LANClient] Heartbeat sent to discovered helper {helper.Name}@{helper.IPAddress} (as {heartbeatData.ClientName}, Role={heartbeatData.ClientRole})"); + } + } + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex2) + { + log.Error("[LANClient] Heartbeat monitor error: " + ex2.Message); + } + } + log.Information("[LANClient] Heartbeat monitor stopped"); + } + + public async Task ConnectToHelperAsync(string ipAddress) + { + if (activeConnections.ContainsKey(ipAddress)) + { + log.Debug("[LANClient] Already connected to " + ipAddress); + return true; + } + try + { + log.Information($"[LANClient] Connecting to helper at {ipAddress}:{config.LANServerPort}..."); + TcpClient client = new TcpClient(); + await client.ConnectAsync(ipAddress, config.LANServerPort); + activeConnections[ipAddress] = client; + log.Information("[LANClient] ✓ Connected to " + ipAddress); + LANHelperStatusResponse statusResponse = await RequestHelperStatusAsync(ipAddress); + if (statusResponse != null) + { + discoveredHelpers[ipAddress] = new LANHelperInfo + { + Name = statusResponse.Name, + WorldId = statusResponse.WorldId, + IPAddress = ipAddress, + Status = statusResponse.Status, + LastSeen = DateTime.Now + }; + log.Information($"[LANClient] Helper discovered: {statusResponse.Name} ({statusResponse.Status})"); + } + Task.Run(() => ListenToHelperAsync(ipAddress, client), cancellationTokenSource.Token); + return true; + } + catch (Exception ex) + { + log.Error("[LANClient] Failed to connect to " + ipAddress + ": " + ex.Message); + return false; + } + } + + private async Task ListenToHelperAsync(string ipAddress, TcpClient client) + { + try + { + using NetworkStream stream = client.GetStream(); + using StreamReader reader = new StreamReader(stream, Encoding.UTF8); + while (client.Connected && !cancellationTokenSource.Token.IsCancellationRequested) + { + string line = await reader.ReadLineAsync(cancellationTokenSource.Token); + if (string.IsNullOrEmpty(line)) + { + break; + } + try + { + LANMessage message = JsonConvert.DeserializeObject(line); + if (message != null) + { + HandleHelperMessage(ipAddress, message); + } + } + catch (JsonException ex) + { + log.Error("[LANClient] Invalid message from " + ipAddress + ": " + ex.Message); + } + } + } + catch (Exception ex2) + { + log.Error("[LANClient] Connection to " + ipAddress + " lost: " + ex2.Message); + } + finally + { + log.Information("[LANClient] Disconnected from " + ipAddress); + activeConnections.Remove(ipAddress); + client.Close(); + } + } + + private void HandleHelperMessage(string ipAddress, LANMessage message) + { + log.Debug($"[LANClient] Received {message.Type} from {ipAddress}"); + switch (message.Type) + { + case LANMessageType.HELPER_STATUS: + { + LANHelperStatusResponse status = message.GetData(); + if (status == null) + { + break; + } + if (!discoveredHelpers.ContainsKey(ipAddress)) + { + log.Information($"[LANClient] New helper discovered via status: {status.Name}@{status.WorldId} ({ipAddress})"); + discoveredHelpers[ipAddress] = new LANHelperInfo + { + Name = status.Name, + WorldId = status.WorldId, + IPAddress = ipAddress, + Status = status.Status, + LastSeen = DateTime.Now + }; + } + else + { + discoveredHelpers[ipAddress].Status = status.Status; + discoveredHelpers[ipAddress].LastSeen = DateTime.Now; + if (discoveredHelpers[ipAddress].Name != status.Name) + { + discoveredHelpers[ipAddress].Name = status.Name; + discoveredHelpers[ipAddress].WorldId = status.WorldId; + } + } + break; + } + case LANMessageType.INVITE_ACCEPTED: + log.Information("[LANClient] ✓ Helper at " + ipAddress + " accepted invite"); + break; + case LANMessageType.FOLLOW_STARTED: + log.Information("[LANClient] ✓ Helper at " + ipAddress + " started following"); + break; + case LANMessageType.FOLLOW_ARRIVED: + log.Information("[LANClient] ✓ Helper at " + ipAddress + " arrived at destination"); + break; + case LANMessageType.HELPER_READY: + log.Information("[LANClient] ✓ Helper at " + ipAddress + " is ready"); + break; + case LANMessageType.HELPER_IN_PARTY: + log.Information("[LANClient] ✓ Helper at " + ipAddress + " joined party"); + break; + case LANMessageType.HELPER_IN_DUTY: + log.Information("[LANClient] ✓ Helper at " + ipAddress + " entered duty"); + break; + case LANMessageType.CHAUFFEUR_HELPER_READY_FOR_MOUNT: + { + LANChauffeurResponse readyData = message.GetData(); + if (readyData != null) + { + log.Information($"[LANClient] Received Chauffeur Mount Ready from {readyData.QuesterName}@{readyData.QuesterWorldId}"); + this.OnChauffeurMessageReceived?.Invoke(this, new ChauffeurMessageEventArgs(LANMessageType.CHAUFFEUR_HELPER_READY_FOR_MOUNT, readyData)); + } + break; + } + case LANMessageType.CHAUFFEUR_HELPER_ARRIVED_DEST: + { + LANChauffeurResponse arrivedData = message.GetData(); + if (arrivedData != null) + { + log.Information($"[LANClient] Received Chauffeur Arrived from {arrivedData.QuesterName}@{arrivedData.QuesterWorldId}"); + this.OnChauffeurMessageReceived?.Invoke(this, new ChauffeurMessageEventArgs(LANMessageType.CHAUFFEUR_HELPER_ARRIVED_DEST, arrivedData)); + } + break; + } + case LANMessageType.INVITE_NOTIFICATION: + case LANMessageType.DUTY_COMPLETE: + case LANMessageType.FOLLOW_COMMAND: + case LANMessageType.CHAUFFEUR_PICKUP_REQUEST: + break; + } + } + + public async Task RequestHelperAsync(string ipAddress, string dutyName = "") + { + IPlayerCharacter player = clientState.LocalPlayer; + if (player == null) + { + return false; + } + LANHelperRequest request = new LANHelperRequest + { + QuesterName = player.Name.ToString(), + QuesterWorldId = (ushort)player.HomeWorld.RowId, + DutyName = dutyName + }; + return await SendMessageAsync(ipAddress, new LANMessage(LANMessageType.REQUEST_HELPER, request)); + } + + public async Task RequestHelperStatusAsync(string ipAddress) + { + if (!(await SendMessageAsync(ipAddress, new LANMessage(LANMessageType.HELPER_STATUS)))) + { + return null; + } + await Task.Delay(500); + if (discoveredHelpers.TryGetValue(ipAddress, out LANHelperInfo helper)) + { + return new LANHelperStatusResponse + { + Name = helper.Name, + WorldId = helper.WorldId, + Status = helper.Status + }; + } + return null; + } + + public async Task SendFollowCommandAsync(string ipAddress, float x, float y, float z, uint territoryId) + { + LANFollowCommand followCmd = new LANFollowCommand + { + X = x, + Y = y, + Z = z, + TerritoryId = territoryId + }; + log.Information($"[LANClient] Sending follow command to {ipAddress}: ({x:F2}, {y:F2}, {z:F2})"); + return await SendMessageAsync(ipAddress, new LANMessage(LANMessageType.FOLLOW_COMMAND, followCmd)); + } + + public async Task NotifyInviteSentAsync(string ipAddress, string helperName) + { + log.Information("[LANClient] Notifying " + ipAddress + " of invite sent to " + helperName); + return await SendMessageAsync(ipAddress, new LANMessage(LANMessageType.INVITE_NOTIFICATION, helperName)); + } + + private async Task SendMessageAsync(string ipAddress, LANMessage message) + { + _ = 2; + try + { + if (!activeConnections.ContainsKey(ipAddress) && !(await ConnectToHelperAsync(ipAddress))) + { + return false; + } + TcpClient client = activeConnections[ipAddress]; + if (!client.Connected) + { + log.Warning("[LANClient] Not connected to " + ipAddress + ", reconnecting..."); + activeConnections.Remove(ipAddress); + if (!(await ConnectToHelperAsync(ipAddress))) + { + return false; + } + client = activeConnections[ipAddress]; + } + string json = JsonConvert.SerializeObject(message); + byte[] bytes = Encoding.UTF8.GetBytes(json + "\n"); + await client.GetStream().WriteAsync(bytes, 0, bytes.Length); + log.Debug($"[LANClient] Sent {message.Type} to {ipAddress}"); + return true; + } + catch (Exception ex) + { + log.Error("[LANClient] Failed to send message to " + ipAddress + ": " + ex.Message); + return false; + } + } + + public async Task ScanNetworkAsync(int timeoutSeconds = 5) + { + log.Information($"[LANClient] \ud83d\udce1 Scanning network for helpers (timeout: {timeoutSeconds}s)..."); + int foundCount = 0; + try + { + using UdpClient udpClient = new UdpClient(47789); + udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, optionValue: true); + CancellationTokenSource cancellation = new CancellationTokenSource(); + cancellation.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds)); + while (!cancellation.Token.IsCancellationRequested) + { + try + { + UdpReceiveResult result = await udpClient.ReceiveAsync(cancellation.Token); + dynamic announcement = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(result.Buffer)); + if (announcement?.Type == "HELPER_ANNOUNCE") + { + string helperIP = result.RemoteEndPoint.Address.ToString(); + string helperName = (string)announcement.Name; + _ = (int)announcement.Port; + log.Information("[LANClient] ✓ Found helper: " + helperName + " at " + helperIP); + if (!config.LANHelperIPs.Contains(helperIP)) + { + config.LANHelperIPs.Add(helperIP); + config.Save(); + log.Information("[LANClient] → Added " + helperIP + " to configuration"); + foundCount++; + } + await ConnectToHelperAsync(helperIP); + } + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex2) + { + log.Debug("[LANClient] Scan error: " + ex2.Message); + } + } + } + catch (Exception ex3) + { + log.Error("[LANClient] Network scan failed: " + ex3.Message); + } + if (foundCount > 0) + { + log.Information($"[LANClient] ✓ Scan complete: Found {foundCount} new helper(s)"); + } + else + { + log.Information("[LANClient] Scan complete: No new helpers found"); + } + return foundCount; + } + + public LANHelperInfo? GetFirstAvailableHelper() + { + return (from h in discoveredHelpers.Values + where h.Status == LANHelperStatus.Available + orderby h.LastSeen + select h).FirstOrDefault(); + } + + public async Task SendChauffeurSummonAsync(string ipAddress, LANChauffeurSummon summonData) + { + log.Information("[LANClient] *** SENDING CHAUFFEUR_PICKUP_REQUEST to " + ipAddress + " ***"); + log.Information($"[LANClient] Summon data: Quester={summonData.QuesterName}@{summonData.QuesterWorldId}, Zone={summonData.ZoneId}"); + LANMessage message = new LANMessage(LANMessageType.CHAUFFEUR_PICKUP_REQUEST, summonData); + bool num = await SendMessageAsync(ipAddress, message); + if (num) + { + log.Information("[LANClient] ✓ CHAUFFEUR_PICKUP_REQUEST sent successfully to " + ipAddress); + } + else + { + log.Error("[LANClient] ✗ FAILED to send CHAUFFEUR_PICKUP_REQUEST to " + ipAddress); + } + return num; + } + + public void DisconnectAll() + { + log.Information("[LANClient] Disconnecting from all helpers..."); + foreach (KeyValuePair kvp in activeConnections.ToList()) + { + try + { + SendMessageAsync(kvp.Key, new LANMessage(LANMessageType.DISCONNECT)).Wait(1000); + kvp.Value.Close(); + } + catch + { + } + } + activeConnections.Clear(); + discoveredHelpers.Clear(); + } + + public void Dispose() + { + cancellationTokenSource?.Cancel(); + DisconnectAll(); + } +} diff --git a/QuestionableCompanion/QuestionableCompanion.Services/LANHelperServer.cs b/QuestionableCompanion/QuestionableCompanion.Services/LANHelperServer.cs new file mode 100644 index 0000000..1f80d62 --- /dev/null +++ b/QuestionableCompanion/QuestionableCompanion.Services/LANHelperServer.cs @@ -0,0 +1,577 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Numerics; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Plugin.Services; +using Newtonsoft.Json; +using QuestionableCompanion.Models; + +namespace QuestionableCompanion.Services; + +public class LANHelperServer : IDisposable +{ + private readonly IPluginLog log; + + private readonly IClientState clientState; + + private readonly IFramework framework; + + private readonly Configuration config; + + private readonly PartyInviteAutoAccept partyInviteAutoAccept; + + private readonly ICommandManager commandManager; + + private readonly Plugin plugin; + + private TcpListener? listener; + + private CancellationTokenSource? cancellationTokenSource; + + private readonly List connectedClients = new List(); + + private readonly Dictionary activeConnections = new Dictionary(); + + private readonly Dictionary knownQuesters = new Dictionary(); + + private bool isRunning; + + private string? cachedPlayerName; + + private ushort cachedWorldId; + + private DateTime lastCacheRefresh = DateTime.MinValue; + + private const int CACHE_REFRESH_SECONDS = 30; + + public bool IsRunning => isRunning; + + public int ConnectedClientCount => connectedClients.Count; + + public List GetConnectedClientNames() + { + DateTime now = DateTime.Now; + foreach (string s in (from kvp in knownQuesters + where (now - kvp.Value).TotalSeconds > 60.0 + select kvp.Key).ToList()) + { + knownQuesters.Remove(s); + } + return knownQuesters.Keys.ToList(); + } + + public LANHelperServer(IPluginLog log, IClientState clientState, IFramework framework, Configuration config, PartyInviteAutoAccept partyInviteAutoAccept, ICommandManager commandManager, Plugin plugin) + { + this.log = log; + this.clientState = clientState; + this.framework = framework; + this.config = config; + this.partyInviteAutoAccept = partyInviteAutoAccept; + this.commandManager = commandManager; + this.plugin = plugin; + } + + public void Start() + { + if (isRunning) + { + log.Warning("[LANServer] Server already running"); + return; + } + Task.Run(async delegate + { + try + { + framework.Update += OnFrameworkUpdate; + int retries = 5; + while (retries > 0) + { + try + { + listener = new TcpListener(IPAddress.Any, config.LANServerPort); + listener.Start(); + } + catch (SocketException ex) when (ex.SocketErrorCode == SocketError.AddressAlreadyInUse) + { + retries--; + if (retries == 0) + { + throw; + } + log.Warning($"[LANServer] Port {config.LANServerPort} in use, retrying in 1s... ({retries} retries left)"); + await Task.Delay(1000); + continue; + } + break; + } + cancellationTokenSource = new CancellationTokenSource(); + isRunning = true; + log.Information("[LANServer] ===== LAN HELPER SERVER STARTED (v2-DEBUG) ====="); + log.Information($"[LANServer] Listening on port {config.LANServerPort}"); + log.Information("[LANServer] Waiting for player info cache... (via framework update)"); + Task.Run(() => AcceptClientsAsync(cancellationTokenSource.Token)); + Task.Run(() => BroadcastPresenceAsync(cancellationTokenSource.Token)); + } + catch (Exception ex2) + { + log.Error("[LANServer] Failed to start server: " + ex2.Message); + isRunning = false; + framework.Update -= OnFrameworkUpdate; + } + }); + } + + private void OnFrameworkUpdate(IFramework framework) + { + if (isRunning) + { + DateTime now = DateTime.Now; + if ((now - lastCacheRefresh).TotalSeconds >= 30.0) + { + log.Debug($"[LANServer] Framework.Update triggered cache refresh (last: {(now - lastCacheRefresh).TotalSeconds:F1}s ago)"); + RefreshPlayerCache(); + } + } + } + + private void RefreshPlayerCache() + { + try + { + log.Debug("[LANServer] RefreshPlayerCache called"); + IPlayerCharacter player = clientState.LocalPlayer; + if (player != null) + { + string newName = player.Name.ToString(); + ushort newWorldId = (ushort)player.HomeWorld.RowId; + if (cachedPlayerName != newName || cachedWorldId != newWorldId) + { + if (cachedPlayerName == null) + { + log.Information($"[LANServer] ✓ Player info cached: {newName}@{newWorldId}"); + } + else + { + log.Information($"[LANServer] Player info updated: {newName}@{newWorldId}"); + } + cachedPlayerName = newName; + cachedWorldId = newWorldId; + } + lastCacheRefresh = DateTime.Now; + } + else + { + log.Warning("[LANServer] RefreshPlayerCache: LocalPlayer is NULL!"); + } + lastCacheRefresh = DateTime.Now; + } + catch (Exception ex) + { + log.Error("[LANServer] RefreshPlayerCache ERROR: " + ex.Message); + log.Error("[LANServer] Stack: " + ex.StackTrace); + } + } + + private async Task BroadcastPresenceAsync(CancellationToken cancellationToken) + { + _ = 3; + try + { + using UdpClient udpClient = new UdpClient(); + udpClient.EnableBroadcast = true; + IPEndPoint broadcastEndpoint = new IPEndPoint(IPAddress.Broadcast, 47789); + if (cachedPlayerName == null) + { + return; + } + string json = JsonConvert.SerializeObject(new + { + Type = "HELPER_ANNOUNCE", + Name = cachedPlayerName, + WorldId = cachedWorldId, + Port = config.LANServerPort + }); + byte[] bytes = Encoding.UTF8.GetBytes(json); + for (int i = 0; i < 3; i++) + { + await udpClient.SendAsync(bytes, bytes.Length, broadcastEndpoint); + log.Information($"[LANServer] \ud83d\udce1 Broadcast announcement sent ({i + 1}/3)"); + await Task.Delay(500, cancellationToken); + } + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(30000, cancellationToken); + await udpClient.SendAsync(bytes, bytes.Length, broadcastEndpoint); + log.Debug("[LANServer] Broadcast presence updated"); + } + } + catch (OperationCanceledException) + { + } + catch (Exception ex2) + { + log.Error("[LANServer] UDP broadcast error: " + ex2.Message); + } + } + + public void Stop() + { + if (!isRunning && listener == null) + { + return; + } + log.Information("[LANServer] Stopping server..."); + isRunning = false; + cancellationTokenSource?.Cancel(); + framework.Update -= OnFrameworkUpdate; + lock (connectedClients) + { + foreach (TcpClient client in connectedClients.ToList()) + { + try + { + if (client.Connected) + { + try + { + NetworkStream stream = client.GetStream(); + if (stream.CanWrite) + { + string json = JsonConvert.SerializeObject(new LANMessage(LANMessageType.DISCONNECT)); + byte[] bytes = Encoding.UTF8.GetBytes(json + "\n"); + stream.Write(bytes, 0, bytes.Length); + } + } + catch + { + } + } + client.Close(); + client.Dispose(); + } + catch + { + } + } + connectedClients.Clear(); + } + try + { + listener?.Stop(); + } + catch (Exception ex) + { + log.Warning("[LANServer] Error stopping listener: " + ex.Message); + } + listener = null; + log.Information("[LANServer] Server stopped"); + } + + private async Task AcceptClientsAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested && isRunning) + { + try + { + TcpClient client = await listener.AcceptTcpClientAsync(cancellationToken); + string clientIP = ((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString(); + log.Information("[LANServer] Client connected from " + clientIP); + lock (connectedClients) + { + connectedClients.Add(client); + } + Task.Run(() => HandleClientAsync(client, cancellationToken), cancellationToken); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex2) + { + log.Error("[LANServer] Error accepting client: " + ex2.Message); + await Task.Delay(1000, cancellationToken); + } + } + } + + private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken) + { + string clientIP = ((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString(); + try + { + using NetworkStream stream = client.GetStream(); + using StreamReader reader = new StreamReader(stream, Encoding.UTF8); + while (!cancellationToken.IsCancellationRequested && client.Connected) + { + string line = await reader.ReadLineAsync(cancellationToken); + if (string.IsNullOrEmpty(line)) + { + break; + } + try + { + LANMessage message = JsonConvert.DeserializeObject(line); + if (message != null) + { + await HandleMessageAsync(client, message, clientIP); + } + } + catch (JsonException ex) + { + log.Error("[LANServer] Invalid message from " + clientIP + ": " + ex.Message); + } + } + } + catch (Exception ex2) + { + log.Error("[LANServer] Client " + clientIP + " error: " + ex2.Message); + } + finally + { + log.Information("[LANServer] Client " + clientIP + " disconnected"); + lock (connectedClients) + { + connectedClients.Remove(client); + } + client.Close(); + } + } + + private async Task HandleMessageAsync(TcpClient client, LANMessage message, string clientIP) + { + log.Debug($"[LANServer] Received {message.Type} from {clientIP}"); + switch (message.Type) + { + case LANMessageType.REQUEST_HELPER: + await HandleHelperRequest(client, message); + break; + case LANMessageType.HELPER_STATUS: + await HandleStatusRequest(client); + break; + case LANMessageType.INVITE_NOTIFICATION: + await HandleInviteNotification(client, message); + break; + case LANMessageType.FOLLOW_COMMAND: + await HandleFollowCommand(client, message); + break; + case LANMessageType.CHAUFFEUR_PICKUP_REQUEST: + await HandleChauffeurSummon(message); + break; + case LANMessageType.HEARTBEAT: + { + LANHeartbeat heartbeatData = message.GetData(); + if (heartbeatData != null && heartbeatData.ClientRole == "Quester" && !string.IsNullOrEmpty(heartbeatData.ClientName)) + { + string questerKey = $"{heartbeatData.ClientName}@{heartbeatData.ClientWorldId}"; + knownQuesters[questerKey] = DateTime.Now; + } + SendMessage(client, new LANMessage(LANMessageType.HEARTBEAT)); + break; + } + default: + log.Debug($"[LANServer] Unhandled message type: {message.Type}"); + break; + } + } + + private async Task HandleHelperRequest(TcpClient client, LANMessage message) + { + LANHelperRequest request = message.GetData(); + if (request != null) + { + log.Information("[LANServer] Helper requested by " + request.QuesterName + " for duty: " + request.DutyName); + await SendCurrentStatus(client); + partyInviteAutoAccept.EnableForQuester(request.QuesterName); + log.Information("[LANServer] Auto-accept enabled for " + request.QuesterName); + } + } + + private async Task HandleStatusRequest(TcpClient client) + { + await SendCurrentStatus(client); + } + + private async Task SendCurrentStatus(TcpClient client) + { + try + { + log.Debug("[LANServer] SendCurrentStatus: Start"); + if (cachedPlayerName == null) + { + log.Warning("[LANServer] SendCurrentStatus: Player info not cached! Sending NotReady status."); + LANHelperStatusResponse notReadyStatus = new LANHelperStatusResponse + { + Name = "Unknown", + WorldId = 0, + Status = LANHelperStatus.Offline, + CurrentActivity = "Waiting for character login..." + }; + SendMessage(client, new LANMessage(LANMessageType.HELPER_STATUS, notReadyStatus)); + return; + } + log.Debug($"[LANServer] SendCurrentStatus: Cached Name={cachedPlayerName}, World={cachedWorldId}"); + LANHelperStatusResponse status = new LANHelperStatusResponse + { + Name = cachedPlayerName, + WorldId = cachedWorldId, + Status = LANHelperStatus.Available, + CurrentActivity = "Ready" + }; + log.Debug("[LANServer] SendCurrentStatus: Status object created"); + LANMessage msg = new LANMessage(LANMessageType.HELPER_STATUS, status); + log.Debug("[LANServer] SendCurrentStatus: LANMessage created"); + SendMessage(client, msg); + log.Debug("[LANServer] SendCurrentStatus: Message sent"); + } + catch (Exception ex) + { + log.Error("[LANServer] SendCurrentStatus CRASH: " + ex.Message); + log.Error("[LANServer] Stack: " + ex.StackTrace); + } + } + + private async Task HandleInviteNotification(TcpClient client, LANMessage message) + { + string questerName = message.GetData(); + log.Information("[LANServer] Invite notification from " + questerName); + SendMessage(client, new LANMessage(LANMessageType.INVITE_ACCEPTED)); + } + + private async Task HandleFollowCommand(TcpClient client, LANMessage message) + { + LANFollowCommand followCmd = message.GetData(); + if (followCmd != null) + { + ChauffeurModeService chauffeurSvc = plugin.GetChauffeurMode(); + if (chauffeurSvc == null) + { + log.Warning("[LANServer] No ChauffeurModeService available for position update"); + return; + } + if (chauffeurSvc.IsTransportingQuester) + { + log.Debug("[LANServer] Ignoring FOLLOW_COMMAND - Chauffeur Mode is actively transporting"); + return; + } + string questerName = config.AssignedQuesterForFollowing ?? "LAN Quester"; + chauffeurSvc.UpdateQuesterPositionFromLAN(followCmd.X, followCmd.Y, followCmd.Z, followCmd.TerritoryId, questerName); + log.Debug($"[LANServer] Updated quester position: ({followCmd.X:F2}, {followCmd.Y:F2}, {followCmd.Z:F2}) Zone={followCmd.TerritoryId}"); + SendMessage(client, new LANMessage(LANMessageType.FOLLOW_STARTED)); + } + } + + private void SendMessage(TcpClient client, LANMessage message) + { + try + { + if (client.Connected) + { + string json = JsonConvert.SerializeObject(message); + byte[] bytes = Encoding.UTF8.GetBytes(json + "\n"); + client.GetStream().Write(bytes, 0, bytes.Length); + } + } + catch (Exception ex) + { + log.Error("[LANServer] Failed to send message: " + ex.Message); + } + } + + public void BroadcastMessage(LANMessage message) + { + lock (connectedClients) + { + foreach (TcpClient client in connectedClients.ToList()) + { + SendMessage(client, message); + } + } + } + + private async Task HandleChauffeurSummon(LANMessage message) + { + LANChauffeurSummon summonData = message.GetData(); + if (summonData == null) + { + log.Error("[LANServer] HandleChauffeurSummon: Failed to deserialize summon data!"); + return; + } + log.Information("[LANServer] ========================================="); + log.Information("[LANServer] *** CHAUFFEUR PICKUP REQUEST RECEIVED ***"); + log.Information("[LANServer] ========================================="); + log.Information($"[LANServer] Quester: {summonData.QuesterName}@{summonData.QuesterWorldId}"); + log.Information($"[LANServer] Zone: {summonData.ZoneId}"); + log.Information($"[LANServer] Target: ({summonData.TargetX:F2}, {summonData.TargetY:F2}, {summonData.TargetZ:F2})"); + log.Information($"[LANServer] AttuneAetheryte: {summonData.IsAttuneAetheryte}"); + ChauffeurModeService chauffeur = plugin.GetChauffeurMode(); + if (chauffeur != null) + { + Vector3 targetPos = new Vector3(summonData.TargetX, summonData.TargetY, summonData.TargetZ); + Vector3 questerPos = new Vector3(summonData.QuesterX, summonData.QuesterY, summonData.QuesterZ); + log.Information("[LANServer] Calling ChauffeurModeService.StartHelperWorkflow..."); + await framework.RunOnFrameworkThread(delegate + { + chauffeur.StartHelperWorkflow(summonData.QuesterName, summonData.QuesterWorldId, summonData.ZoneId, targetPos, questerPos, summonData.IsAttuneAetheryte); + }); + log.Information("[LANServer] StartHelperWorkflow dispatched to framework thread"); + } + else + { + log.Error("[LANServer] ChauffeurModeService is null! Cannot start helper workflow."); + } + } + + public void SendChauffeurMountReady(string questerName, ushort questerWorldId) + { + LANChauffeurResponse response = new LANChauffeurResponse + { + QuesterName = questerName, + QuesterWorldId = questerWorldId + }; + LANMessage message = new LANMessage(LANMessageType.CHAUFFEUR_HELPER_READY_FOR_MOUNT, response); + log.Information($"[LANServer] Sending Chauffeur Mount Ready to connected clients for {questerName}@{questerWorldId}"); + lock (connectedClients) + { + foreach (TcpClient client in connectedClients.ToList()) + { + if (client.Connected) + { + SendMessage(client, message); + } + } + } + } + + public void SendChauffeurArrived(string questerName, ushort questerWorldId) + { + LANChauffeurResponse response = new LANChauffeurResponse + { + QuesterName = questerName, + QuesterWorldId = questerWorldId + }; + LANMessage message = new LANMessage(LANMessageType.CHAUFFEUR_HELPER_ARRIVED_DEST, response); + log.Information($"[LANServer] Sending Chauffeur Arrived to connected clients for {questerName}@{questerWorldId}"); + lock (connectedClients) + { + foreach (TcpClient client in connectedClients.ToList()) + { + if (client.Connected) + { + SendMessage(client, message); + } + } + } + } + + public void Dispose() + { + Stop(); + } +} diff --git a/QuestionableCompanion/QuestionableCompanion.Services/MultiClientIPC.cs b/QuestionableCompanion/QuestionableCompanion.Services/MultiClientIPC.cs index 5ec8fee..eae4473 100644 --- a/QuestionableCompanion/QuestionableCompanion.Services/MultiClientIPC.cs +++ b/QuestionableCompanion/QuestionableCompanion.Services/MultiClientIPC.cs @@ -21,6 +21,8 @@ public class MultiClientIPC : IDisposable private readonly ICallGateProvider passengerMountedProvider; + private readonly ICallGateProvider helperStatusProvider; + private readonly ICallGateSubscriber requestHelperSubscriber; private readonly ICallGateSubscriber dismissHelperSubscriber; @@ -31,6 +33,8 @@ public class MultiClientIPC : IDisposable private readonly ICallGateSubscriber passengerMountedSubscriber; + private readonly ICallGateSubscriber helperStatusSubscriber; + public event Action? OnHelperRequested; public event Action? OnHelperDismissed; @@ -41,6 +45,8 @@ public class MultiClientIPC : IDisposable public event Action? OnPassengerMounted; + public event Action? OnHelperStatusUpdate; + public MultiClientIPC(IDalamudPluginInterface pluginInterface, IPluginLog log) { this.pluginInterface = pluginInterface; @@ -50,11 +56,13 @@ public class MultiClientIPC : IDisposable helperAvailableProvider = pluginInterface.GetIpcProvider("QSTCompanion.HelperAvailable"); chatMessageProvider = pluginInterface.GetIpcProvider("QSTCompanion.ChatMessage"); passengerMountedProvider = pluginInterface.GetIpcProvider("QSTCompanion.PassengerMounted"); + helperStatusProvider = pluginInterface.GetIpcProvider("QSTCompanion.HelperStatus"); requestHelperSubscriber = pluginInterface.GetIpcSubscriber("QSTCompanion.RequestHelper"); dismissHelperSubscriber = pluginInterface.GetIpcSubscriber("QSTCompanion.DismissHelper"); helperAvailableSubscriber = pluginInterface.GetIpcSubscriber("QSTCompanion.HelperAvailable"); chatMessageSubscriber = pluginInterface.GetIpcSubscriber("QSTCompanion.ChatMessage"); passengerMountedSubscriber = pluginInterface.GetIpcSubscriber("QSTCompanion.PassengerMounted"); + helperStatusSubscriber = pluginInterface.GetIpcSubscriber("QSTCompanion.HelperStatus"); requestHelperProvider.RegisterFunc(delegate(string name, ushort worldId) { OnRequestHelperReceived(name, worldId); @@ -80,6 +88,11 @@ public class MultiClientIPC : IDisposable OnPassengerMountedReceived(questerName, questerWorld); return (object?)null; }); + helperStatusProvider.RegisterFunc(delegate(string helperName, ushort helperWorld, string status) + { + OnHelperStatusReceived(helperName, helperWorld, status); + return (object?)null; + }); log.Information("[MultiClientIPC] ✅ IPC initialized successfully"); } @@ -213,6 +226,32 @@ public class MultiClientIPC : IDisposable } } + public void BroadcastHelperStatus(string helperName, ushort worldId, string status) + { + try + { + log.Debug($"[MultiClientIPC] Broadcasting helper status: {helperName}@{worldId} = {status}"); + helperStatusSubscriber.InvokeFunc(helperName, worldId, status); + } + catch (Exception ex) + { + log.Error("[MultiClientIPC] Failed to broadcast helper status: " + ex.Message); + } + } + + private void OnHelperStatusReceived(string helperName, ushort helperWorld, string status) + { + try + { + log.Debug($"[MultiClientIPC] Received helper status: {helperName}@{helperWorld} = {status}"); + this.OnHelperStatusUpdate?.Invoke(helperName, helperWorld, status); + } + catch (Exception ex) + { + log.Error("[MultiClientIPC] Error handling helper status: " + ex.Message); + } + } + public void Dispose() { try @@ -221,6 +260,7 @@ public class MultiClientIPC : IDisposable dismissHelperProvider.UnregisterFunc(); helperAvailableProvider.UnregisterFunc(); chatMessageProvider.UnregisterFunc(); + helperStatusProvider.UnregisterFunc(); } catch (Exception ex) { diff --git a/QuestionableCompanion/QuestionableCompanion.Services/PartyInviteAutoAccept.cs b/QuestionableCompanion/QuestionableCompanion.Services/PartyInviteAutoAccept.cs index 8ebd8b9..eb9f48a 100644 --- a/QuestionableCompanion/QuestionableCompanion.Services/PartyInviteAutoAccept.cs +++ b/QuestionableCompanion/QuestionableCompanion.Services/PartyInviteAutoAccept.cs @@ -1,6 +1,5 @@ using System; using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; namespace QuestionableCompanion.Services; @@ -21,6 +20,8 @@ public class PartyInviteAutoAccept : IDisposable private DateTime autoAcceptUntil = DateTime.MinValue; + private bool hasLoggedAlwaysAccept; + public PartyInviteAutoAccept(IPluginLog log, IFramework framework, IGameGui gameGui, IPartyList partyList, Configuration configuration) { this.log = log; @@ -47,16 +48,50 @@ public class PartyInviteAutoAccept : IDisposable log.Information("[PartyInviteAutoAccept] Will accept ALL party invites during this time!"); } + public void EnableForQuester(string questerName) + { + shouldAutoAccept = true; + autoAcceptUntil = DateTime.Now.AddSeconds(60.0); + log.Information("[PartyInviteAutoAccept] Auto-accept enabled for quester: " + questerName); + log.Information("[PartyInviteAutoAccept] Will accept invites for 60 seconds"); + } + private unsafe void OnFrameworkUpdate(IFramework framework) { - if (!shouldAutoAccept) + bool shouldAcceptNow = false; + if (configuration.IsHighLevelHelper && configuration.AlwaysAutoAcceptInvites) { - return; + if (!hasLoggedAlwaysAccept) + { + log.Information("[PartyInviteAutoAccept] === ALWAYS AUTO-ACCEPT ENABLED ==="); + log.Information("[PartyInviteAutoAccept] Helper will continuously accept ALL party invites"); + log.Information("[PartyInviteAutoAccept] This mode is ALWAYS ON (no timeout)"); + hasLoggedAlwaysAccept = true; + } + shouldAcceptNow = true; } - if (DateTime.Now > autoAcceptUntil) + else if (shouldAutoAccept) + { + if (hasLoggedAlwaysAccept) + { + log.Information("[PartyInviteAutoAccept] Always auto-accept disabled"); + hasLoggedAlwaysAccept = false; + } + if (DateTime.Now > autoAcceptUntil) + { + shouldAutoAccept = false; + log.Information("[PartyInviteAutoAccept] Auto-accept window expired"); + return; + } + shouldAcceptNow = true; + } + else if (hasLoggedAlwaysAccept) + { + log.Information("[PartyInviteAutoAccept] Always auto-accept disabled"); + hasLoggedAlwaysAccept = false; + } + if (!shouldAcceptNow) { - shouldAutoAccept = false; - log.Information("[PartyInviteAutoAccept] Auto-accept window expired"); return; } try @@ -72,66 +107,35 @@ public class PartyInviteAutoAccept : IDisposable break; } } - if (addonPtr == IntPtr.Zero) + if (addonPtr != IntPtr.Zero) { - if (DateTime.Now.Second % 5 != 0) - { - return; - } - log.Debug($"[PartyInviteAutoAccept] Still waiting for party invite dialog... ({(autoAcceptUntil - DateTime.Now).TotalSeconds:F0}s remaining)"); - if (DateTime.Now.Second % 10 != 0) - { - return; - } - log.Warning("[PartyInviteAutoAccept] === DUMPING ALL VISIBLE ADDONS ==="); - RaptureAtkUnitManager* atkStage = RaptureAtkUnitManager.Instance(); - if (atkStage != null) - { - AtkUnitManager* unitManager = &atkStage->AtkUnitManager; - for (int j = 0; j < unitManager->AllLoadedUnitsList.Count; j++) - { - AtkUnitBase* addon = unitManager->AllLoadedUnitsList.Entries[j].Value; - if (addon != null && addon->IsVisible) - { - string name2 = addon->NameString; - log.Warning("[PartyInviteAutoAccept] Visible addon: " + name2); - } - } - } - log.Warning("[PartyInviteAutoAccept] === END ADDON DUMP ==="); - } - else - { - AtkUnitBase* addon2 = (AtkUnitBase*)addonPtr; - if (addon2 == null) + AtkUnitBase* addon = (AtkUnitBase*)addonPtr; + if (addon == null) { log.Warning("[PartyInviteAutoAccept] Addon pointer is null!"); - return; } - if (!addon2->IsVisible) + else if (addon->IsVisible) { - log.Debug("[PartyInviteAutoAccept] Addon exists but not visible yet"); - return; + AtkValue* values = stackalloc AtkValue[1]; + *values = new AtkValue + { + Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int, + Int = 0 + }; + addon->FireCallback(1u, values); + AtkValue* values2 = stackalloc AtkValue[2]; + *values2 = new AtkValue + { + Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int, + Int = 0 + }; + values2[1] = new AtkValue + { + Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.UInt, + UInt = 0u + }; + addon->FireCallback(2u, values2); } - AtkValue* values = stackalloc AtkValue[1]; - *values = new AtkValue - { - Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int, - Int = 0 - }; - addon2->FireCallback(1u, values); - AtkValue* values2 = stackalloc AtkValue[2]; - *values2 = new AtkValue - { - Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int, - Int = 0 - }; - values2[1] = new AtkValue - { - Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.UInt, - UInt = 0u - }; - addon2->FireCallback(2u, values2); } } catch (Exception ex) diff --git a/QuestionableCompanion/QuestionableCompanion.Services/QuestPreCheckService.cs b/QuestionableCompanion/QuestionableCompanion.Services/QuestPreCheckService.cs index 0b51dd4..45aa18f 100644 --- a/QuestionableCompanion/QuestionableCompanion.Services/QuestPreCheckService.cs +++ b/QuestionableCompanion/QuestionableCompanion.Services/QuestPreCheckService.cs @@ -293,6 +293,22 @@ public class QuestPreCheckService : IDisposable log.Information("[QuestPreCheck] Pre-check results cleared"); } + public void ClearCharacterData(string characterName) + { + if (questDatabase.ContainsKey(characterName)) + { + int questCount = questDatabase[characterName].Count; + questDatabase.Remove(characterName); + SaveQuestDatabase(); + log.Information($"[QuestPreCheck] Cleared {questCount} quests for {characterName}"); + } + else + { + log.Information("[QuestPreCheck] No quest data found for " + characterName); + } + lastRefreshByCharacter.Remove(characterName); + } + public void Dispose() { SaveQuestDatabase(); diff --git a/QuestionableCompanion/QuestionableCompanion.Services/QuestRotationExecutionService.cs b/QuestionableCompanion/QuestionableCompanion.Services/QuestRotationExecutionService.cs index b37ad68..1b661c2 100644 --- a/QuestionableCompanion/QuestionableCompanion.Services/QuestRotationExecutionService.cs +++ b/QuestionableCompanion/QuestionableCompanion.Services/QuestRotationExecutionService.cs @@ -538,6 +538,31 @@ public class QuestRotationExecutionService : IDisposable return false; } + public void ClearCharacterQuestData(string characterName) + { + log.Information("[QuestRotation] Clearing all quest data for " + characterName); + int questsCleared = 0; + foreach (KeyValuePair> kvp in questCompletionByCharacter.ToList()) + { + if (kvp.Value.Remove(characterName)) + { + questsCleared++; + } + if (kvp.Value.Count == 0) + { + questCompletionByCharacter.Remove(kvp.Key); + } + } + log.Information($"[QuestRotation] Removed {characterName} from {questsCleared} quests in rotation tracking"); + if (preCheckService != null) + { + preCheckService.ClearCharacterData(characterName); + log.Information("[QuestRotation] Cleared " + characterName + " data from PreCheck service"); + } + onDataChanged?.Invoke(); + log.Information("[QuestRotation] Quest data reset complete for " + characterName); + } + private void ScanAndSaveAllCompletedQuests(string characterName) { if (string.IsNullOrEmpty(characterName)) @@ -637,12 +662,19 @@ public class QuestRotationExecutionService : IDisposable { log.Warning("[ErrorRecovery] Disconnect detected for " + charToRelog); log.Information("[ErrorRecovery] Automatically relogging to " + charToRelog + "..."); - errorRecoveryService.Reset(); - currentState.Phase = RotationPhase.WaitingForCharacterLogin; - currentState.CurrentCharacter = charToRelog; - currentState.PhaseStartTime = DateTime.Now; - autoRetainerIpc.SwitchCharacter(charToRelog); - log.Information("[ErrorRecovery] Relog initiated for " + charToRelog); + if (errorRecoveryService.RequestRelog()) + { + errorRecoveryService.Reset(); + currentState.Phase = RotationPhase.WaitingForCharacterLogin; + currentState.CurrentCharacter = charToRelog; + currentState.PhaseStartTime = DateTime.Now; + log.Information("[ErrorRecovery] Relog initiated for " + charToRelog); + } + else + { + log.Error("[ErrorRecovery] Failed to request relog via AutoRetainer"); + errorRecoveryService.Reset(); + } return; } log.Warning("[ErrorRecovery] Disconnect detected but no character to relog to"); @@ -652,19 +684,12 @@ public class QuestRotationExecutionService : IDisposable { deathHandler.Update(); } - if (dungeonAutomation != null) + if (dungeonAutomation != null && !submarineManager.IsSubmarinePaused) { - if (submarineManager.IsSubmarinePaused) + dungeonAutomation.Update(); + if (isRotationActive && configuration.EnableAutoDutyUnsynced && !dungeonAutomation.IsWaitingForParty && currentState.Phase != RotationPhase.WaitingForCharacterLogin && currentState.Phase != RotationPhase.WaitingBeforeCharacterSwitch && currentState.Phase != RotationPhase.WaitingForHomeworldReturn && currentState.Phase != RotationPhase.ScanningQuests && currentState.Phase != RotationPhase.CheckingQuestCompletion && currentState.Phase != RotationPhase.InitializingFirstCharacter) { - log.Debug("[QuestRotation] Submarine multi-mode active - skipping dungeon validation"); - } - else - { - dungeonAutomation.Update(); - if (isRotationActive && configuration.EnableAutoDutyUnsynced && !dungeonAutomation.IsWaitingForParty && currentState.Phase != RotationPhase.WaitingForCharacterLogin && currentState.Phase != RotationPhase.WaitingBeforeCharacterSwitch && currentState.Phase != RotationPhase.WaitingForHomeworldReturn && currentState.Phase != RotationPhase.ScanningQuests && currentState.Phase != RotationPhase.CheckingQuestCompletion && currentState.Phase != RotationPhase.InitializingFirstCharacter) - { - _ = submarineManager.IsSubmarinePaused; - } + _ = submarineManager.IsSubmarinePaused; } } if (combatDutyDetection != null) @@ -1164,7 +1189,25 @@ public class QuestRotationExecutionService : IDisposable { string currentQuestIdStr2 = questionableIPC.GetCurrentQuestId(); byte? currentSequence2 = questionableIPC.GetCurrentSequence(); - if (!string.IsNullOrEmpty(currentQuestIdStr2) && currentSequence2.HasValue && uint.TryParse(currentQuestIdStr2, out var currentQuestId2)) + uint currentQuestId2; + if (string.IsNullOrEmpty(currentQuestIdStr2) && currentState.HasQuestBeenAccepted) + { + if (QuestManager.Instance() != null) + { + byte gameQuestSeq = QuestManager.GetQuestSequence((ushort)questId); + if (gameQuestSeq >= activeStopPoint.Sequence.Value) + { + log.Information("[QuestRotation] ✓ Questionable auto-stopped at stop point!"); + log.Information($"[QuestRotation] Quest {questId} Sequence {gameQuestSeq} >= {activeStopPoint.Sequence.Value}"); + shouldRotate = true; + } + else + { + log.Debug($"[QuestRotation] Questionable stopped but not at stop sequence (seq {gameQuestSeq} < {activeStopPoint.Sequence.Value})"); + } + } + } + else if (!string.IsNullOrEmpty(currentQuestIdStr2) && currentSequence2.HasValue && uint.TryParse(currentQuestIdStr2, out currentQuestId2)) { if (currentQuestId2 == questId) { @@ -1315,17 +1358,24 @@ public class QuestRotationExecutionService : IDisposable { return; } - log.Information("[QuestRotation] ========================================"); - log.Information("[QuestRotation] === SENDING HOMEWORLD RETURN COMMAND ==="); - log.Information("[QuestRotation] ========================================"); - try + if (configuration.ReturnToHomeworldOnStopQuest) { - commandManager.ProcessCommand("/li"); - log.Information("[QuestRotation] ✓ /li command sent (homeworld return)"); + log.Information("[QuestRotation] ========================================"); + log.Information("[QuestRotation] === SENDING HOMEWORLD RETURN COMMAND ==="); + log.Information("[QuestRotation] ========================================"); + try + { + commandManager.ProcessCommand("/li"); + log.Information("[QuestRotation] ✓ /li command sent (homeworld return)"); + } + catch (Exception ex) + { + log.Error("[QuestRotation] Failed to send /li command: " + ex.Message); + } } - catch (Exception ex) + else { - log.Error("[QuestRotation] Failed to send /li command: " + ex.Message); + log.Information("[QuestRotation] Skipping homeworld return (setting disabled)"); } Task.Delay(2000).ContinueWith(delegate { diff --git a/QuestionableCompanion/QuestionableCompanion.Services/StepsOfFaithHandler.cs b/QuestionableCompanion/QuestionableCompanion.Services/StepsOfFaithHandler.cs index 6443eb9..980b68d 100644 --- a/QuestionableCompanion/QuestionableCompanion.Services/StepsOfFaithHandler.cs +++ b/QuestionableCompanion/QuestionableCompanion.Services/StepsOfFaithHandler.cs @@ -47,10 +47,6 @@ public class StepsOfFaithHandler : IDisposable public bool ShouldActivate(uint questId, bool isInSoloDuty) { - if (!config.EnableAutoDutyUnsynced) - { - return false; - } if (isActive) { return false; @@ -70,8 +66,10 @@ public class StepsOfFaithHandler : IDisposable } if (characterHandledStatus.GetValueOrDefault(characterName, defaultValue: false)) { + log.Debug("[StepsOfFaith] Character " + characterName + " already handled SoF - skipping"); return false; } + log.Information("[StepsOfFaith] Handler will activate for " + characterName); return true; } @@ -108,11 +106,6 @@ public class StepsOfFaithHandler : IDisposable } log.Information("[StepsOfFaith] Waiting 25s for stabilization..."); Thread.Sleep(25000); - log.Information("[StepsOfFaith] Disabling Bossmod Rotation..."); - framework.RunOnFrameworkThread(delegate - { - commandManager.ProcessCommand("/vbm ar disable"); - }); log.Information("[StepsOfFaith] Moving to target position..."); framework.RunOnFrameworkThread(delegate { diff --git a/QuestionableCompanion/QuestionableCompanion.Services/SubmarineManager.cs b/QuestionableCompanion/QuestionableCompanion.Services/SubmarineManager.cs index 0ef6c1a..53dbed5 100644 --- a/QuestionableCompanion/QuestionableCompanion.Services/SubmarineManager.cs +++ b/QuestionableCompanion/QuestionableCompanion.Services/SubmarineManager.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text.RegularExpressions; using Dalamud.Plugin.Services; using Newtonsoft.Json.Linq; @@ -28,6 +27,8 @@ public class SubmarineManager : IDisposable private bool submarinesPaused; + private bool externalPause; + private bool submarinesWaitingForSeq0; private bool submarineReloginInProgress; @@ -36,7 +37,17 @@ public class SubmarineManager : IDisposable private string? originalCharacterForSubmarines; - public bool IsSubmarinePaused => submarinesPaused; + public bool IsSubmarinePaused + { + get + { + if (!submarinesPaused) + { + return externalPause; + } + return true; + } + } public bool IsWaitingForSequence0 => submarinesWaitingForSeq0; @@ -44,6 +55,12 @@ public class SubmarineManager : IDisposable public bool IsSubmarineJustCompleted => submarineJustCompleted; + public void SetExternalPause(bool paused) + { + externalPause = paused; + log.Information($"[SubmarineManager] External pause set to: {paused}"); + } + public SubmarineManager(IPluginLog log, AutoRetainerIPC autoRetainerIPC, Configuration config, ICommandManager? commandManager = null, IFramework? framework = null) { this.log = log; @@ -88,7 +105,7 @@ public class SubmarineManager : IDisposable public bool CheckSubmarines() { - if (!config.EnableSubmarineCheck) + if (!config.EnableSubmarineCheck || externalPause) { return false; } @@ -154,7 +171,7 @@ public class SubmarineManager : IDisposable public int CheckSubmarinesSoon() { - if (!config.EnableSubmarineCheck) + if (!config.EnableSubmarineCheck || externalPause) { return 0; } @@ -230,25 +247,39 @@ public class SubmarineManager : IDisposable { JObject json = JObject.Parse(jsonContent); HashSet enabledSubs = new HashSet(StringComparer.OrdinalIgnoreCase); - if (json.SelectTokens("$..EnabledSubs").FirstOrDefault() is JArray enabledSubsArray) + IEnumerable enumerable = json.SelectTokens("$..EnabledSubs"); + int arrayCount = 0; + foreach (JToken item in enumerable) { - foreach (JToken item in enabledSubsArray) + if (!(item is JArray enabledSubsArray)) { - string subName = item.Value(); + continue; + } + arrayCount++; + foreach (JToken item2 in enabledSubsArray) + { + string subName = item2.Value(); if (!string.IsNullOrEmpty(subName)) { enabledSubs.Add(subName); } } + } + if (arrayCount > 0) + { if (enabledSubs.Count > 0) { - log.Information($"[SubmarineManager] Found {enabledSubs.Count} enabled submarines: {string.Join(", ", enabledSubs)}"); + log.Information($"[SubmarineManager] Found {enabledSubs.Count} unique submarine name(s) across {arrayCount} character(s): {string.Join(", ", enabledSubs)}"); } else { - log.Information("[SubmarineManager] EnabledSubs array found but empty - NO submarines will be checked"); + log.Information($"[SubmarineManager] Found {arrayCount} EnabledSubs array(s) but all empty - NO submarines will be checked"); } FindReturnTimes(json, returnTimes, enabledSubs); + if (returnTimes.Count > 0) + { + log.Information($"[SubmarineManager] Total submarines to monitor: {returnTimes.Count} (including same-named subs from different characters)"); + } } else { @@ -281,10 +312,6 @@ public class SubmarineManager : IDisposable { long returnTime = returnTimeToken.Value(); returnTimes.Add(returnTime); - if (enabledSubs != null) - { - log.Debug($"[SubmarineManager] Including submarine '{submarineName}' (ReturnTime: {returnTime})"); - } } } { diff --git a/QuestionableCompanion/QuestionableCompanion.Windows/NewMainWindow.cs b/QuestionableCompanion/QuestionableCompanion.Windows/NewMainWindow.cs index 9653d02..4572c83 100644 --- a/QuestionableCompanion/QuestionableCompanion.Windows/NewMainWindow.cs +++ b/QuestionableCompanion/QuestionableCompanion.Windows/NewMainWindow.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Numerics; +using System.Threading.Tasks; using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; @@ -140,6 +141,8 @@ public class NewMainWindow : Window, IDisposable private DateTime lastEventQuestRefresh = DateTime.MinValue; + private string? newLANHelperIP; + private readonly Dictionary> dataCenterWorlds = new Dictionary> { { @@ -533,6 +536,7 @@ public class NewMainWindow : Window, IDisposable DrawSidebarItem("Event Quest", 6, 0); DrawSidebarItem("MSQ Progression", 7, 0); DrawSidebarItem("Data Center Travel", 8, 0); + DrawSidebarItem("Multiboxing", 12, 0); DrawSidebarItem("Settings", 9, 0); } else @@ -616,7 +620,7 @@ public class NewMainWindow : Window, IDisposable uint rightColor = ImGui.ColorConvertFloat4ToU32(new Vector4(colorSecondary.X * 0.3f, colorSecondary.Y * 0.3f, colorSecondary.Z * 0.3f, 1f)); drawList.AddRectFilledMultiColor(windowPos, windowPos + new Vector2(windowSize.X, height), leftColor, rightColor, rightColor, leftColor); Vector2 titlePos = windowPos + new Vector2(10f, 7f); - drawList.AddText(titlePos, ImGui.ColorConvertFloat4ToU32(new Vector4(1f, 1f, 1f, 0.9f)), "Questionable Companion V.1.0.5"); + drawList.AddText(titlePos, ImGui.ColorConvertFloat4ToU32(new Vector4(1f, 1f, 1f, 0.9f)), "Questionable Companion V.1.0.6"); Vector2 minimizeButtonPos = windowPos + new Vector2(windowSize.X - 60f, 3f); Vector2 minimizeButtonSize = new Vector2(24f, 24f); if (ImGui.IsMouseHoveringRect(minimizeButtonPos, minimizeButtonPos + minimizeButtonSize)) @@ -718,6 +722,9 @@ public class NewMainWindow : Window, IDisposable case 11: DrawWarningTab(); break; + case 12: + DrawMultiboxingTab(); + break; } } } @@ -776,9 +783,9 @@ public class NewMainWindow : Window, IDisposable { selectedDataCenter = config.DCTravelDataCenter; } - if (string.IsNullOrEmpty(selectedWorld) && !string.IsNullOrEmpty(config.DCTravelTargetWorld)) + if (string.IsNullOrEmpty(selectedWorld) && !string.IsNullOrEmpty(config.DCTravelWorld)) { - selectedWorld = config.DCTravelTargetWorld; + selectedWorld = config.DCTravelWorld; } if (string.IsNullOrEmpty(selectedDataCenter)) { @@ -921,7 +928,7 @@ public class NewMainWindow : Window, IDisposable ImGui.TextUnformatted(text2); ImGui.SameLine(); ImGui.PushStyleColor(ImGuiCol.Text, colorPrimary); - ImGui.TextUnformatted((config.DCTravelTargetWorld.Length > 0) ? config.DCTravelTargetWorld : "Not Set"); + ImGui.TextUnformatted((config.DCTravelWorld.Length > 0) ? config.DCTravelWorld : "Not Set"); ImGui.PopStyleColor(); ImU8String text3 = new ImU8String(8, 0); text3.AppendLiteral("Status: "); @@ -946,7 +953,7 @@ public class NewMainWindow : Window, IDisposable if (ImGui.Button("Apply", new Vector2(120f, 30f))) { config.DCTravelDataCenter = selectedDataCenter; - config.DCTravelTargetWorld = selectedWorld; + config.DCTravelWorld = selectedWorld; config.Save(); log.Information("[DCTravel] Configuration saved: " + selectedDataCenter + " -> " + selectedWorld); } @@ -955,7 +962,7 @@ public class NewMainWindow : Window, IDisposable if (ImGui.Button("Cancel", new Vector2(120f, 30f))) { selectedDataCenter = config.DCTravelDataCenter; - selectedWorld = config.DCTravelTargetWorld; + selectedWorld = config.DCTravelWorld; if (string.IsNullOrEmpty(selectedDataCenter)) { selectedDataCenter = dataCenterWorlds.Keys.First(); @@ -978,81 +985,18 @@ public class NewMainWindow : Window, IDisposable } } - private void DrawSettingsTabFull() + private void DrawMultiboxingTab() { ImGui.PushStyleColor(ImGuiCol.Text, colorPrimary); - ImGui.TextUnformatted("Plugin Settings"); + ImGui.TextUnformatted("Multiboxing Settings"); ImGui.PopStyleColor(); ImGuiHelpers.ScaledDummy(10f); - using ImRaii.IEndObject child = ImRaii.Child("SettingsScrollArea", new Vector2(0f, 0f), border: false, ImGuiWindowFlags.None); + using ImRaii.IEndObject child = ImRaii.Child("MultiboxingScrollArea", new Vector2(0f, 0f), border: false, ImGuiWindowFlags.None); if (!child.Success) { return; } Configuration config = plugin.Configuration; - DrawSettingSection("Submarine Management", delegate - { - config.EnableSubmarineCheck = DrawSettingWithInfo("Enable Submarine Monitoring", config.EnableSubmarineCheck, "Automatically monitors submarines and pauses quest rotation when submarines are ready.\nPrevents quest progression while submarines need attention.\nImpact: Rotation will pause when submarines are detected."); - if (ImGui.IsItemDeactivatedAfterEdit()) - { - config.Save(); - } - if (config.EnableSubmarineCheck) - { - ImGui.Indent(); - int v = config.SubmarineCheckInterval; - if (ImGui.SliderInt("Check Interval (seconds)", ref v, 30, 300)) - { - config.SubmarineCheckInterval = v; - config.Save(); - } - DrawInfoIcon("How often to check for submarine status.\nLower values = more frequent checks but higher CPU usage."); - int v2 = config.SubmarineReloginCooldown; - if (ImGui.SliderInt("Cooldown after Relog (seconds)", ref v2, 60, 300)) - { - config.SubmarineReloginCooldown = v2; - config.Save(); - } - DrawInfoIcon("Time to wait after character switch before checking submarines again."); - int v3 = config.SubmarineWaitTime; - if (ImGui.SliderInt("Wait time before submarine (seconds)", ref v3, 10, 120)) - { - config.SubmarineWaitTime = v3; - config.Save(); - } - DrawInfoIcon("Delay before starting submarine operations after detection."); - ImGui.Unindent(); - } - }, config.EnableSubmarineCheck); - ImGuiHelpers.ScaledDummy(10f); - DrawSettingSection("AutoRetainer Post Process Event Quests", delegate - { - config.RunEventQuestsOnARPostProcess = DrawSettingWithInfo("Run Event Quests on AR Post Process", config.RunEventQuestsOnARPostProcess, "AUTO-DETECTION: Automatically detects and runs active Event Quests when AutoRetainer completes a character.\nEvent Quests are detected via Questionable IPC (same as manual Event Quest tab).\nAll prerequisites will be automatically resolved and executed.\nAutoRetainer will wait until all Event Quests are completed before proceeding.\nImpact: Extends AR post-process time but ensures Event Quests are completed."); - if (ImGui.IsItemDeactivatedAfterEdit()) - { - config.Save(); - } - if (config.RunEventQuestsOnARPostProcess) - { - ImGui.Indent(); - ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0.4f, 0.8f, 0.4f, 1f)); - ImGui.TextUnformatted("Auto-Detection Enabled"); - ImGui.PopStyleColor(); - ImGui.PushStyleColor(ImGuiCol.Text, colorSecondary); - ImGui.TextWrapped("Event Quests will be automatically detected from Questionable when AR Post Process starts. No manual configuration needed - just enable this setting and the plugin will handle the rest!"); - ImGui.PopStyleColor(); - ImGuiHelpers.ScaledDummy(5f); - int v = config.EventQuestPostProcessTimeoutMinutes; - if (ImGui.SliderInt("Timeout (minutes)", ref v, 10, 60)) - { - config.EventQuestPostProcessTimeoutMinutes = v; - config.Save(); - } - DrawInfoIcon("Maximum time to wait for Event Quests to complete.\nAfter timeout, AR will proceed with next character."); - ImGui.Unindent(); - } - }, config.RunEventQuestsOnARPostProcess); - ImGuiHelpers.ScaledDummy(10f); DrawSettingSection("Dungeon Automation", delegate { bool enableAutoDutyUnsynced = config.EnableAutoDutyUnsynced; @@ -1086,6 +1030,15 @@ public class NewMainWindow : Window, IDisposable config.Save(); } DrawInfoIcon("How often to re-send party invites if members don't join."); + ImGuiHelpers.ScaledDummy(5f); + bool v4 = config.EnableARRPrimalCheck; + if (ImGui.Checkbox("Check ARR Primals when hitting flag", ref v4)) + { + config.EnableARRPrimalCheck = v4; + config.Save(); + Plugin.Log.Information("[Multiboxing] ARR Primal Check: " + (v4 ? "ENABLED" : "DISABLED")); + } + DrawInfoIcon("Checks if ARR Hard Mode Primals (Ifrit/Garuda/Titan) are done.\nRequired for Quest 363 (Good Intentions)."); ImGui.Unindent(); } }, config.EnableAutoDutyUnsynced); @@ -1116,6 +1069,7 @@ public class NewMainWindow : Window, IDisposable config.IsQuester = true; config.IsHighLevelHelper = false; config.Save(); + Plugin.Log.Information("[Multiboxing] Role changed to: Quester"); } ImGui.SameLine(); DrawInfoIcon("This client will quest and invite helpers for dungeons"); @@ -1126,11 +1080,168 @@ public class NewMainWindow : Window, IDisposable Plugin.Framework.RunOnFrameworkThread(delegate { Plugin.Instance?.GetHelperManager()?.AnnounceIfHelper(); + Plugin.Instance?.GetChauffeurMode()?.StartHelperStatusBroadcast(); }); config.Save(); + Plugin.Log.Information("[Multiboxing] Role changed to: High-Level Helper"); } ImGui.SameLine(); DrawInfoIcon("This client will help with dungeons.\nAutoDuty starts/stops automatically on duty enter/leave"); + if (config.IsHighLevelHelper) + { + ImGuiHelpers.ScaledDummy(5f); + ImGui.Indent(); + bool v = config.AlwaysAutoAcceptInvites; + if (ImGui.Checkbox("Always Auto-Accept Party Invites", ref v)) + { + config.AlwaysAutoAcceptInvites = v; + config.Save(); + } + DrawInfoIcon("Continuously accept ALL party invites (useful for ManualInput mode without IPC)"); + ImGui.Unindent(); + } + ImGuiHelpers.ScaledDummy(10f); + ImGui.Separator(); + ImGuiHelpers.ScaledDummy(5f); + ImGui.TextColored(in colorPrimary, "LAN Multi-PC Helper System"); + ImGui.TextWrapped("Connect helpers on different PCs in your HOME NETWORK."); + ImGuiHelpers.ScaledDummy(3f); + config.EnableLANHelpers = DrawSettingWithInfo("Enable LAN Helper System", config.EnableLANHelpers, "Connect to helpers on other PCs in YOUR home network.\nNOT accessible from internet! Only devices in your home can connect."); + if (ImGui.IsItemDeactivatedAfterEdit()) + { + config.Save(); + } + if (config.EnableLANHelpers) + { + ImGui.Indent(); + if (config.IsHighLevelHelper) + { + bool flag = DrawSettingWithInfo("Start LAN Server on this PC", config.StartLANServer, "Enable so OTHER PCs in your home can connect to THIS PC.\nNOT exposed to internet! Only devices in your home can connect."); + if (flag != config.StartLANServer) + { + config.StartLANServer = flag; + config.Save(); + plugin.ToggleLANServer(flag); + } + } + ImGuiHelpers.ScaledDummy(5f); + ImGui.TextColored(in colorSecondary, "Server Port:"); + ImGui.SetNextItemWidth(150f); + int data = config.LANServerPort; + if (ImGui.InputInt("##LANPort", ref data) && data >= 1024 && data <= 65535) + { + config.LANServerPort = data; + config.Save(); + } + ImGui.SameLine(); + DrawInfoIcon("Port for local network communication (default: 47788).\nFirewall may need to allow this port."); + ImGuiHelpers.ScaledDummy(5f); + ImGui.TextColored(in colorSecondary, "Helper PC IP Addresses:"); + ImGui.TextWrapped("Add IPs of OTHER PCs in your home with helper characters:"); + ImGuiHelpers.ScaledDummy(3f); + if (config.LANHelperIPs == null) + { + config.LANHelperIPs = new List(); + } + for (int num2 = 0; num2 < config.LANHelperIPs.Count; num2++) + { + ImU8String strId = new ImU8String(3, 1); + strId.AppendLiteral("IP_"); + strId.AppendFormatted(num2); + ImGui.PushID(strId); + ImGui.BulletText(config.LANHelperIPs[num2]); + ImGui.SameLine(); + if (ImGui.SmallButton("\ud83d\udd04 Reconnect")) + { + string ip = config.LANHelperIPs[num2]; + LANHelperClient lanClient = plugin.GetLANHelperClient(); + if (lanClient != null) + { + Task.Run(async delegate + { + Plugin.Log.Information("[UI] Manual reconnect to " + ip + "..."); + await lanClient.ConnectToHelperAsync(ip); + }); + } + } + ImGui.SameLine(); + if (ImGui.SmallButton("Remove")) + { + config.LANHelperIPs.RemoveAt(num2); + config.Save(); + num2--; + } + ImGui.PopID(); + } + if (config.LANHelperIPs.Count == 0) + { + ImGui.TextColored(new Vector4(1f, 0.8f, 0.2f, 1f), "No IPs configured"); + ImGui.TextWrapped("Add IPs below (or use 127.0.0.1 for same-PC testing)"); + } + ImGuiHelpers.ScaledDummy(3f); + ImGui.TextColored(in colorSecondary, "Add new IP:"); + ImGui.SetNextItemWidth(200f); + string buf = newLANHelperIP ?? ""; + if (ImGui.InputText("##NewIP", ref buf, 50)) + { + newLANHelperIP = buf; + } + ImGui.SameLine(); + if (ImGui.Button("Add IP") && !string.IsNullOrWhiteSpace(newLANHelperIP)) + { + string trimmedIP = newLANHelperIP.Trim(); + if (!config.LANHelperIPs.Contains(trimmedIP)) + { + config.LANHelperIPs.Add(trimmedIP); + config.Save(); + newLANHelperIP = ""; + LANHelperClient lanClient2 = plugin.GetLANHelperClient(); + if (lanClient2 != null) + { + Task.Run(async delegate + { + await lanClient2.ConnectToHelperAsync(trimmedIP); + }); + } + } + } + ImGui.SameLine(); + if (ImGui.SmallButton("Add Localhost") && !config.LANHelperIPs.Contains("127.0.0.1")) + { + config.LANHelperIPs.Add("127.0.0.1"); + config.Save(); + LANHelperClient lanClient3 = plugin.GetLANHelperClient(); + if (lanClient3 != null) + { + Task.Run(async delegate + { + await lanClient3.ConnectToHelperAsync("127.0.0.1"); + }); + } + } + ImGuiHelpers.ScaledDummy(3f); + ImGui.TextColored(new Vector4(0.7f, 0.7f, 0.7f, 1f), "\ud83d\udca1 Tip: Run 'ipconfig' and use your IPv4-Adresse (like 192.168.x.x)"); + ImGuiHelpers.ScaledDummy(5f); + ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0.4f, 0.8f, 0.4f, 1f)); + if (config.StartLANServer) + { + ImU8String text = new ImU8String(48, 1); + text.AppendLiteral("✓ LAN Server enabled (LOCAL network only, port "); + text.AppendFormatted(config.LANServerPort); + text.AppendLiteral(")"); + ImGui.TextWrapped(text); + } + if (config.LANHelperIPs.Count > 0) + { + ImU8String text2 = new ImU8String(37, 1); + text2.AppendLiteral("✓ Will connect to "); + text2.AppendFormatted(config.LANHelperIPs.Count); + text2.AppendLiteral(" local helper PC(s)"); + ImGui.TextWrapped(text2); + } + ImGui.PopStyleColor(); + ImGui.Unindent(); + } ImGuiHelpers.ScaledDummy(10f); if (config.IsQuester) { @@ -1140,139 +1251,201 @@ public class NewMainWindow : Window, IDisposable ImGui.TextWrapped("Helpers are automatically discovered via IPC when they have 'I'm a High-Level Helper' enabled:"); ImGuiHelpers.ScaledDummy(5f); List<(string, ushort)> availableHelpers = plugin.GetAvailableHelpers(); - if (availableHelpers.Count != 0) + if (availableHelpers.Count == 0) + { + ImGui.TextColored(new Vector4(1f, 0.8f, 0.2f, 1f), "No helpers discovered yet"); + ImGui.TextWrapped("Make sure helper clients are running with 'I'm a High-Level Helper' enabled."); + } + else { Vector4 col = new Vector4(0.2f, 1f, 0.2f, 1f); - ImU8String text = new ImU8String(21, 1); - text.AppendFormatted(availableHelpers.Count); - text.AppendLiteral(" helper(s) available:"); - ImGui.TextColored(in col, text); - ImGuiHelpers.ScaledDummy(5f); - ImGui.TextUnformatted("Preferred Helper for Chauffeur:"); + ImU8String text3 = new ImU8String(20, 1); + text3.AppendFormatted(availableHelpers.Count); + text3.AppendLiteral(" helper(s) available"); + ImGui.TextColored(in col, text3); + } + ImGuiHelpers.ScaledDummy(5f); + ImGui.Separator(); + ImGuiHelpers.ScaledDummy(5f); + ImGui.TextColored(in colorPrimary, "Helper Selection Mode"); + ImGuiHelpers.ScaledDummy(3f); + int helperSelection = (int)config.HelperSelection; + if (ImGui.RadioButton("Auto", helperSelection == 0)) + { + config.HelperSelection = HelperSelectionMode.Auto; + config.PreferredHelper = ""; + config.ManualHelperName = ""; + config.Save(); + } + ImGui.SameLine(); + DrawInfoIcon("First available helper via IPC"); + if (ImGui.RadioButton("Dropdown", helperSelection == 1)) + { + config.HelperSelection = HelperSelectionMode.Dropdown; + config.ManualHelperName = ""; + config.Save(); + } + ImGui.SameLine(); + DrawInfoIcon("Select specific helper from list"); + if (config.HelperSelection == HelperSelectionMode.Dropdown && availableHelpers.Count > 0) + { + ImGui.Indent(); ImGui.SetNextItemWidth(250f); - List list = new List { "Auto (First Available)" }; - foreach (var item5 in availableHelpers) + string text4 = (string.IsNullOrEmpty(config.PreferredHelper) ? "-- Select --" : config.PreferredHelper); + if (ImGui.BeginCombo("##PreferredHelper", text4)) { - string item = item5.Item1; - ushort item2 = item5.Item2; - ExcelSheet excelSheet = Plugin.DataManager.GetExcelSheet(); - string text2 = "Unknown"; - if (excelSheet != null) + foreach (var item5 in availableHelpers) { - foreach (World current2 in excelSheet) + string item = item5.Item1; + ushort item2 = item5.Item2; + ExcelSheet excelSheet = Plugin.DataManager.GetExcelSheet(); + string text5 = "Unknown"; + if (excelSheet != null) { - if (current2.RowId == item2) + foreach (World current2 in excelSheet) { - text2 = current2.Name.ExtractText(); - break; - } - } - } - list.Add(item + "@" + text2); - } - string text3 = (string.IsNullOrEmpty(config.PreferredHelper) ? "Auto (First Available)" : config.PreferredHelper); - if (ImGui.BeginCombo("##PreferredHelper", text3)) - { - foreach (string current3 in list) - { - bool flag = text3 == current3; - if (ImGui.Selectable(current3, flag)) - { - config.PreferredHelper = ((current3 == "Auto (First Available)") ? "" : current3); - config.Save(); - } - if (flag) - { - ImGui.SetItemDefaultFocus(); - } - } - ImGui.EndCombo(); - } - ImGui.SameLine(); - DrawInfoIcon("Select which helper to use for Chauffeur Mode.\n'Auto' will use the first available helper."); - if (!string.IsNullOrEmpty(config.PreferredHelper)) - { - ImGuiHelpers.ScaledDummy(3f); - string text4 = (Plugin.Instance?.GetChauffeurMode())?.GetHelperStatus(config.PreferredHelper); - Vector4 col2; - ImU8String text5; - switch (text4) - { - case "Available": - col = new Vector4(0.2f, 1f, 0.2f, 1f); - goto IL_04f7; - case "Transporting": - col = new Vector4(1f, 0.8f, 0f, 1f); - goto IL_04f7; - case "InDungeon": - col = new Vector4(1f, 0.3f, 0.3f, 1f); - goto IL_04f7; - default: - col = colorSecondary; - goto IL_04f7; - case null: - { - ImGui.TextColored(in colorSecondary, "Helper Status: Unknown (waiting for update...)"); - break; - } - IL_04f7: - col2 = col; - text5 = new ImU8String(15, 1); - text5.AppendLiteral("Helper Status: "); - text5.AppendFormatted(text4); - ImGui.TextColored(in col2, text5); - break; - } - } - ImGuiHelpers.ScaledDummy(5f); - ImGui.TextUnformatted("Available Helpers:"); - ChauffeurModeService chauffeurModeService = Plugin.Instance?.GetChauffeurMode(); - { - foreach (var item6 in availableHelpers) - { - string item3 = item6.Item1; - ushort item4 = item6.Item2; - ExcelSheet excelSheet2 = Plugin.DataManager.GetExcelSheet(); - string text6 = "Unknown"; - if (excelSheet2 != null) - { - foreach (World current5 in excelSheet2) - { - if (current5.RowId == item4) + if (current2.RowId == item2) { - text6 = current5.Name.ExtractText(); + text5 = current2.Name.ExtractText(); break; } } } - string text7 = item3 + "@" + text6; - string text8 = chauffeurModeService?.GetHelperStatus(text7); - ImU8String text9 = new ImU8String(4, 1); - text9.AppendLiteral(" • "); - text9.AppendFormatted(text7); - ImGui.TextUnformatted(text9); - if (text8 != null) + string text6 = item + "@" + text5; + bool selected = config.PreferredHelper == text6; + if (ImGui.Selectable(text6, selected)) { - ImGui.SameLine(); - Vector4 col3 = text8 switch - { - "Available" => new Vector4(0.2f, 1f, 0.2f, 1f), - "Transporting" => new Vector4(1f, 0.8f, 0f, 1f), - "InDungeon" => new Vector4(1f, 0.3f, 0.3f, 1f), - _ => colorSecondary, - }; - ImU8String text10 = new ImU8String(2, 1); - text10.AppendLiteral("["); - text10.AppendFormatted(text8); - text10.AppendLiteral("]"); - ImGui.TextColored(in col3, text10); + config.PreferredHelper = text6; + config.Save(); } } - return; + ImGui.EndCombo(); + } + if (!string.IsNullOrEmpty(config.PreferredHelper)) + { + string text7 = (Plugin.Instance?.GetChauffeurMode())?.GetHelperStatus(config.PreferredHelper); + Vector4 col; + Vector4 col2; + ImU8String text8; + switch (text7) + { + case "Available": + col = new Vector4(0.2f, 1f, 0.2f, 1f); + goto IL_0c39; + case "Transporting": + col = new Vector4(1f, 0.8f, 0f, 1f); + goto IL_0c39; + case "InDungeon": + col = new Vector4(1f, 0.3f, 0.3f, 1f); + goto IL_0c39; + default: + col = colorSecondary; + goto IL_0c39; + case null: + break; + IL_0c39: + col2 = col; + ImGui.SameLine(); + text8 = new ImU8String(2, 1); + text8.AppendLiteral("["); + text8.AppendFormatted(text7); + text8.AppendLiteral("]"); + ImGui.TextColored(in col2, text8); + break; + } + } + ImGui.Unindent(); + } + else if (config.HelperSelection == HelperSelectionMode.Dropdown && availableHelpers.Count == 0) + { + ImGui.Indent(); + ImGui.TextColored(new Vector4(1f, 0.8f, 0.2f, 1f), "⚠ No helpers available to select"); + ImGui.Unindent(); + } + if (ImGui.RadioButton("Manual Input", helperSelection == 2)) + { + config.HelperSelection = HelperSelectionMode.ManualInput; + config.PreferredHelper = ""; + config.Save(); + } + ImGui.SameLine(); + DrawInfoIcon("Manual entry (Dungeon invites only - NOT Chauffeur/Following!)"); + if (config.HelperSelection == HelperSelectionMode.ManualInput) + { + ImGui.Indent(); + ImGui.SetNextItemWidth(250f); + string buf2 = config.ManualHelperName; + if (ImGui.InputText("##ManualHelperInput", ref buf2, 100)) + { + config.ManualHelperName = buf2; + config.Save(); + } + ImGui.SameLine(); + DrawInfoIcon("Format: CharacterName@WorldName"); + if (!string.IsNullOrEmpty(config.ManualHelperName)) + { + if (config.ManualHelperName.Contains("@")) + { + ImGui.SameLine(); + ImGui.TextColored(new Vector4(0.2f, 1f, 0.2f, 1f), "✓"); + } + else + { + ImGui.SameLine(); + ImGui.TextColored(new Vector4(1f, 0.5f, 0f, 1f), "⚠"); + } + } + ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(1f, 0.8f, 0.2f, 1f)); + ImGui.TextWrapped("⚠ Cannot be used with Chauffeur/Following (requires IPC)"); + ImGui.PopStyleColor(); + ImGui.Unindent(); + } + if (availableHelpers.Count > 0) + { + ImGuiHelpers.ScaledDummy(5f); + ImGui.TextUnformatted("Available Helpers:"); + ChauffeurModeService chauffeurModeService = Plugin.Instance?.GetChauffeurMode(); + foreach (var item6 in availableHelpers) + { + string item3 = item6.Item1; + ushort item4 = item6.Item2; + ExcelSheet excelSheet2 = Plugin.DataManager.GetExcelSheet(); + string text9 = "Unknown"; + if (excelSheet2 != null) + { + foreach (World current4 in excelSheet2) + { + if (current4.RowId == item4) + { + text9 = current4.Name.ExtractText(); + break; + } + } + } + string text10 = item3 + "@" + text9; + string text11 = chauffeurModeService?.GetHelperStatus(text10); + ImU8String text12 = new ImU8String(4, 1); + text12.AppendLiteral(" • "); + text12.AppendFormatted(text10); + ImGui.TextUnformatted(text12); + if (text11 != null) + { + ImGui.SameLine(); + Vector4 col3 = text11 switch + { + "Available" => new Vector4(0.2f, 1f, 0.2f, 1f), + "Transporting" => new Vector4(1f, 0.8f, 0f, 1f), + "InDungeon" => new Vector4(1f, 0.3f, 0.3f, 1f), + _ => colorSecondary, + }; + ImU8String text13 = new ImU8String(2, 1); + text13.AppendLiteral("["); + text13.AppendFormatted(text11); + text13.AppendLiteral("]"); + ImGui.TextColored(in col3, text13); + } } } - ImGui.TextColored(new Vector4(1f, 0.8f, 0.2f, 1f), "No helpers discovered yet"); - ImGui.TextWrapped("Make sure helper clients are running with 'I'm a High-Level Helper' enabled."); } }, config.IsQuester || config.IsHighLevelHelper); ImGuiHelpers.ScaledDummy(10f); @@ -1291,6 +1464,7 @@ public class NewMainWindow : Window, IDisposable { config.ChauffeurModeEnabled = v; config.Save(); + Plugin.Log.Information("[Multiboxing] Chauffeur Mode: " + (v ? "ENABLED" : "DISABLED")); } DrawInfoIcon("Enable automatic helper summoning for long-distance travel in non-flying zones"); if (config.ChauffeurModeEnabled) @@ -1488,6 +1662,7 @@ public class NewMainWindow : Window, IDisposable { config.EnableHelperFollowing = v; config.Save(); + Plugin.Log.Information("[Multiboxing] Helper Following (Quester): " + (v ? "ENABLED" : "DISABLED")); } if (string.IsNullOrEmpty(config.AssignedHelperForFollowing)) { @@ -1565,6 +1740,7 @@ public class NewMainWindow : Window, IDisposable { config.EnableHelperFollowing = v2; config.Save(); + Plugin.Log.Information("[Multiboxing] Helper Following (Helper): " + (v2 ? "ENABLED" : "DISABLED")); } if (string.IsNullOrEmpty(config.AssignedQuesterForFollowing)) { @@ -1617,6 +1793,82 @@ public class NewMainWindow : Window, IDisposable } } }, config.EnableHelperFollowing); + } + + private void DrawSettingsTabFull() + { + ImGui.PushStyleColor(ImGuiCol.Text, colorPrimary); + ImGui.TextUnformatted("Plugin Settings"); + ImGui.PopStyleColor(); + ImGuiHelpers.ScaledDummy(10f); + using ImRaii.IEndObject child = ImRaii.Child("SettingsScrollArea", new Vector2(0f, 0f), border: false, ImGuiWindowFlags.None); + if (!child.Success) + { + return; + } + Configuration config = plugin.Configuration; + DrawSettingSection("Submarine Management", delegate + { + config.EnableSubmarineCheck = DrawSettingWithInfo("Enable Submarine Monitoring", config.EnableSubmarineCheck, "Automatically monitors submarines and pauses quest rotation when submarines are ready.\nPrevents quest progression while submarines need attention.\nImpact: Rotation will pause when submarines are detected."); + if (ImGui.IsItemDeactivatedAfterEdit()) + { + config.Save(); + } + if (config.EnableSubmarineCheck) + { + ImGui.Indent(); + int v = config.SubmarineCheckInterval; + if (ImGui.SliderInt("Check Interval (seconds)##Submarine", ref v, 30, 300)) + { + config.SubmarineCheckInterval = v; + config.Save(); + } + DrawInfoIcon("How often to check for submarine status.\nLower values = more frequent checks but higher CPU usage."); + int v2 = config.SubmarineReloginCooldown; + if (ImGui.SliderInt("Cooldown after Relog (seconds)", ref v2, 60, 300)) + { + config.SubmarineReloginCooldown = v2; + config.Save(); + } + DrawInfoIcon("Time to wait after character switch before checking submarines again."); + int v3 = config.SubmarineWaitTime; + if (ImGui.SliderInt("Wait time before submarine (seconds)", ref v3, 10, 120)) + { + config.SubmarineWaitTime = v3; + config.Save(); + } + DrawInfoIcon("Delay before starting submarine operations after detection."); + ImGui.Unindent(); + } + }, config.EnableSubmarineCheck); + ImGuiHelpers.ScaledDummy(10f); + DrawSettingSection("AutoRetainer Post Process Event Quests", delegate + { + config.RunEventQuestsOnARPostProcess = DrawSettingWithInfo("Run Event Quests on AR Post Process", config.RunEventQuestsOnARPostProcess, "AUTO-DETECTION: Automatically detects and runs active Event Quests when AutoRetainer completes a character.\nEvent Quests are detected via Questionable IPC (same as manual Event Quest tab).\nAll prerequisites will be automatically resolved and executed.\nAutoRetainer will wait until all Event Quests are completed before proceeding.\nImpact: Extends AR post-process time but ensures Event Quests are completed."); + if (ImGui.IsItemDeactivatedAfterEdit()) + { + config.Save(); + } + if (config.RunEventQuestsOnARPostProcess) + { + ImGui.Indent(); + ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0.4f, 0.8f, 0.4f, 1f)); + ImGui.TextUnformatted("Auto-Detection Enabled"); + ImGui.PopStyleColor(); + ImGui.PushStyleColor(ImGuiCol.Text, colorSecondary); + ImGui.TextWrapped("Event Quests will be automatically detected from Questionable when AR Post Process starts. No manual configuration needed - just enable this setting and the plugin will handle the rest!"); + ImGui.PopStyleColor(); + ImGuiHelpers.ScaledDummy(5f); + int v = config.EventQuestPostProcessTimeoutMinutes; + if (ImGui.SliderInt("Timeout (minutes)", ref v, 10, 60)) + { + config.EventQuestPostProcessTimeoutMinutes = v; + config.Save(); + } + DrawInfoIcon("Maximum time to wait for Event Quests to complete.\nAfter timeout, AR will proceed with next character."); + ImGui.Unindent(); + } + }, config.RunEventQuestsOnARPostProcess); ImGuiHelpers.ScaledDummy(10f); DrawSettingSection("Movement Monitor", delegate { @@ -2529,7 +2781,7 @@ public class NewMainWindow : Window, IDisposable private void DrawMSQOverall(List characters) { - using ImRaii.IEndObject table = ImRaii.Table("MSQOverallTable", 4, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY); + using ImRaii.IEndObject table = ImRaii.Table("MSQOverallTable", 5, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY); if (!table.Success) { return; @@ -2538,10 +2790,12 @@ public class NewMainWindow : Window, IDisposable ImGui.TableSetupColumn("MSQ Progress", ImGuiTableColumnFlags.WidthFixed, 120f); ImGui.TableSetupColumn("Current MSQ", ImGuiTableColumnFlags.WidthStretch); ImGui.TableSetupColumn("Completion %", ImGuiTableColumnFlags.WidthFixed, 100f); + ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, 70f); ImGui.TableSetupScrollFreeze(0, 1); ImGui.TableHeadersRow(); - foreach (string character in characters) + for (int charIndex = 0; charIndex < characters.Count; charIndex++) { + string character = characters[charIndex]; if (!characterProgressCache.TryGetValue(character, out CharacterProgressInfo progressInfo)) { GetCharacterProgress(character); @@ -2569,6 +2823,27 @@ public class NewMainWindow : Window, IDisposable overlay.AppendFormatted(percentage, "F1"); overlay.AppendLiteral("%"); ImGui.ProgressBar(fraction, sizeArg, overlay); + ImGui.TableNextColumn(); + using (ImRaii.PushId(charIndex)) + { + ImGui.PushStyleColor(ImGuiCol.Button, colorAccent); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, new Vector4(colorAccent.X * 1.2f, colorAccent.Y * 1.2f, colorAccent.Z * 1.2f, 1f)); + if (ImGui.Button("Reset")) + { + questRotationService.ClearCharacterQuestData(character); + characterProgressCache.Remove(character); + log.Information("[MSQProgression] Reset quest data for " + character); + } + ImGui.PopStyleColor(2); + if (ImGui.IsItemHovered()) + { + ImU8String tooltip = new ImU8String(85, 1); + tooltip.AppendLiteral("Reset all quest completion data for "); + tooltip.AppendFormatted(character); + tooltip.AppendLiteral(".\nUse this if data was corrupted during rotation."); + ImGui.SetTooltip(tooltip); + } + } } } diff --git a/QuestionableCompanion/QuestionableCompanion/Configuration.cs b/QuestionableCompanion/QuestionableCompanion/Configuration.cs index 7dc61ea..e11c327 100644 --- a/QuestionableCompanion/QuestionableCompanion/Configuration.cs +++ b/QuestionableCompanion/QuestionableCompanion/Configuration.cs @@ -132,6 +132,12 @@ public class Configuration : IPluginConfiguration public HelperStatus CurrentHelperStatus { get; set; } + public HelperSelectionMode HelperSelection { get; set; } + + public string ManualHelperName { get; set; } = ""; + + public bool AlwaysAutoAcceptInvites { get; set; } + public bool EnableHelperFollowing { get; set; } public float HelperFollowDistance { get; set; } = 100f; @@ -142,6 +148,8 @@ public class Configuration : IPluginConfiguration public string AssignedHelperForFollowing { get; set; } = ""; + public bool EnableARRPrimalCheck { get; set; } + public bool EnableSafeWaitBeforeCharacterSwitch { get; set; } public bool EnableSafeWaitAfterCharacterSwitch { get; set; } @@ -187,6 +195,14 @@ public class Configuration : IPluginConfiguration } }; + public bool EnableLANHelpers { get; set; } + + public int LANServerPort { get; set; } = 47788; + + public List LANHelperIPs { get; set; } = new List(); + + public bool StartLANServer { get; set; } + public void Save() { Plugin.PluginInterface.SavePluginConfig(this); diff --git a/QuestionableCompanion/QuestionableCompanion/HelperSelectionMode.cs b/QuestionableCompanion/QuestionableCompanion/HelperSelectionMode.cs new file mode 100644 index 0000000..585278e --- /dev/null +++ b/QuestionableCompanion/QuestionableCompanion/HelperSelectionMode.cs @@ -0,0 +1,8 @@ +namespace QuestionableCompanion; + +public enum HelperSelectionMode +{ + Auto, + Dropdown, + ManualInput +} diff --git a/QuestionableCompanion/QuestionableCompanion/Plugin.cs b/QuestionableCompanion/QuestionableCompanion/Plugin.cs index 76d471a..1159440 100644 --- a/QuestionableCompanion/QuestionableCompanion/Plugin.cs +++ b/QuestionableCompanion/QuestionableCompanion/Plugin.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Numerics; using System.Reflection; +using System.Threading.Tasks; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.Command; using Dalamud.Interface.Windowing; @@ -129,6 +130,12 @@ public sealed class Plugin : IDalamudPlugin, IDisposable private ErrorRecoveryService ErrorRecoveryService { get; init; } + private LANHelperServer? LANHelperServer { get; set; } + + private LANHelperClient? LANHelperClient { get; set; } + + private ARRTrialAutomationService ARRTrialAutomation { get; init; } + private ConfigWindow ConfigWindow { get; init; } private NewMainWindow NewMainWindow { get; init; } @@ -178,17 +185,67 @@ public sealed class Plugin : IDalamudPlugin, IDisposable DeathHandler = new DeathHandlerService(Condition, Log, ClientState, CommandManager, Framework, Configuration, GameGui, DataManager); Log.Debug("[Plugin] Initializing MemoryHelper..."); MemoryHelper = new MemoryHelper(Log, GameInterop); + if (Configuration.EnableLANHelpers) + { + Log.Information("[Plugin] LAN Helper System ENABLED - Initializing..."); + LANHelperClient = new LANHelperClient(Log, ClientState, Framework, Configuration); + if (Configuration.StartLANServer) + { + Log.Information("[Plugin] Starting LAN Helper Server..."); + LANHelperServer = new LANHelperServer(Log, ClientState, Framework, Configuration, PartyInviteAutoAccept, CommandManager, this); + LANHelperServer.Start(); + } + Task.Run(async delegate + { + await Task.Delay(2000); + await LANHelperClient.Initialize(); + }); + } + else + { + Log.Debug("[Plugin] LAN Helper System disabled"); + } Log.Debug("[Plugin] Initializing HelperManager..."); - HelperManager = new HelperManager(Configuration, Log, CommandManager, Condition, ClientState, Framework, PartyInviteService, MultiClientIPC, CrossProcessIPC, PartyInviteAutoAccept, MemoryHelper); + HelperManager = new HelperManager(Configuration, Log, CommandManager, Condition, ClientState, Framework, PartyInviteService, MultiClientIPC, CrossProcessIPC, PartyInviteAutoAccept, MemoryHelper, LANHelperClient, PartyList); Log.Debug("[Plugin] Initializing DungeonAutomation..."); - DungeonAutomation = new DungeonAutomationService(Condition, Log, ClientState, CommandManager, Framework, GameGui, Configuration, HelperManager, MemoryHelper, QuestionableIPC); + DungeonAutomation = new DungeonAutomationService(Condition, Log, ClientState, CommandManager, Framework, GameGui, Configuration, HelperManager, MemoryHelper, QuestionableIPC, CrossProcessIPC, MultiClientIPC); Log.Debug("[Plugin] Initializing StepsOfFaithHandler..."); StepsOfFaithHandler = new StepsOfFaithHandler(Condition, Log, ClientState, CommandManager, Framework, Configuration); Log.Debug("[Plugin] Initializing MSQProgressionService..."); MSQProgressionService = new MSQProgressionService(DataManager, Log, QuestDetection, ObjectTable, Framework); Log.Debug("[Plugin] Initializing ChauffeurMode..."); ChauffeurMode = new ChauffeurModeService(Configuration, Log, ClientState, Condition, Framework, CommandManager, DataManager, PartyList, ObjectTable, QuestionableIPC, CrossProcessIPC, PartyInviteService, PartyInviteAutoAccept, PluginInterface, MemoryHelper, MovementMonitor); + Log.Debug("[Plugin] Initializing ARRTrialAutomation..."); + ARRTrialAutomation = new ARRTrialAutomationService(Log, Framework, CommandManager, ChatGui, Configuration, QuestionableIPC, SubmarineManager, HelperManager, PartyList, Condition, MemoryHelper); + QuestDetection.QuestCompleted += delegate(uint questId, string questName) + { + if (questId == 89) + { + Log.Information("[Plugin] Quest 89 completed - triggering ARR Primal check"); + ARRTrialAutomation.OnTriggerQuestComplete(); + } + ARRTrialAutomation.OnQuestComplete(questId); + }; + Log.Debug("[Plugin] ARRTrialAutomation wired to QuestDetection.QuestCompleted"); MovementMonitor.SetChauffeurMode(ChauffeurMode); + if (LANHelperClient != null) + { + LANHelperClient.OnChauffeurMessageReceived += delegate(object? sender, LANHelperClient.ChauffeurMessageEventArgs args) + { + Framework.RunOnFrameworkThread(delegate + { + if (args.Type == LANMessageType.CHAUFFEUR_HELPER_READY_FOR_MOUNT) + { + ChauffeurMode.OnChauffeurMountReady(args.Data.QuesterName, args.Data.QuesterWorldId); + } + else if (args.Type == LANMessageType.CHAUFFEUR_HELPER_ARRIVED_DEST) + { + ChauffeurMode.OnChauffeurArrived(args.Data.QuesterName, args.Data.QuesterWorldId); + } + }); + }; + Log.Debug("[Plugin] LANHelperClient Chauffeur events wired to ChauffeurMode"); + } Log.Debug("[Plugin] Initializing AR Post Process Event Quest Service..."); EventQuestResolver eventQuestResolver = new EventQuestResolver(DataManager, Log); ARPostProcessService = new ARPostProcessEventQuestService(PluginInterface, QuestionableIPC, eventQuestResolver, Configuration, Log, Framework, CommandManager, LifestreamIPC); @@ -198,7 +255,7 @@ public sealed class Plugin : IDalamudPlugin, IDisposable AlliedSocietyRotationService = new AlliedSocietyRotationService(QuestionableIPC, AlliedSocietyDatabase, AlliedSocietyQuestSelector, AutoRetainerIPC, Configuration, Log, Framework, CommandManager, Condition, ClientState); AlliedSocietyPriorityWindow = new AlliedSocietyPriorityWindow(Configuration, AlliedSocietyDatabase); Log.Debug("[Plugin] Initializing Error Recovery Service..."); - ErrorRecoveryService = new ErrorRecoveryService(Log, GameInterop, ClientState, AutoRetainerIPC, Framework, GameGui); + ErrorRecoveryService = new ErrorRecoveryService(Log, GameInterop, ClientState, Framework, GameGui, AutoRetainerIPC); QuestRotationService.SetErrorRecoveryService(ErrorRecoveryService); MultiClientIPC.OnChatMessageReceived += OnMultiClientChatReceived; CrossProcessIPC.OnChatMessageReceived += OnMultiClientChatReceived; @@ -211,6 +268,7 @@ public sealed class Plugin : IDalamudPlugin, IDisposable QuestRotationService.SetDeathHandler(DeathHandler); QuestRotationService.SetDungeonAutomation(DungeonAutomation); QuestRotationService.SetStepsOfFaithHandler(StepsOfFaithHandler); + DungeonAutomation.SetRotationActiveChecker(() => QuestRotationService.IsRotationActive); Log.Debug("[Plugin] Initializing DataCenterService..."); DataCenterService dataCenterService = new DataCenterService(DataManager, Log); Log.Debug($"[Plugin] Loaded {Configuration.StopPoints?.Count ?? 0} stop points from config"); @@ -295,6 +353,35 @@ public sealed class Plugin : IDalamudPlugin, IDisposable } } + public LANHelperClient? GetLANHelperClient() + { + return LANHelperClient; + } + + public void ToggleLANServer(bool enable) + { + if (enable) + { + if (LANHelperServer == null) + { + Log.Information("[Plugin] Starting LAN Helper Server (Runtime)..."); + LANHelperServer = new LANHelperServer(Log, ClientState, Framework, Configuration, PartyInviteAutoAccept, CommandManager, this); + LANHelperServer.Start(); + } + else if (!LANHelperServer.IsRunning) + { + LANHelperServer.Start(); + } + } + else if (LANHelperServer != null) + { + Log.Information("[Plugin] Stopping LAN Helper Server (Runtime)..."); + LANHelperServer.Stop(); + LANHelperServer.Dispose(); + LANHelperServer = null; + } + } + private void SaveEventQuestCompletionData() { if (EventQuestService != null) @@ -401,6 +488,8 @@ public sealed class Plugin : IDalamudPlugin, IDisposable QuestTrackingService?.Dispose(); QuestDetection?.Dispose(); HelperManager?.Dispose(); + LANHelperServer?.Dispose(); + LANHelperClient?.Dispose(); PartyInviteAutoAccept?.Dispose(); CrossProcessIPC?.Dispose(); MultiClientIPC?.Dispose(); @@ -456,13 +545,15 @@ public sealed class Plugin : IDalamudPlugin, IDisposable private void OnCommand(string command, string args) { string argLower = args.Trim().ToLower(); - if (argLower == "dbg") + switch (argLower) { + case "arrtrials": + ARRTrialAutomation.StartTrialChain(); + return; + case "dbg": DebugWindow.Toggle(); return; - } - if (argLower == "task") - { + case "task": TestGetCurrentTask(); return; } @@ -701,18 +792,69 @@ public sealed class Plugin : IDalamudPlugin, IDisposable Log.Information("========================================"); return; } - List<(string, ushort)> availableHelpers = HelperManager.GetAvailableHelpers(); - if (availableHelpers.Count == 0) + string modeText = Configuration.HelperSelection switch { - Log.Error("[TEST] No helpers discovered via IPC!"); - Log.Error("[TEST] Make sure helper clients are running with 'I'm a High-Level Helper' enabled"); - Log.Information("========================================"); - return; + HelperSelectionMode.Auto => "Auto (First Available)", + HelperSelectionMode.Dropdown => "Dropdown (Select Specific Helper)", + HelperSelectionMode.ManualInput => "Manual Input", + _ => "Unknown", + }; + Log.Information("[TEST] Current Selection Mode: " + modeText); + Log.Information("[TEST] ----------------------------------------"); + if (Configuration.HelperSelection == HelperSelectionMode.ManualInput) + { + if (string.IsNullOrEmpty(Configuration.ManualHelperName)) + { + Log.Error("[TEST] Manual Input mode selected, but no helper name configured!"); + Log.Error("[TEST] Please configure a helper name in Settings (format: CharacterName@WorldName)"); + } + else + { + Log.Information("[TEST] Manual Helper: " + Configuration.ManualHelperName); + Log.Information("[TEST] This helper will be invited directly (no IPC wait required)"); + } } - Log.Information($"[TEST] Auto-discovered helpers: {availableHelpers.Count}"); - foreach (var (name, worldId) in availableHelpers) + else if (Configuration.HelperSelection == HelperSelectionMode.Dropdown) { - Log.Information($"[TEST] - {name}@{worldId}"); + List<(string, ushort)> availableHelpers = HelperManager.GetAvailableHelpers(); + if (availableHelpers.Count == 0) + { + Log.Warning("[TEST] No helpers discovered via IPC!"); + Log.Warning("[TEST] Make sure helper clients are running with 'I'm a High-Level Helper' enabled"); + } + else + { + Log.Information($"[TEST] Auto-discovered helpers: {availableHelpers.Count}"); + foreach (var (name, worldId) in availableHelpers) + { + Log.Information($"[TEST] - {name}@{worldId}"); + } + } + if (string.IsNullOrEmpty(Configuration.PreferredHelper)) + { + Log.Warning("[TEST] Dropdown mode selected, but no specific helper chosen!"); + Log.Warning("[TEST] Please select a helper from the dropdown in Settings"); + } + else + { + Log.Information("[TEST] Selected Helper: " + Configuration.PreferredHelper); + } + } + else + { + List<(string, ushort)> availableHelpers2 = HelperManager.GetAvailableHelpers(); + if (availableHelpers2.Count == 0) + { + Log.Error("[TEST] No helpers discovered via IPC!"); + Log.Error("[TEST] Make sure helper clients are running with 'I'm a High-Level Helper' enabled"); + Log.Information("========================================"); + return; + } + Log.Information($"[TEST] Auto-discovered helpers: {availableHelpers2.Count}"); + foreach (var (name2, worldId2) in availableHelpers2) + { + Log.Information($"[TEST] - {name2}@{worldId2}"); + } } Log.Information("[TEST] Invoking HelperManager.InviteHelpers()..."); HelperManager.InviteHelpers(); @@ -1115,6 +1257,11 @@ public sealed class Plugin : IDalamudPlugin, IDisposable return HelperManager; } + public LANHelperServer? GetLANHelperServer() + { + return LANHelperServer; + } + public DungeonAutomationService? GetDungeonAutomation() { return DungeonAutomation;