qstcompanion v1.0.1
This commit is contained in:
parent
3e10cbbbf2
commit
44c67ab71b
79 changed files with 21148 additions and 0 deletions
8
QuestionableCompanion/--y__InlineArray5.cs
Normal file
8
QuestionableCompanion/--y__InlineArray5.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Auto)]
|
||||||
|
[InlineArray(5)]
|
||||||
|
internal struct _003C_003Ey__InlineArray5<T>
|
||||||
|
{
|
||||||
|
}
|
||||||
8
QuestionableCompanion/--y__InlineArray7.cs
Normal file
8
QuestionableCompanion/--y__InlineArray7.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Auto)]
|
||||||
|
[InlineArray(7)]
|
||||||
|
internal struct _003C_003Ey__InlineArray7<T>
|
||||||
|
{
|
||||||
|
}
|
||||||
0
QuestionableCompanion/-PrivateImplementationDetails-.cs
Normal file
0
QuestionableCompanion/-PrivateImplementationDetails-.cs
Normal file
2902
QuestionableCompanion/ChauffeurModeService.cs
Normal file
2902
QuestionableCompanion/ChauffeurModeService.cs
Normal file
File diff suppressed because it is too large
Load diff
37
QuestionableCompanion/QSTCompanion.csproj
Normal file
37
QuestionableCompanion/QSTCompanion.csproj
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<AssemblyName>QSTCompanion</AssemblyName>
|
||||||
|
<GenerateAssemblyInfo>False</GenerateAssemblyInfo>
|
||||||
|
<TargetFramework>netcoreapp9.0</TargetFramework>
|
||||||
|
<PlatformTarget>x64</PlatformTarget>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup>
|
||||||
|
<LangVersion>12.0</LangVersion>
|
||||||
|
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup />
|
||||||
|
<ItemGroup />
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Dalamud">
|
||||||
|
<HintPath>..\..\..\..\..\ffxiv\alyssile-xivl\addon\Hooks\dev\Dalamud.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="Newtonsoft.Json">
|
||||||
|
<HintPath>..\..\..\..\..\ffxiv\alyssile-xivl\addon\Hooks\dev\Newtonsoft.Json.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="Lumina">
|
||||||
|
<HintPath>..\..\..\..\..\ffxiv\alyssile-xivl\addon\Hooks\dev\Lumina.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="Lumina.Excel">
|
||||||
|
<HintPath>..\..\..\..\..\ffxiv\alyssile-xivl\addon\Hooks\dev\Lumina.Excel.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="FFXIVClientStructs">
|
||||||
|
<HintPath>..\..\..\..\..\ffxiv\alyssile-xivl\addon\Hooks\dev\FFXIVClientStructs.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="Dalamud.Bindings.ImGui">
|
||||||
|
<HintPath>..\..\..\..\..\ffxiv\alyssile-xivl\addon\Hooks\dev\Dalamud.Bindings.ImGui.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="InteropGenerator.Runtime">
|
||||||
|
<HintPath>..\..\..\..\..\ffxiv\alyssile-xivl\addon\Hooks\dev\InteropGenerator.Runtime.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
namespace QuestionableCompanion.Data;
|
||||||
|
|
||||||
|
public class ExpansionProgress
|
||||||
|
{
|
||||||
|
public MSQExpansionData.Expansion Expansion { get; set; }
|
||||||
|
|
||||||
|
public int CompletedCount { get; set; }
|
||||||
|
|
||||||
|
public int ExpectedCount { get; set; }
|
||||||
|
|
||||||
|
public float Percentage { get; set; }
|
||||||
|
|
||||||
|
public bool IsComplete { get; set; }
|
||||||
|
|
||||||
|
public string ExpansionName => MSQExpansionData.GetExpansionName(Expansion);
|
||||||
|
|
||||||
|
public string ExpansionShortName => MSQExpansionData.GetExpansionShortName(Expansion);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,479 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Data;
|
||||||
|
|
||||||
|
public static class MSQExpansionData
|
||||||
|
{
|
||||||
|
public enum Expansion
|
||||||
|
{
|
||||||
|
ARealmReborn,
|
||||||
|
Heavensward,
|
||||||
|
Stormblood,
|
||||||
|
Shadowbringers,
|
||||||
|
Endwalker,
|
||||||
|
Dawntrail
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly Dictionary<Expansion, HashSet<uint>> ExpansionQuests = new Dictionary<Expansion, HashSet<uint>>
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Expansion.ARealmReborn,
|
||||||
|
new HashSet<uint>()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Expansion.Heavensward,
|
||||||
|
new HashSet<uint>()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Expansion.Stormblood,
|
||||||
|
new HashSet<uint>()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Expansion.Shadowbringers,
|
||||||
|
new HashSet<uint>()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Expansion.Endwalker,
|
||||||
|
new HashSet<uint>()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Expansion.Dawntrail,
|
||||||
|
new HashSet<uint>()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Dictionary<Expansion, int> ExpectedQuestCounts = new Dictionary<Expansion, int>
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Expansion.ARealmReborn,
|
||||||
|
200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Expansion.Heavensward,
|
||||||
|
100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Expansion.Stormblood,
|
||||||
|
100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Expansion.Shadowbringers,
|
||||||
|
100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Expansion.Endwalker,
|
||||||
|
100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Expansion.Dawntrail,
|
||||||
|
100
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Dictionary<Expansion, string> ExpansionNames = new Dictionary<Expansion, string>
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Expansion.ARealmReborn,
|
||||||
|
"A Realm Reborn"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Expansion.Heavensward,
|
||||||
|
"Heavensward"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Expansion.Stormblood,
|
||||||
|
"Stormblood"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Expansion.Shadowbringers,
|
||||||
|
"Shadowbringers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Expansion.Endwalker,
|
||||||
|
"Endwalker"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Expansion.Dawntrail,
|
||||||
|
"Dawntrail"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Dictionary<Expansion, string> ExpansionShortNames = new Dictionary<Expansion, string>
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Expansion.ARealmReborn,
|
||||||
|
"ARR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Expansion.Heavensward,
|
||||||
|
"HW"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Expansion.Stormblood,
|
||||||
|
"SB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Expansion.Shadowbringers,
|
||||||
|
"ShB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Expansion.Endwalker,
|
||||||
|
"EW"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Expansion.Dawntrail,
|
||||||
|
"DT"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static void RegisterQuest(uint questId, Expansion expansion)
|
||||||
|
{
|
||||||
|
if (ExpansionQuests.TryGetValue(expansion, out HashSet<uint> quests))
|
||||||
|
{
|
||||||
|
quests.Add(questId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ClearQuests()
|
||||||
|
{
|
||||||
|
foreach (HashSet<uint> value in ExpansionQuests.Values)
|
||||||
|
{
|
||||||
|
value.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Expansion GetExpansionForQuest(uint questId)
|
||||||
|
{
|
||||||
|
foreach (var (expansion2, hashSet2) in ExpansionQuests)
|
||||||
|
{
|
||||||
|
if (hashSet2.Contains(questId))
|
||||||
|
{
|
||||||
|
return expansion2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Expansion.ARealmReborn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlySet<uint> GetQuestsForExpansion(Expansion expansion)
|
||||||
|
{
|
||||||
|
if (!ExpansionQuests.TryGetValue(expansion, out HashSet<uint> quests))
|
||||||
|
{
|
||||||
|
return new HashSet<uint>();
|
||||||
|
}
|
||||||
|
return quests;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int GetExpectedQuestCount(Expansion expansion)
|
||||||
|
{
|
||||||
|
if (!ExpectedQuestCounts.TryGetValue(expansion, out var count))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetExpansionName(Expansion expansion)
|
||||||
|
{
|
||||||
|
if (!ExpansionNames.TryGetValue(expansion, out string name))
|
||||||
|
{
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetExpansionShortName(Expansion expansion)
|
||||||
|
{
|
||||||
|
if (!ExpansionShortNames.TryGetValue(expansion, out string name))
|
||||||
|
{
|
||||||
|
return "???";
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<Expansion> GetAllExpansions()
|
||||||
|
{
|
||||||
|
return from e in Enum.GetValues<Expansion>()
|
||||||
|
orderby (int)e
|
||||||
|
select e;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int GetCompletedQuestCountForExpansion(IEnumerable<uint> completedQuestIds, Expansion expansion)
|
||||||
|
{
|
||||||
|
IReadOnlySet<uint> expansionQuests = GetQuestsForExpansion(expansion);
|
||||||
|
return completedQuestIds.Count((uint qId) => expansionQuests.Contains(qId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe static (Expansion expansion, string debugInfo) GetCurrentExpansionFromGameWithDebug()
|
||||||
|
{
|
||||||
|
StringBuilder debug = new StringBuilder();
|
||||||
|
debug.AppendLine("=== AGENT SCENARIO TREE DEBUG ===");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AgentScenarioTree* agentScenarioTree = AgentScenarioTree.Instance();
|
||||||
|
StringBuilder stringBuilder = debug;
|
||||||
|
StringBuilder stringBuilder2 = stringBuilder;
|
||||||
|
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(30, 1, stringBuilder);
|
||||||
|
handler.AppendLiteral("AgentScenarioTree.Instance(): ");
|
||||||
|
handler.AppendFormatted((agentScenarioTree != null) ? "OK" : "NULL");
|
||||||
|
stringBuilder2.AppendLine(ref handler);
|
||||||
|
if (agentScenarioTree == null)
|
||||||
|
{
|
||||||
|
debug.AppendLine("ERROR: AgentScenarioTree is NULL!");
|
||||||
|
return (expansion: Expansion.ARealmReborn, debugInfo: debug.ToString());
|
||||||
|
}
|
||||||
|
stringBuilder = debug;
|
||||||
|
StringBuilder stringBuilder3 = stringBuilder;
|
||||||
|
handler = new StringBuilder.AppendInterpolatedStringHandler(25, 1, stringBuilder);
|
||||||
|
handler.AppendLiteral("AgentScenarioTree->Data: ");
|
||||||
|
handler.AppendFormatted((agentScenarioTree->Data != null) ? "OK" : "NULL");
|
||||||
|
stringBuilder3.AppendLine(ref handler);
|
||||||
|
if (agentScenarioTree->Data == null)
|
||||||
|
{
|
||||||
|
debug.AppendLine("ERROR: AgentScenarioTree->Data is NULL!");
|
||||||
|
return (expansion: Expansion.ARealmReborn, debugInfo: debug.ToString());
|
||||||
|
}
|
||||||
|
ushort currentQuest = agentScenarioTree->Data->CurrentScenarioQuest;
|
||||||
|
ushort completedQuest = agentScenarioTree->Data->CompleteScenarioQuest;
|
||||||
|
stringBuilder = debug;
|
||||||
|
StringBuilder stringBuilder4 = stringBuilder;
|
||||||
|
handler = new StringBuilder.AppendInterpolatedStringHandler(33, 2, stringBuilder);
|
||||||
|
handler.AppendLiteral("CurrentScenarioQuest (raw): ");
|
||||||
|
handler.AppendFormatted(currentQuest);
|
||||||
|
handler.AppendLiteral(" (0x");
|
||||||
|
handler.AppendFormatted(currentQuest, "X4");
|
||||||
|
handler.AppendLiteral(")");
|
||||||
|
stringBuilder4.AppendLine(ref handler);
|
||||||
|
stringBuilder = debug;
|
||||||
|
StringBuilder stringBuilder5 = stringBuilder;
|
||||||
|
handler = new StringBuilder.AppendInterpolatedStringHandler(34, 2, stringBuilder);
|
||||||
|
handler.AppendLiteral("CompleteScenarioQuest (raw): ");
|
||||||
|
handler.AppendFormatted(completedQuest);
|
||||||
|
handler.AppendLiteral(" (0x");
|
||||||
|
handler.AppendFormatted(completedQuest, "X4");
|
||||||
|
handler.AppendLiteral(")");
|
||||||
|
stringBuilder5.AppendLine(ref handler);
|
||||||
|
ushort questToCheck = ((currentQuest != 0) ? currentQuest : completedQuest);
|
||||||
|
stringBuilder = debug;
|
||||||
|
StringBuilder stringBuilder6 = stringBuilder;
|
||||||
|
handler = new StringBuilder.AppendInterpolatedStringHandler(25, 2, stringBuilder);
|
||||||
|
handler.AppendLiteral("Quest to check: ");
|
||||||
|
handler.AppendFormatted(questToCheck);
|
||||||
|
handler.AppendLiteral(" (using ");
|
||||||
|
handler.AppendFormatted((currentQuest != 0) ? "Current" : "Completed");
|
||||||
|
handler.AppendLiteral(")");
|
||||||
|
stringBuilder6.AppendLine(ref handler);
|
||||||
|
if (questToCheck == 0)
|
||||||
|
{
|
||||||
|
debug.AppendLine("WARNING: Both CurrentScenarioQuest and CompleteScenarioQuest are 0!");
|
||||||
|
return (expansion: Expansion.ARealmReborn, debugInfo: debug.ToString());
|
||||||
|
}
|
||||||
|
uint questId = (uint)(questToCheck | 0x10000);
|
||||||
|
stringBuilder = debug;
|
||||||
|
StringBuilder stringBuilder7 = stringBuilder;
|
||||||
|
handler = new StringBuilder.AppendInterpolatedStringHandler(25, 2, stringBuilder);
|
||||||
|
handler.AppendLiteral("Converted Quest ID: ");
|
||||||
|
handler.AppendFormatted(questId);
|
||||||
|
handler.AppendLiteral(" (0x");
|
||||||
|
handler.AppendFormatted(questId, "X8");
|
||||||
|
handler.AppendLiteral(")");
|
||||||
|
stringBuilder7.AppendLine(ref handler);
|
||||||
|
Expansion expansion = GetExpansionForQuest(questId);
|
||||||
|
stringBuilder = debug;
|
||||||
|
StringBuilder stringBuilder8 = stringBuilder;
|
||||||
|
handler = new StringBuilder.AppendInterpolatedStringHandler(22, 2, stringBuilder);
|
||||||
|
handler.AppendLiteral("Expansion for Quest ");
|
||||||
|
handler.AppendFormatted(questId);
|
||||||
|
handler.AppendLiteral(": ");
|
||||||
|
handler.AppendFormatted(GetExpansionName(expansion));
|
||||||
|
stringBuilder8.AppendLine(ref handler);
|
||||||
|
IReadOnlySet<uint> expansionQuests = GetQuestsForExpansion(expansion);
|
||||||
|
bool isRegistered = expansionQuests.Contains(questId);
|
||||||
|
stringBuilder = debug;
|
||||||
|
StringBuilder stringBuilder9 = stringBuilder;
|
||||||
|
handler = new StringBuilder.AppendInterpolatedStringHandler(23, 3, stringBuilder);
|
||||||
|
handler.AppendLiteral("Quest ");
|
||||||
|
handler.AppendFormatted(questId);
|
||||||
|
handler.AppendLiteral(" registered in ");
|
||||||
|
handler.AppendFormatted(expansion);
|
||||||
|
handler.AppendLiteral(": ");
|
||||||
|
handler.AppendFormatted(isRegistered);
|
||||||
|
stringBuilder9.AppendLine(ref handler);
|
||||||
|
if (!isRegistered)
|
||||||
|
{
|
||||||
|
stringBuilder = debug;
|
||||||
|
StringBuilder stringBuilder10 = stringBuilder;
|
||||||
|
handler = new StringBuilder.AppendInterpolatedStringHandler(48, 1, stringBuilder);
|
||||||
|
handler.AppendLiteral("WARNING: Quest ");
|
||||||
|
handler.AppendFormatted(questId);
|
||||||
|
handler.AppendLiteral(" is NOT in our registered quests!");
|
||||||
|
stringBuilder10.AppendLine(ref handler);
|
||||||
|
stringBuilder = debug;
|
||||||
|
StringBuilder stringBuilder11 = stringBuilder;
|
||||||
|
handler = new StringBuilder.AppendInterpolatedStringHandler(30, 2, stringBuilder);
|
||||||
|
handler.AppendLiteral("Total registered quests for ");
|
||||||
|
handler.AppendFormatted(expansion);
|
||||||
|
handler.AppendLiteral(": ");
|
||||||
|
handler.AppendFormatted(expansionQuests.Count);
|
||||||
|
stringBuilder11.AppendLine(ref handler);
|
||||||
|
foreach (Expansion exp in GetAllExpansions())
|
||||||
|
{
|
||||||
|
if (GetQuestsForExpansion(exp).Contains(questId))
|
||||||
|
{
|
||||||
|
stringBuilder = debug;
|
||||||
|
StringBuilder stringBuilder12 = stringBuilder;
|
||||||
|
handler = new StringBuilder.AppendInterpolatedStringHandler(10, 1, stringBuilder);
|
||||||
|
handler.AppendLiteral("FOUND in ");
|
||||||
|
handler.AppendFormatted(exp);
|
||||||
|
handler.AppendLiteral("!");
|
||||||
|
stringBuilder12.AppendLine(ref handler);
|
||||||
|
expansion = exp;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stringBuilder = debug;
|
||||||
|
StringBuilder stringBuilder13 = stringBuilder;
|
||||||
|
handler = new StringBuilder.AppendInterpolatedStringHandler(25, 1, stringBuilder);
|
||||||
|
handler.AppendLiteral(">>> FINAL EXPANSION: ");
|
||||||
|
handler.AppendFormatted(GetExpansionName(expansion));
|
||||||
|
handler.AppendLiteral(" <<<");
|
||||||
|
stringBuilder13.AppendLine(ref handler);
|
||||||
|
return (expansion: expansion, debugInfo: debug.ToString());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
StringBuilder stringBuilder = debug;
|
||||||
|
StringBuilder stringBuilder14 = stringBuilder;
|
||||||
|
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(11, 1, stringBuilder);
|
||||||
|
handler.AppendLiteral("EXCEPTION: ");
|
||||||
|
handler.AppendFormatted(ex.Message);
|
||||||
|
stringBuilder14.AppendLine(ref handler);
|
||||||
|
stringBuilder = debug;
|
||||||
|
StringBuilder stringBuilder15 = stringBuilder;
|
||||||
|
handler = new StringBuilder.AppendInterpolatedStringHandler(7, 1, stringBuilder);
|
||||||
|
handler.AppendLiteral("Stack: ");
|
||||||
|
handler.AppendFormatted(ex.StackTrace);
|
||||||
|
stringBuilder15.AppendLine(ref handler);
|
||||||
|
return (expansion: Expansion.ARealmReborn, debugInfo: debug.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Expansion GetCurrentExpansionFromGame()
|
||||||
|
{
|
||||||
|
return GetCurrentExpansionFromGameWithDebug().expansion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Expansion GetCurrentExpansion(IEnumerable<uint> completedQuestIds)
|
||||||
|
{
|
||||||
|
List<uint> questList = completedQuestIds.ToList();
|
||||||
|
if (questList.Count == 0)
|
||||||
|
{
|
||||||
|
return Expansion.ARealmReborn;
|
||||||
|
}
|
||||||
|
foreach (Expansion expansion in GetAllExpansions().Reverse().ToList())
|
||||||
|
{
|
||||||
|
IReadOnlySet<uint> expansionQuests = GetQuestsForExpansion(expansion);
|
||||||
|
if (questList.Where((uint qId) => expansionQuests.Contains(qId)).ToList().Count > 0)
|
||||||
|
{
|
||||||
|
return expansion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Expansion.ARealmReborn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetExpansionDetectionDebugInfo(IEnumerable<uint> completedQuestIds)
|
||||||
|
{
|
||||||
|
List<uint> questList = completedQuestIds.ToList();
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
result.AppendLine("=== EXPANSION DETECTION DEBUG ===");
|
||||||
|
StringBuilder stringBuilder = result;
|
||||||
|
StringBuilder stringBuilder2 = stringBuilder;
|
||||||
|
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(24, 1, stringBuilder);
|
||||||
|
handler.AppendLiteral("Total completed quests: ");
|
||||||
|
handler.AppendFormatted(questList.Count);
|
||||||
|
stringBuilder2.AppendLine(ref handler);
|
||||||
|
result.AppendLine("");
|
||||||
|
result.AppendLine("Checking expansions from highest to lowest:");
|
||||||
|
result.AppendLine("");
|
||||||
|
foreach (Expansion expansion in GetAllExpansions().Reverse())
|
||||||
|
{
|
||||||
|
IReadOnlySet<uint> expansionQuests = GetQuestsForExpansion(expansion);
|
||||||
|
List<uint> completedInExpansion = questList.Where((uint qId) => expansionQuests.Contains(qId)).ToList();
|
||||||
|
float percentage = ((expansionQuests.Count > 0) ? ((float)completedInExpansion.Count / (float)expansionQuests.Count * 100f) : 0f);
|
||||||
|
stringBuilder = result;
|
||||||
|
StringBuilder stringBuilder3 = stringBuilder;
|
||||||
|
handler = new StringBuilder.AppendInterpolatedStringHandler(4, 2, stringBuilder);
|
||||||
|
handler.AppendFormatted(GetExpansionName(expansion));
|
||||||
|
handler.AppendLiteral(" (");
|
||||||
|
handler.AppendFormatted(GetExpansionShortName(expansion));
|
||||||
|
handler.AppendLiteral("):");
|
||||||
|
stringBuilder3.AppendLine(ref handler);
|
||||||
|
stringBuilder = result;
|
||||||
|
StringBuilder stringBuilder4 = stringBuilder;
|
||||||
|
handler = new StringBuilder.AppendInterpolatedStringHandler(28, 1, stringBuilder);
|
||||||
|
handler.AppendLiteral(" - Total MSQ in expansion: ");
|
||||||
|
handler.AppendFormatted(expansionQuests.Count);
|
||||||
|
stringBuilder4.AppendLine(ref handler);
|
||||||
|
stringBuilder = result;
|
||||||
|
StringBuilder stringBuilder5 = stringBuilder;
|
||||||
|
handler = new StringBuilder.AppendInterpolatedStringHandler(32, 2, stringBuilder);
|
||||||
|
handler.AppendLiteral(" - Completed by character: ");
|
||||||
|
handler.AppendFormatted(completedInExpansion.Count);
|
||||||
|
handler.AppendLiteral(" (");
|
||||||
|
handler.AppendFormatted(percentage, "F1");
|
||||||
|
handler.AppendLiteral("%)");
|
||||||
|
stringBuilder5.AppendLine(ref handler);
|
||||||
|
if (completedInExpansion.Count > 0)
|
||||||
|
{
|
||||||
|
string samples = string.Join(", ", completedInExpansion.OrderByDescending((uint x) => x).Take(5));
|
||||||
|
stringBuilder = result;
|
||||||
|
StringBuilder stringBuilder6 = stringBuilder;
|
||||||
|
handler = new StringBuilder.AppendInterpolatedStringHandler(22, 1, stringBuilder);
|
||||||
|
handler.AppendLiteral(" - Sample Quest IDs: ");
|
||||||
|
handler.AppendFormatted(samples);
|
||||||
|
stringBuilder6.AppendLine(ref handler);
|
||||||
|
result.AppendLine(" >>> HAS COMPLETED QUESTS - WOULD SELECT THIS EXPANSION <<<");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.AppendLine(" - No quests completed in this expansion");
|
||||||
|
}
|
||||||
|
result.AppendLine("");
|
||||||
|
}
|
||||||
|
Expansion currentExpansion = GetCurrentExpansion(questList);
|
||||||
|
result.AppendLine("===========================================");
|
||||||
|
stringBuilder = result;
|
||||||
|
StringBuilder stringBuilder7 = stringBuilder;
|
||||||
|
handler = new StringBuilder.AppendInterpolatedStringHandler(34, 1, stringBuilder);
|
||||||
|
handler.AppendLiteral(">>> FINAL DETECTED EXPANSION: ");
|
||||||
|
handler.AppendFormatted(GetExpansionName(currentExpansion));
|
||||||
|
handler.AppendLiteral(" <<<");
|
||||||
|
stringBuilder7.AppendLine(ref handler);
|
||||||
|
result.AppendLine("===========================================");
|
||||||
|
return result.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ExpansionProgress GetExpansionProgress(IEnumerable<uint> completedQuestIds, Expansion expansion)
|
||||||
|
{
|
||||||
|
int completed = GetCompletedQuestCountForExpansion(completedQuestIds, expansion);
|
||||||
|
int expected = GetExpectedQuestCount(expansion);
|
||||||
|
return new ExpansionProgress
|
||||||
|
{
|
||||||
|
Expansion = expansion,
|
||||||
|
CompletedCount = completed,
|
||||||
|
ExpectedCount = expected,
|
||||||
|
Percentage = ((expected > 0) ? ((float)completed / (float)expected * 100f) : 0f),
|
||||||
|
IsComplete = (completed >= expected)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ExpansionProgress> GetAllExpansionProgress(IEnumerable<uint> completedQuestIds)
|
||||||
|
{
|
||||||
|
return (from exp in GetAllExpansions()
|
||||||
|
select GetExpansionProgress(completedQuestIds, exp)).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
using System;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Helpers;
|
||||||
|
|
||||||
|
public static class ImGuiDragDrop
|
||||||
|
{
|
||||||
|
public static void SetDragDropPayload<T>(string type, T data, ImGuiCond cond = ImGuiCond.None) where T : struct
|
||||||
|
{
|
||||||
|
ReadOnlySpan<byte> span = MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in data, 1));
|
||||||
|
ImGui.SetDragDropPayload(type, span, cond);
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe static bool AcceptDragDropPayload<T>(string type, out T payload, ImGuiDragDropFlags flags = ImGuiDragDropFlags.None) where T : struct
|
||||||
|
{
|
||||||
|
ImGuiPayload* pload = ImGui.AcceptDragDropPayload(type, flags);
|
||||||
|
payload = ((pload != null) ? Unsafe.Read<T>(pload->Data) : default(T));
|
||||||
|
return pload != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
public class AlliedSocietyCharacterStatus
|
||||||
|
{
|
||||||
|
public required string CharacterId { get; set; }
|
||||||
|
|
||||||
|
public AlliedSocietyRotationStatus Status { get; set; }
|
||||||
|
|
||||||
|
public DateTime? LastCompletionDate { get; set; }
|
||||||
|
|
||||||
|
public List<string> ImportedQuestIds { get; set; } = new List<string>();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
public class AlliedSocietyConfiguration
|
||||||
|
{
|
||||||
|
public List<AlliedSocietyPriority> Priorities { get; set; } = new List<AlliedSocietyPriority>();
|
||||||
|
|
||||||
|
public AlliedSocietyQuestMode QuestMode { get; set; }
|
||||||
|
|
||||||
|
public void InitializeDefaults()
|
||||||
|
{
|
||||||
|
Priorities.Clear();
|
||||||
|
for (byte i = 1; i <= 20; i++)
|
||||||
|
{
|
||||||
|
Priorities.Add(new AlliedSocietyPriority
|
||||||
|
{
|
||||||
|
SocietyId = i,
|
||||||
|
Enabled = true,
|
||||||
|
Order = i - 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
public class AlliedSocietyPriority
|
||||||
|
{
|
||||||
|
public required byte SocietyId { get; set; }
|
||||||
|
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
|
||||||
|
public int Order { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
namespace QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
public class AlliedSocietyProgress
|
||||||
|
{
|
||||||
|
public required string CharacterId { get; set; }
|
||||||
|
|
||||||
|
public required byte SocietyId { get; set; }
|
||||||
|
|
||||||
|
public int CurrentRank { get; set; }
|
||||||
|
|
||||||
|
public bool IsMaxRank { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
public enum AlliedSocietyQuestMode
|
||||||
|
{
|
||||||
|
OnlyThreePerSociety,
|
||||||
|
AllAvailableQuests
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
namespace QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
public enum AlliedSocietyRotationPhase
|
||||||
|
{
|
||||||
|
Idle,
|
||||||
|
StartingRotation,
|
||||||
|
ImportingQuests,
|
||||||
|
WaitingForQuestAccept,
|
||||||
|
MonitoringQuests,
|
||||||
|
CheckingCompletion,
|
||||||
|
WaitingForCharacterSwitch,
|
||||||
|
Completed
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
public enum AlliedSocietyRotationStatus
|
||||||
|
{
|
||||||
|
Ready,
|
||||||
|
InProgress,
|
||||||
|
Complete
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class CharacterProgressInfo
|
||||||
|
{
|
||||||
|
public string World { get; set; } = "Unknown";
|
||||||
|
|
||||||
|
public uint LastQuestId { get; set; }
|
||||||
|
|
||||||
|
public string LastQuestName { get; set; } = "—";
|
||||||
|
|
||||||
|
public int CompletedQuestCount { get; set; }
|
||||||
|
|
||||||
|
public DateTime LastUpdatedUtc { get; set; } = DateTime.MinValue;
|
||||||
|
|
||||||
|
public uint LastCompletedMSQId { get; set; }
|
||||||
|
|
||||||
|
public string LastCompletedMSQName { get; set; } = "—";
|
||||||
|
|
||||||
|
public int CompletedMSQCount { get; set; }
|
||||||
|
|
||||||
|
public float MSQCompletionPercentage { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class ExecutionState
|
||||||
|
{
|
||||||
|
public string ActiveProfile { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string CurrentCharacter { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public uint CurrentQuestId { get; set; }
|
||||||
|
|
||||||
|
public string CurrentQuestName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string CurrentSequence { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public ExecutionStatus Status { get; set; }
|
||||||
|
|
||||||
|
public int Progress { get; set; }
|
||||||
|
|
||||||
|
public DateTime LastUpdate { get; set; } = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
namespace QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
public enum ExecutionStatus
|
||||||
|
{
|
||||||
|
Idle,
|
||||||
|
Waiting,
|
||||||
|
Queued,
|
||||||
|
Running,
|
||||||
|
Complete,
|
||||||
|
Failed
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class LogEntry
|
||||||
|
{
|
||||||
|
public DateTime Timestamp { get; set; } = DateTime.Now;
|
||||||
|
|
||||||
|
public LogLevel Level { get; set; }
|
||||||
|
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string FormattedTimestamp => Timestamp.ToString("HH:mm:ss");
|
||||||
|
|
||||||
|
public string FormattedMessage => $"[{FormattedTimestamp}] {GetLevelIcon()} {Message}";
|
||||||
|
|
||||||
|
private string GetLevelIcon()
|
||||||
|
{
|
||||||
|
return Level switch
|
||||||
|
{
|
||||||
|
LogLevel.Info => "→",
|
||||||
|
LogLevel.Success => "✓",
|
||||||
|
LogLevel.Warning => "⏳",
|
||||||
|
LogLevel.Error => "✗",
|
||||||
|
LogLevel.Debug => "▶",
|
||||||
|
_ => "•",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
public enum LogLevel
|
||||||
|
{
|
||||||
|
Info,
|
||||||
|
Success,
|
||||||
|
Warning,
|
||||||
|
Error,
|
||||||
|
Debug
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class QuestConfig
|
||||||
|
{
|
||||||
|
public uint QuestId { get; set; }
|
||||||
|
|
||||||
|
public string QuestName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public TriggerType TriggerType { get; set; } = TriggerType.OnComplete;
|
||||||
|
|
||||||
|
public SequenceConfig SequenceAfterQuest { get; set; } = new SequenceConfig();
|
||||||
|
|
||||||
|
public string NextCharacter { get; set; } = "auto_next";
|
||||||
|
|
||||||
|
public string AssignedCharacter { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class QuestProfile
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "New Profile";
|
||||||
|
|
||||||
|
public List<string> Characters { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
public List<QuestConfig> Quests { get; set; } = new List<QuestConfig>();
|
||||||
|
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
namespace QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
public enum RotationPhase
|
||||||
|
{
|
||||||
|
Idle,
|
||||||
|
InitializingFirstCharacter,
|
||||||
|
WaitingForCharacterLogin,
|
||||||
|
ScanningQuests,
|
||||||
|
CheckingQuestCompletion,
|
||||||
|
DCTraveling,
|
||||||
|
WaitingForQuestStart,
|
||||||
|
Questing,
|
||||||
|
InCombat,
|
||||||
|
InDungeon,
|
||||||
|
HandlingSubmarines,
|
||||||
|
SyncingCharacterData,
|
||||||
|
WaitingForChauffeur,
|
||||||
|
TravellingWithChauffeur,
|
||||||
|
QuestActive,
|
||||||
|
WaitingForNextCharacterSwitch,
|
||||||
|
WaitingBeforeCharacterSwitch,
|
||||||
|
WaitingForHomeworldReturn,
|
||||||
|
Completed,
|
||||||
|
Error
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,249 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
public class RotationState
|
||||||
|
{
|
||||||
|
private readonly object _lock = new object();
|
||||||
|
|
||||||
|
private uint _currentStopQuestId;
|
||||||
|
|
||||||
|
private List<string> _selectedCharacters = new List<string>();
|
||||||
|
|
||||||
|
private string _currentCharacter = "";
|
||||||
|
|
||||||
|
private string _nextCharacter = "";
|
||||||
|
|
||||||
|
private List<string> _completedCharacters = new List<string>();
|
||||||
|
|
||||||
|
private List<string> _remainingCharacters = new List<string>();
|
||||||
|
|
||||||
|
private RotationPhase _phase;
|
||||||
|
|
||||||
|
private DateTime _phaseStartTime = DateTime.Now;
|
||||||
|
|
||||||
|
private string _errorMessage = "";
|
||||||
|
|
||||||
|
private DateTime? _rotationStartTime;
|
||||||
|
|
||||||
|
private bool _hasQuestBeenAccepted;
|
||||||
|
|
||||||
|
private uint? _lastKnownQuestState;
|
||||||
|
|
||||||
|
public uint CurrentStopQuestId
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _currentStopQuestId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_currentStopQuestId = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> SelectedCharacters
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return new List<string>(_selectedCharacters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_selectedCharacters = new List<string>(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CurrentCharacter
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _currentCharacter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_currentCharacter = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string NextCharacter
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _nextCharacter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_nextCharacter = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> CompletedCharacters
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return new List<string>(_completedCharacters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_completedCharacters = new List<string>(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> RemainingCharacters
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return new List<string>(_remainingCharacters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_remainingCharacters = new List<string>(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RotationPhase Phase
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _phase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_phase = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime PhaseStartTime
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _phaseStartTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_phaseStartTime = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ErrorMessage
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _errorMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_errorMessage = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime? RotationStartTime
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _rotationStartTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_rotationStartTime = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasQuestBeenAccepted
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _hasQuestBeenAccepted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_hasQuestBeenAccepted = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint? LastKnownQuestState
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _lastKnownQuestState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_lastKnownQuestState = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class SequenceConfig
|
||||||
|
{
|
||||||
|
public SequenceType Type { get; set; }
|
||||||
|
|
||||||
|
public string Value { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public bool WaitForCompletion { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
public enum SequenceType
|
||||||
|
{
|
||||||
|
QuestionableProfile,
|
||||||
|
InternalAction
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
using System;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class StopPoint
|
||||||
|
{
|
||||||
|
public uint QuestId { get; set; }
|
||||||
|
|
||||||
|
[JsonInclude]
|
||||||
|
[JsonPropertyName("Sequence")]
|
||||||
|
public byte? Sequence { get; set; }
|
||||||
|
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.Now;
|
||||||
|
|
||||||
|
public string DisplayName
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (Sequence.HasValue)
|
||||||
|
{
|
||||||
|
return $"Quest {QuestId} (Seq {Sequence.Value})";
|
||||||
|
}
|
||||||
|
return $"Quest {QuestId}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
public enum TriggerType
|
||||||
|
{
|
||||||
|
OnAccept,
|
||||||
|
OnComplete
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,454 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Dalamud.Plugin.Ipc;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class ARPostProcessEventQuestService : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IDalamudPluginInterface pluginInterface;
|
||||||
|
|
||||||
|
private readonly QuestionableIPC questionableIPC;
|
||||||
|
|
||||||
|
private readonly EventQuestResolver eventQuestResolver;
|
||||||
|
|
||||||
|
private readonly Configuration configuration;
|
||||||
|
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private readonly IFramework framework;
|
||||||
|
|
||||||
|
private readonly ICommandManager commandManager;
|
||||||
|
|
||||||
|
private readonly LifestreamIPC lifestreamIPC;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<object>? characterAdditionalTaskSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<string, object>? characterPostProcessSubscriber;
|
||||||
|
|
||||||
|
private Action? characterAdditionalTaskHandler;
|
||||||
|
|
||||||
|
private Action<string>? characterPostProcessHandler;
|
||||||
|
|
||||||
|
private bool isProcessingEventQuests;
|
||||||
|
|
||||||
|
private DateTime postProcessStartTime;
|
||||||
|
|
||||||
|
private List<string> currentQuestHierarchy = new List<string>();
|
||||||
|
|
||||||
|
private string currentPluginName = string.Empty;
|
||||||
|
|
||||||
|
private string lastTerritoryWaitDetected = string.Empty;
|
||||||
|
|
||||||
|
private DateTime lastTerritoryTeleportTime = DateTime.MinValue;
|
||||||
|
|
||||||
|
private const string PLUGIN_NAME = "QuestionableCompanion";
|
||||||
|
|
||||||
|
private const string AR_CHARACTER_ADDITIONAL_TASK = "AutoRetainer.OnCharacterAdditionalTask";
|
||||||
|
|
||||||
|
private const string AR_CHARACTER_POST_PROCESS_EVENT = "AutoRetainer.OnCharacterReadyForPostprocess";
|
||||||
|
|
||||||
|
private const string AR_FINISH_CHARACTER_POST_PROCESS = "AutoRetainer.FinishCharacterPostprocessRequest";
|
||||||
|
|
||||||
|
private const string AR_REQUEST_CHARACTER_POST_PROCESS = "AutoRetainer.RequestCharacterPostprocess";
|
||||||
|
|
||||||
|
public ARPostProcessEventQuestService(IDalamudPluginInterface pluginInterface, QuestionableIPC questionableIPC, EventQuestResolver eventQuestResolver, Configuration configuration, IPluginLog log, IFramework framework, ICommandManager commandManager, LifestreamIPC lifestreamIPC)
|
||||||
|
{
|
||||||
|
this.pluginInterface = pluginInterface;
|
||||||
|
this.questionableIPC = questionableIPC;
|
||||||
|
this.eventQuestResolver = eventQuestResolver;
|
||||||
|
this.configuration = configuration;
|
||||||
|
this.log = log;
|
||||||
|
this.framework = framework;
|
||||||
|
this.commandManager = commandManager;
|
||||||
|
this.lifestreamIPC = lifestreamIPC;
|
||||||
|
InitializeIPC();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeIPC()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
characterAdditionalTaskSubscriber = pluginInterface.GetIpcSubscriber<object>("AutoRetainer.OnCharacterAdditionalTask");
|
||||||
|
if (characterAdditionalTaskSubscriber == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
characterAdditionalTaskHandler = delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RegisterWithAutoRetainer();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
characterAdditionalTaskSubscriber.Subscribe(characterAdditionalTaskHandler);
|
||||||
|
characterPostProcessSubscriber = pluginInterface.GetIpcSubscriber<string, object>("AutoRetainer.OnCharacterReadyForPostprocess");
|
||||||
|
if (characterPostProcessSubscriber == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
characterPostProcessHandler = delegate(string pluginName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
OnARCharacterPostProcessStarted(pluginName);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
characterPostProcessSubscriber.Subscribe(characterPostProcessHandler);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterWithAutoRetainer()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
pluginInterface.GetIpcSubscriber<string, object>("AutoRetainer.RequestCharacterPostprocess").InvokeAction("QuestionableCompanion");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnARCharacterPostProcessStarted(string pluginName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (pluginName != "QuestionableCompanion")
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!configuration.RunEventQuestsOnARPostProcess)
|
||||||
|
{
|
||||||
|
FinishPostProcess();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentPluginName = pluginName;
|
||||||
|
postProcessStartTime = DateTime.Now;
|
||||||
|
framework.RunOnFrameworkThread(async delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ProcessEventQuestsAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
FinishPostProcess();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
FinishPostProcess();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessEventQuestsAsync()
|
||||||
|
{
|
||||||
|
if (isProcessingEventQuests)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isProcessingEventQuests = true;
|
||||||
|
bool shouldFinishPostProcess = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ = 1;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<string> detectedEventQuests = DetectActiveEventQuests();
|
||||||
|
if (detectedEventQuests.Count == 0)
|
||||||
|
{
|
||||||
|
shouldFinishPostProcess = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentQuestHierarchy = new List<string>(detectedEventQuests);
|
||||||
|
await ImportEventQuestsForPostProcess(detectedEventQuests);
|
||||||
|
await WaitForEventQuestsCompletion(detectedEventQuests);
|
||||||
|
shouldFinishPostProcess = true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
shouldFinishPostProcess = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (shouldFinishPostProcess)
|
||||||
|
{
|
||||||
|
await ClearPriorityQuests();
|
||||||
|
FinishPostProcess();
|
||||||
|
}
|
||||||
|
isProcessingEventQuests = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string> DetectActiveEventQuests()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return questionableIPC.GetCurrentlyActiveEventQuests() ?? new List<string>();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return new List<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ImportEventQuestsForPostProcess(List<string> detectedEventQuests)
|
||||||
|
{
|
||||||
|
List<string> allQuestsToImport = new List<string>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (string questId in detectedEventQuests)
|
||||||
|
{
|
||||||
|
foreach (string quest in await GetQuestHierarchy(questId))
|
||||||
|
{
|
||||||
|
if (!allQuestsToImport.Contains(quest))
|
||||||
|
{
|
||||||
|
allQuestsToImport.Add(quest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!questionableIPC.IsAvailable)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
questionableIPC.ClearQuestPriority();
|
||||||
|
await Task.Delay(500);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
foreach (string questId2 in allQuestsToImport)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
questionableIPC.AddQuestPriority(questId2);
|
||||||
|
await Task.Delay(100);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Task.Delay(500);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/qst start");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<string>> GetQuestHierarchy(string questId)
|
||||||
|
{
|
||||||
|
List<string> hierarchy = new List<string>();
|
||||||
|
HashSet<string> visited = new HashSet<string>();
|
||||||
|
await CollectPrerequisitesRecursive(questId, hierarchy, visited);
|
||||||
|
return hierarchy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CollectPrerequisitesRecursive(string questId, List<string> hierarchy, HashSet<string> visited)
|
||||||
|
{
|
||||||
|
if (visited.Contains(questId))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visited.Add(questId);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<string> prerequisites = eventQuestResolver.ResolveEventQuestDependencies(questId);
|
||||||
|
if (prerequisites.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (string prereq in prerequisites)
|
||||||
|
{
|
||||||
|
await CollectPrerequisitesRecursive(prereq, hierarchy, visited);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hierarchy.Add(questId);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
hierarchy.Add(questId);
|
||||||
|
}
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WaitForEventQuestsCompletion(List<string> originalEventQuests)
|
||||||
|
{
|
||||||
|
TimeSpan maxWaitTime = TimeSpan.FromMinutes(configuration.EventQuestPostProcessTimeoutMinutes);
|
||||||
|
DateTime startTime = DateTime.Now;
|
||||||
|
TimeSpan checkInterval = TimeSpan.FromSeconds(2L);
|
||||||
|
while (DateTime.Now - startTime < maxWaitTime)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CheckForTerritoryWait();
|
||||||
|
if (!questionableIPC.IsAvailable)
|
||||||
|
{
|
||||||
|
await Task.Delay(checkInterval);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bool isRunning = questionableIPC.IsRunning();
|
||||||
|
List<string> currentEventQuests = DetectActiveEventQuests();
|
||||||
|
if (originalEventQuests.Where((string q) => currentEventQuests.Contains(q)).ToList().Count == 0)
|
||||||
|
{
|
||||||
|
if (!isRunning)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/qst stop");
|
||||||
|
});
|
||||||
|
await Task.Delay(500);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
await Task.Delay(checkInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckForTerritoryWait()
|
||||||
|
{
|
||||||
|
if (!questionableIPC.IsRunning())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
object task = questionableIPC.GetCurrentTask();
|
||||||
|
if (task == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!(task is JObject jObject))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JToken taskNameToken = jObject["TaskName"];
|
||||||
|
if (taskNameToken == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
string territoryKey = $"{territoryName}_{territoryId}";
|
||||||
|
double timeSinceLastTeleport = (DateTime.Now - lastTerritoryTeleportTime).TotalSeconds;
|
||||||
|
if (lastTerritoryWaitDetected == territoryKey && timeSinceLastTeleport < 60.0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastTerritoryWaitDetected = territoryKey;
|
||||||
|
lastTerritoryTeleportTime = DateTime.Now;
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/li " + territoryName);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ClearPriorityQuests()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (questionableIPC.IsAvailable)
|
||||||
|
{
|
||||||
|
questionableIPC.ClearQuestPriority();
|
||||||
|
await Task.Delay(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FinishPostProcess()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
pluginInterface.GetIpcSubscriber<object>("AutoRetainer.FinishCharacterPostprocessRequest").InvokeAction();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (characterAdditionalTaskHandler != null && characterAdditionalTaskSubscriber != null)
|
||||||
|
{
|
||||||
|
characterAdditionalTaskSubscriber.Unsubscribe(characterAdditionalTaskHandler);
|
||||||
|
}
|
||||||
|
if (characterPostProcessHandler != null && characterPostProcessSubscriber != null)
|
||||||
|
{
|
||||||
|
characterPostProcessSubscriber.Unsubscribe(characterPostProcessHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,162 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class AlliedSocietyDatabase
|
||||||
|
{
|
||||||
|
private readonly Configuration configuration;
|
||||||
|
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
public AlliedSocietyDatabase(Configuration configuration, IPluginLog log)
|
||||||
|
{
|
||||||
|
this.configuration = configuration;
|
||||||
|
this.log = log;
|
||||||
|
if (configuration.AlliedSociety.RotationConfig.Priorities.Count == 0)
|
||||||
|
{
|
||||||
|
configuration.AlliedSociety.RotationConfig.InitializeDefaults();
|
||||||
|
SaveToConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveToConfig()
|
||||||
|
{
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateCharacterProgress(string characterId, byte societyId, int rank, bool isMaxRank)
|
||||||
|
{
|
||||||
|
if (!configuration.AlliedSociety.CharacterProgress.ContainsKey(characterId))
|
||||||
|
{
|
||||||
|
configuration.AlliedSociety.CharacterProgress[characterId] = new List<AlliedSocietyProgress>();
|
||||||
|
}
|
||||||
|
List<AlliedSocietyProgress> progressList = configuration.AlliedSociety.CharacterProgress[characterId];
|
||||||
|
AlliedSocietyProgress existing = progressList.FirstOrDefault((AlliedSocietyProgress p) => p.SocietyId == societyId);
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
existing.CurrentRank = rank;
|
||||||
|
existing.IsMaxRank = isMaxRank;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
progressList.Add(new AlliedSocietyProgress
|
||||||
|
{
|
||||||
|
CharacterId = characterId,
|
||||||
|
SocietyId = societyId,
|
||||||
|
CurrentRank = rank,
|
||||||
|
IsMaxRank = isMaxRank
|
||||||
|
});
|
||||||
|
}
|
||||||
|
SaveToConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AlliedSocietyProgress? GetProgress(string characterId, byte societyId)
|
||||||
|
{
|
||||||
|
if (configuration.AlliedSociety.CharacterProgress.TryGetValue(characterId, out List<AlliedSocietyProgress> list))
|
||||||
|
{
|
||||||
|
return list.FirstOrDefault((AlliedSocietyProgress p) => p.SocietyId == societyId);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AlliedSocietyCharacterStatus GetCharacterStatus(string characterId)
|
||||||
|
{
|
||||||
|
if (!configuration.AlliedSociety.CharacterStatuses.ContainsKey(characterId))
|
||||||
|
{
|
||||||
|
configuration.AlliedSociety.CharacterStatuses[characterId] = new AlliedSocietyCharacterStatus
|
||||||
|
{
|
||||||
|
CharacterId = characterId,
|
||||||
|
Status = AlliedSocietyRotationStatus.Ready
|
||||||
|
};
|
||||||
|
SaveToConfig();
|
||||||
|
}
|
||||||
|
return configuration.AlliedSociety.CharacterStatuses[characterId];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateCharacterStatus(string characterId, AlliedSocietyRotationStatus status)
|
||||||
|
{
|
||||||
|
GetCharacterStatus(characterId).Status = status;
|
||||||
|
SaveToConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetCharacterComplete(string characterId, DateTime completionDate)
|
||||||
|
{
|
||||||
|
AlliedSocietyCharacterStatus characterStatus = GetCharacterStatus(characterId);
|
||||||
|
characterStatus.Status = AlliedSocietyRotationStatus.Complete;
|
||||||
|
characterStatus.LastCompletionDate = completionDate;
|
||||||
|
characterStatus.ImportedQuestIds.Clear();
|
||||||
|
SaveToConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CheckAndResetExpired(DateTime nextResetDate)
|
||||||
|
{
|
||||||
|
List<string> charactersToReset = GetCharactersNeedingReset(nextResetDate);
|
||||||
|
foreach (string charId in charactersToReset)
|
||||||
|
{
|
||||||
|
log.Information("[AlliedSociety] Resetting status for character " + charId);
|
||||||
|
AlliedSocietyCharacterStatus characterStatus = GetCharacterStatus(charId);
|
||||||
|
characterStatus.Status = AlliedSocietyRotationStatus.Ready;
|
||||||
|
characterStatus.ImportedQuestIds.Clear();
|
||||||
|
}
|
||||||
|
if (charactersToReset.Count > 0)
|
||||||
|
{
|
||||||
|
SaveToConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> GetCharactersNeedingReset(DateTime nextResetDate)
|
||||||
|
{
|
||||||
|
List<string> result = new List<string>();
|
||||||
|
DateTime lastResetDate = nextResetDate.AddDays(-1.0);
|
||||||
|
foreach (KeyValuePair<string, AlliedSocietyCharacterStatus> kvp in configuration.AlliedSociety.CharacterStatuses)
|
||||||
|
{
|
||||||
|
AlliedSocietyCharacterStatus status = kvp.Value;
|
||||||
|
if (status.Status == AlliedSocietyRotationStatus.Ready)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (status.LastCompletionDate.HasValue)
|
||||||
|
{
|
||||||
|
if (status.LastCompletionDate.Value < lastResetDate)
|
||||||
|
{
|
||||||
|
result.Add(kvp.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.Add(kvp.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearAllStatuses()
|
||||||
|
{
|
||||||
|
foreach (KeyValuePair<string, AlliedSocietyCharacterStatus> kvp in configuration.AlliedSociety.CharacterStatuses)
|
||||||
|
{
|
||||||
|
kvp.Value.Status = AlliedSocietyRotationStatus.Ready;
|
||||||
|
kvp.Value.ImportedQuestIds.Clear();
|
||||||
|
}
|
||||||
|
SaveToConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddImportedQuest(string characterId, string questId)
|
||||||
|
{
|
||||||
|
AlliedSocietyCharacterStatus status = GetCharacterStatus(characterId);
|
||||||
|
if (!status.ImportedQuestIds.Contains(questId))
|
||||||
|
{
|
||||||
|
status.ImportedQuestIds.Add(questId);
|
||||||
|
SaveToConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearImportedQuests(string characterId)
|
||||||
|
{
|
||||||
|
GetCharacterStatus(characterId).ImportedQuestIds.Clear();
|
||||||
|
SaveToConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class AlliedSocietyQuestSelector
|
||||||
|
{
|
||||||
|
private readonly QuestionableIPC questionableIpc;
|
||||||
|
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
public AlliedSocietyQuestSelector(QuestionableIPC questionableIpc, IPluginLog log)
|
||||||
|
{
|
||||||
|
this.questionableIpc = questionableIpc;
|
||||||
|
this.log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> SelectQuestsForCharacter(string characterId, int remainingAllowances, List<AlliedSocietyPriority> priorities, AlliedSocietyQuestMode mode)
|
||||||
|
{
|
||||||
|
List<string> selectedQuests = new List<string>();
|
||||||
|
int currentAllowances = remainingAllowances;
|
||||||
|
List<AlliedSocietyPriority> list = (from p in priorities
|
||||||
|
where p.Enabled
|
||||||
|
orderby p.Order
|
||||||
|
select p).ToList();
|
||||||
|
log.Debug($"[AlliedSociety] Selecting quests for {characterId}. Allowances: {remainingAllowances}, Mode: {mode}");
|
||||||
|
foreach (AlliedSocietyPriority priority in list)
|
||||||
|
{
|
||||||
|
if (currentAllowances <= 0)
|
||||||
|
{
|
||||||
|
log.Debug("[AlliedSociety] No allowances left, stopping selection");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
byte societyId = priority.SocietyId;
|
||||||
|
List<string> optimalQuests = questionableIpc.GetAlliedSocietyOptimalQuests(societyId);
|
||||||
|
if (optimalQuests.Count == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<string> readyQuests = new List<string>();
|
||||||
|
foreach (string questId in optimalQuests)
|
||||||
|
{
|
||||||
|
if (questionableIpc.IsReadyToAcceptQuest(questId))
|
||||||
|
{
|
||||||
|
readyQuests.Add(questId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (readyQuests.Count == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (mode == AlliedSocietyQuestMode.OnlyThreePerSociety)
|
||||||
|
{
|
||||||
|
List<string> questsToTake = readyQuests;
|
||||||
|
if (questsToTake.Count > 3)
|
||||||
|
{
|
||||||
|
questsToTake = questsToTake.Skip(questsToTake.Count - 3).Take(3).ToList();
|
||||||
|
}
|
||||||
|
foreach (string questId2 in questsToTake)
|
||||||
|
{
|
||||||
|
if (currentAllowances > 0)
|
||||||
|
{
|
||||||
|
selectedQuests.Add(questId2);
|
||||||
|
currentAllowances--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
foreach (string questId3 in readyQuests)
|
||||||
|
{
|
||||||
|
if (currentAllowances > 0)
|
||||||
|
{
|
||||||
|
selectedQuests.Add(questId3);
|
||||||
|
currentAllowances--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Information($"[AlliedSociety] Selected {selectedQuests.Count} quests for {characterId}");
|
||||||
|
return selectedQuests;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,496 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class AlliedSocietyRotationService : IDisposable
|
||||||
|
{
|
||||||
|
private readonly QuestionableIPC questionableIpc;
|
||||||
|
|
||||||
|
private readonly AlliedSocietyDatabase database;
|
||||||
|
|
||||||
|
private readonly AlliedSocietyQuestSelector questSelector;
|
||||||
|
|
||||||
|
private readonly AutoRetainerIPC autoRetainerIpc;
|
||||||
|
|
||||||
|
private readonly Configuration configuration;
|
||||||
|
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private readonly IFramework framework;
|
||||||
|
|
||||||
|
private readonly ICommandManager commandManager;
|
||||||
|
|
||||||
|
private readonly ICondition condition;
|
||||||
|
|
||||||
|
private readonly IClientState clientState;
|
||||||
|
|
||||||
|
private bool isRotationActive;
|
||||||
|
|
||||||
|
private AlliedSocietyRotationPhase currentPhase;
|
||||||
|
|
||||||
|
private List<string> rotationCharacters = new List<string>();
|
||||||
|
|
||||||
|
private int currentCharacterIndex = -1;
|
||||||
|
|
||||||
|
private string currentCharacterId = string.Empty;
|
||||||
|
|
||||||
|
private DateTime phaseStartTime = DateTime.MinValue;
|
||||||
|
|
||||||
|
private int consecutiveNoQuestsCount;
|
||||||
|
|
||||||
|
private DateTime lastTerritoryTeleportTime = DateTime.MinValue;
|
||||||
|
|
||||||
|
private string lastTerritoryName = string.Empty;
|
||||||
|
|
||||||
|
private DateTime lastUpdate = DateTime.MinValue;
|
||||||
|
|
||||||
|
private const double UpdateIntervalMs = 500.0;
|
||||||
|
|
||||||
|
private DateTime characterSwitchStartTime = DateTime.MinValue;
|
||||||
|
|
||||||
|
private const double CharacterSwitchRetrySeconds = 20.0;
|
||||||
|
|
||||||
|
public bool IsRotationActive => isRotationActive;
|
||||||
|
|
||||||
|
public string CurrentCharacterId => currentCharacterId;
|
||||||
|
|
||||||
|
public AlliedSocietyRotationPhase CurrentPhase => currentPhase;
|
||||||
|
|
||||||
|
public AlliedSocietyRotationService(QuestionableIPC questionableIpc, AlliedSocietyDatabase database, AlliedSocietyQuestSelector questSelector, AutoRetainerIPC autoRetainerIpc, Configuration configuration, IPluginLog log, IFramework framework, ICommandManager commandManager, ICondition condition, IClientState clientState)
|
||||||
|
{
|
||||||
|
this.questionableIpc = questionableIpc;
|
||||||
|
this.database = database;
|
||||||
|
this.questSelector = questSelector;
|
||||||
|
this.autoRetainerIpc = autoRetainerIpc;
|
||||||
|
this.configuration = configuration;
|
||||||
|
this.log = log;
|
||||||
|
this.framework = framework;
|
||||||
|
this.commandManager = commandManager;
|
||||||
|
this.condition = condition;
|
||||||
|
this.clientState = clientState;
|
||||||
|
framework.Update += OnFrameworkUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
framework.Update -= OnFrameworkUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartRotation(List<string> characters)
|
||||||
|
{
|
||||||
|
if (isRotationActive)
|
||||||
|
{
|
||||||
|
log.Warning("[AlliedSociety] Rotation already active");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (characters.Count == 0)
|
||||||
|
{
|
||||||
|
log.Warning("[AlliedSociety] No characters selected for rotation");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.Information($"[AlliedSociety] Starting rotation with {characters.Count} characters");
|
||||||
|
rotationCharacters = new List<string>(characters);
|
||||||
|
isRotationActive = true;
|
||||||
|
currentCharacterIndex = -1;
|
||||||
|
AdvanceToNextCharacter();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopRotation()
|
||||||
|
{
|
||||||
|
if (!isRotationActive)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.Information("[AlliedSociety] Stopping rotation");
|
||||||
|
isRotationActive = false;
|
||||||
|
currentPhase = AlliedSocietyRotationPhase.Idle;
|
||||||
|
currentCharacterId = string.Empty;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/qst stop");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFrameworkUpdate(IFramework framework)
|
||||||
|
{
|
||||||
|
if (!isRotationActive || (DateTime.Now - lastUpdate).TotalMilliseconds < 500.0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastUpdate = DateTime.Now;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
switch (currentPhase)
|
||||||
|
{
|
||||||
|
case AlliedSocietyRotationPhase.StartingRotation:
|
||||||
|
HandleStartingRotation();
|
||||||
|
break;
|
||||||
|
case AlliedSocietyRotationPhase.ImportingQuests:
|
||||||
|
HandleImportingQuests();
|
||||||
|
break;
|
||||||
|
case AlliedSocietyRotationPhase.WaitingForQuestAccept:
|
||||||
|
HandleWaitingForQuestAccept();
|
||||||
|
break;
|
||||||
|
case AlliedSocietyRotationPhase.MonitoringQuests:
|
||||||
|
HandleMonitoringQuests();
|
||||||
|
break;
|
||||||
|
case AlliedSocietyRotationPhase.CheckingCompletion:
|
||||||
|
HandleCheckingCompletion();
|
||||||
|
break;
|
||||||
|
case AlliedSocietyRotationPhase.WaitingForCharacterSwitch:
|
||||||
|
HandleWaitingForCharacterSwitch();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[AlliedSociety] Error in rotation loop: " + ex.Message);
|
||||||
|
StopRotation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleStartingRotation()
|
||||||
|
{
|
||||||
|
if (clientState.LocalContentId == 0L)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
string currentLoggedInChar = autoRetainerIpc.GetCurrentCharacter();
|
||||||
|
if (string.IsNullOrEmpty(currentLoggedInChar) || currentLoggedInChar != currentCharacterId)
|
||||||
|
{
|
||||||
|
double waitTime = (DateTime.Now - characterSwitchStartTime).TotalSeconds;
|
||||||
|
if (!(waitTime > 20.0))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.Warning($"[AlliedSociety] Character switch timeout ({waitTime:F1}s). Retrying /ar relog for {currentCharacterId}...");
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/ays relog " + currentCharacterId);
|
||||||
|
log.Information("[AlliedSociety] Retry relog command sent for " + currentCharacterId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[AlliedSociety] Failed to send retry relog: " + ex.Message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
characterSwitchStartTime = DateTime.Now;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
questionableIpc.ForceCheckAvailability();
|
||||||
|
if (questionableIpc.IsAvailable)
|
||||||
|
{
|
||||||
|
log.Information("[AlliedSociety] ✓ Character logged in (" + currentLoggedInChar + "), Questionable ready");
|
||||||
|
SetPhase(AlliedSocietyRotationPhase.ImportingQuests);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleImportingQuests()
|
||||||
|
{
|
||||||
|
int allowances = questionableIpc.GetAlliedSocietyRemainingAllowances();
|
||||||
|
log.Information($"[AlliedSociety] Remaining allowances: {allowances}");
|
||||||
|
if (allowances <= 0)
|
||||||
|
{
|
||||||
|
log.Information("[AlliedSociety] No allowances left. Checking completion...");
|
||||||
|
SetPhase(AlliedSocietyRotationPhase.CheckingCompletion);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<string> quests = questSelector.SelectQuestsForCharacter(currentCharacterId, allowances, configuration.AlliedSociety.RotationConfig.Priorities, configuration.AlliedSociety.RotationConfig.QuestMode);
|
||||||
|
if (quests.Count == 0)
|
||||||
|
{
|
||||||
|
consecutiveNoQuestsCount++;
|
||||||
|
log.Warning($"[AlliedSociety] No quests selected (attempt {consecutiveNoQuestsCount}/3). Checking completion...");
|
||||||
|
if (consecutiveNoQuestsCount >= 3)
|
||||||
|
{
|
||||||
|
log.Error("[AlliedSociety] Failed to select quests 3 times consecutively. No quests available for this character.");
|
||||||
|
log.Information("[AlliedSociety] Marking character as complete and moving to next...");
|
||||||
|
consecutiveNoQuestsCount = 0;
|
||||||
|
database.SetCharacterComplete(currentCharacterId, DateTime.Now);
|
||||||
|
AdvanceToNextCharacter();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetPhase(AlliedSocietyRotationPhase.CheckingCompletion);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
consecutiveNoQuestsCount = 0;
|
||||||
|
log.Information($"[AlliedSociety] Importing {quests.Count} quests to Questionable...");
|
||||||
|
foreach (string questId in quests)
|
||||||
|
{
|
||||||
|
log.Debug("[AlliedSociety] Adding quest " + questId + " to priority");
|
||||||
|
questionableIpc.AddQuestPriority(questId);
|
||||||
|
database.AddImportedQuest(currentCharacterId, questId);
|
||||||
|
}
|
||||||
|
log.Information("✓ All quests imported to Questionable");
|
||||||
|
log.Information("[AlliedSociety] Sending /qst start command...");
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/qst start");
|
||||||
|
log.Information("[AlliedSociety] ✓ /qst start command sent successfully");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[AlliedSociety] ✗ Failed to send /qst start: " + ex.Message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log.Information("[AlliedSociety] Transitioning to WaitingForQuestAccept phase");
|
||||||
|
SetPhase(AlliedSocietyRotationPhase.WaitingForQuestAccept);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleWaitingForQuestAccept()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
object task = questionableIpc.GetCurrentTask();
|
||||||
|
if (task != null && task is JObject jObject)
|
||||||
|
{
|
||||||
|
JToken taskNameToken = jObject["TaskName"];
|
||||||
|
if (taskNameToken != null)
|
||||||
|
{
|
||||||
|
string taskName = taskNameToken.ToString();
|
||||||
|
if (!string.IsNullOrEmpty(taskName) && taskName.Contains("Wait(territory:"))
|
||||||
|
{
|
||||||
|
log.Information("[AlliedSociety] [WaitAccept] Territory wait detected: " + taskName);
|
||||||
|
Match territoryMatch = Regex.Match(taskName, "Wait\\(territory:\\s*(.+?)\\s*\\((\\d+)\\)\\)");
|
||||||
|
if (territoryMatch.Success)
|
||||||
|
{
|
||||||
|
string territoryName = territoryMatch.Groups[1].Value.Trim();
|
||||||
|
TimeSpan timeSinceLastTeleport = DateTime.Now - lastTerritoryTeleportTime;
|
||||||
|
if (territoryName != lastTerritoryName || timeSinceLastTeleport.TotalSeconds > 10.0)
|
||||||
|
{
|
||||||
|
log.Information("[AlliedSociety] [WaitAccept] ▶ Sending Lifestream teleport: /li " + territoryName);
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/li " + territoryName);
|
||||||
|
log.Information("[AlliedSociety] [WaitAccept] ✓ Lifestream command sent successfully");
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
log.Error("[AlliedSociety] [WaitAccept] ✗ Failed to send Lifestream command: " + ex2.Message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
lastTerritoryTeleportTime = DateTime.Now;
|
||||||
|
lastTerritoryName = territoryName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Debug("[AlliedSociety] [WaitAccept] Territory teleport debounced for: " + territoryName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Debug("[AlliedSociety] [WaitAccept] Error checking task for teleport: " + ex.Message);
|
||||||
|
}
|
||||||
|
AlliedSocietyCharacterStatus characterStatus = database.GetCharacterStatus(currentCharacterId);
|
||||||
|
bool allAccepted = true;
|
||||||
|
int acceptedCount = 0;
|
||||||
|
int totalCount = characterStatus.ImportedQuestIds.Count;
|
||||||
|
foreach (string questId in characterStatus.ImportedQuestIds)
|
||||||
|
{
|
||||||
|
if (questionableIpc.IsReadyToAcceptQuest(questId))
|
||||||
|
{
|
||||||
|
allAccepted = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
acceptedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (allAccepted)
|
||||||
|
{
|
||||||
|
log.Information($"[AlliedSociety] ✓ All {totalCount} quests accepted. Monitoring progress...");
|
||||||
|
SetPhase(AlliedSocietyRotationPhase.MonitoringQuests);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleMonitoringQuests()
|
||||||
|
{
|
||||||
|
string currentQuestId = questionableIpc.GetCurrentQuestId();
|
||||||
|
AlliedSocietyCharacterStatus status = database.GetCharacterStatus(currentCharacterId);
|
||||||
|
log.Debug("[AlliedSociety] Monitoring - Current Quest: " + (currentQuestId ?? "null"));
|
||||||
|
if (currentQuestId != null && status.ImportedQuestIds.Contains(currentQuestId))
|
||||||
|
{
|
||||||
|
log.Debug("[AlliedSociety] Working on imported quest: " + currentQuestId);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
object task = questionableIpc.GetCurrentTask();
|
||||||
|
log.Debug("[AlliedSociety] Task object type: " + (task?.GetType().Name ?? "null"));
|
||||||
|
if (task != null && task is JObject jObject)
|
||||||
|
{
|
||||||
|
log.Debug("[AlliedSociety] Task JObject: " + jObject.ToString());
|
||||||
|
JToken taskNameToken = jObject["TaskName"];
|
||||||
|
if (taskNameToken != null)
|
||||||
|
{
|
||||||
|
string taskName = taskNameToken.ToString();
|
||||||
|
log.Information("[AlliedSociety] Current Task Name: '" + taskName + "'");
|
||||||
|
if (!string.IsNullOrEmpty(taskName) && taskName.Contains("Wait(territory:"))
|
||||||
|
{
|
||||||
|
log.Information("[AlliedSociety] ✓ TERRITORY WAIT DETECTED! Raw task: " + taskName);
|
||||||
|
Match territoryMatch = Regex.Match(taskName, "Wait\\(territory:\\s*(.+?)\\s*\\((\\d+)\\)\\)");
|
||||||
|
if (territoryMatch.Success)
|
||||||
|
{
|
||||||
|
string territoryName = territoryMatch.Groups[1].Value.Trim();
|
||||||
|
log.Information("[AlliedSociety] ✓ Territory name parsed: '" + territoryName + "'");
|
||||||
|
TimeSpan timeSinceLastTeleport = DateTime.Now - lastTerritoryTeleportTime;
|
||||||
|
if (territoryName != lastTerritoryName || timeSinceLastTeleport.TotalSeconds > 10.0)
|
||||||
|
{
|
||||||
|
log.Information("[AlliedSociety] ▶ Territory wait detected: " + territoryName);
|
||||||
|
log.Information("[AlliedSociety] ▶ Sending Lifestream teleport command: /li " + territoryName);
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/li " + territoryName);
|
||||||
|
log.Information("[AlliedSociety] ✓ Lifestream command sent successfully");
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
log.Error("[AlliedSociety] ✗ Failed to send Lifestream command: " + ex2.Message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
lastTerritoryTeleportTime = DateTime.Now;
|
||||||
|
lastTerritoryName = territoryName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Information($"[AlliedSociety] ⏸ Territory teleport debounced for: {territoryName} (last: {timeSinceLastTeleport.TotalSeconds:F1}s ago)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Warning("[AlliedSociety] ✗ Territory wait detected but regex didn't match! Raw: " + taskName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(taskName) && taskName.ToLower().Contains("wait"))
|
||||||
|
{
|
||||||
|
log.Debug("[AlliedSociety] Wait task (not territory): " + taskName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Debug("[AlliedSociety] Task has no TaskName property");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (task == null)
|
||||||
|
{
|
||||||
|
log.Debug("[AlliedSociety] GetCurrentTask returned null");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Warning("[AlliedSociety] Task is not JObject, type: " + task.GetType().Name);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[AlliedSociety] Error checking task for teleport: " + ex.Message + "\n" + ex.StackTrace);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentQuestId == null || !status.ImportedQuestIds.Contains(currentQuestId))
|
||||||
|
{
|
||||||
|
log.Information("[AlliedSociety] No longer working on imported quests. Checking completion...");
|
||||||
|
SetPhase(AlliedSocietyRotationPhase.CheckingCompletion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleCheckingCompletion()
|
||||||
|
{
|
||||||
|
int allowances = questionableIpc.GetAlliedSocietyRemainingAllowances();
|
||||||
|
log.Information($"[AlliedSociety] Checking completion. Allowances: {allowances}");
|
||||||
|
if (allowances == 0)
|
||||||
|
{
|
||||||
|
string currentQuestId = questionableIpc.GetCurrentQuestId();
|
||||||
|
AlliedSocietyCharacterStatus status = database.GetCharacterStatus(currentCharacterId);
|
||||||
|
if (currentQuestId != null && status.ImportedQuestIds.Contains(currentQuestId))
|
||||||
|
{
|
||||||
|
log.Information("[AlliedSociety] Still working on final quest " + currentQuestId + ". Waiting...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.Information("[AlliedSociety] Character " + currentCharacterId + " completed all allowances.");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/qst stop");
|
||||||
|
log.Information("[AlliedSociety] Sent /qst stop command after quest completion");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[AlliedSociety] Failed to send /qst stop: " + ex.Message);
|
||||||
|
}
|
||||||
|
questionableIpc.ClearQuestPriority();
|
||||||
|
database.SetCharacterComplete(currentCharacterId, DateTime.Now);
|
||||||
|
SetPhase(AlliedSocietyRotationPhase.WaitingForCharacterSwitch);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Information("[AlliedSociety] Allowances remaining. Trying to import more quests...");
|
||||||
|
questionableIpc.ClearQuestPriority();
|
||||||
|
database.ClearImportedQuests(currentCharacterId);
|
||||||
|
SetPhase(AlliedSocietyRotationPhase.ImportingQuests);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleWaitingForCharacterSwitch()
|
||||||
|
{
|
||||||
|
if (!((DateTime.Now - phaseStartTime).TotalSeconds < 2.0))
|
||||||
|
{
|
||||||
|
AdvanceToNextCharacter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AdvanceToNextCharacter()
|
||||||
|
{
|
||||||
|
currentCharacterIndex++;
|
||||||
|
if (currentCharacterIndex >= rotationCharacters.Count)
|
||||||
|
{
|
||||||
|
log.Information("[AlliedSociety] Rotation completed for all characters.");
|
||||||
|
StopRotation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
string nextChar = rotationCharacters[currentCharacterIndex];
|
||||||
|
if (database.GetCharacterStatus(nextChar).Status == AlliedSocietyRotationStatus.Complete)
|
||||||
|
{
|
||||||
|
log.Information("[AlliedSociety] Skipping " + nextChar + " (Already Complete)");
|
||||||
|
AdvanceToNextCharacter();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.Information("[AlliedSociety] Switching to " + nextChar);
|
||||||
|
currentCharacterId = nextChar;
|
||||||
|
characterSwitchStartTime = DateTime.Now;
|
||||||
|
if (autoRetainerIpc.SwitchCharacter(nextChar))
|
||||||
|
{
|
||||||
|
SetPhase(AlliedSocietyRotationPhase.StartingRotation);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.Error("[AlliedSociety] Failed to switch to " + nextChar);
|
||||||
|
StopRotation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetPhase(AlliedSocietyRotationPhase phase)
|
||||||
|
{
|
||||||
|
log.Information($"[AlliedSociety] Phase: {currentPhase} → {phase}");
|
||||||
|
currentPhase = phase;
|
||||||
|
phaseStartTime = DateTime.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,513 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Dalamud.Plugin.Ipc;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class AutoRetainerIPC : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IDalamudPluginInterface pluginInterface;
|
||||||
|
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private readonly IClientState clientState;
|
||||||
|
|
||||||
|
private readonly ICommandManager commandManager;
|
||||||
|
|
||||||
|
private readonly IFramework framework;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<List<ulong>>? getRegisteredCIDsSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<ulong, object>? getOfflineCharacterDataSubscriber;
|
||||||
|
|
||||||
|
private ICallGateProvider<string, object>? relogProvider;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<bool>? getMultiModeEnabledSubscriber;
|
||||||
|
|
||||||
|
private ICallGateProvider<bool, object>? setMultiModeEnabledProvider;
|
||||||
|
|
||||||
|
private Dictionary<ulong, string> characterCache = new Dictionary<ulong, string>();
|
||||||
|
|
||||||
|
private HashSet<ulong> unknownCIDs = new HashSet<ulong>();
|
||||||
|
|
||||||
|
private bool subscribersInitialized;
|
||||||
|
|
||||||
|
private DateTime lastAvailabilityCheck = DateTime.MinValue;
|
||||||
|
|
||||||
|
private const int AvailabilityCheckCooldownSeconds = 5;
|
||||||
|
|
||||||
|
public bool IsAvailable { get; private set; }
|
||||||
|
|
||||||
|
public AutoRetainerIPC(IDalamudPluginInterface pluginInterface, IPluginLog log, IClientState clientState, ICommandManager commandManager, IFramework framework)
|
||||||
|
{
|
||||||
|
this.pluginInterface = pluginInterface;
|
||||||
|
this.log = log;
|
||||||
|
this.clientState = clientState;
|
||||||
|
this.commandManager = commandManager;
|
||||||
|
this.framework = framework;
|
||||||
|
InitializeIPC();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearCache()
|
||||||
|
{
|
||||||
|
characterCache.Clear();
|
||||||
|
unknownCIDs.Clear();
|
||||||
|
log.Information("[AutoRetainerIPC] Cache cleared");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeIPC()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
getRegisteredCIDsSubscriber = null;
|
||||||
|
getOfflineCharacterDataSubscriber = null;
|
||||||
|
relogProvider = null;
|
||||||
|
getMultiModeEnabledSubscriber = null;
|
||||||
|
setMultiModeEnabledProvider = null;
|
||||||
|
IsAvailable = false;
|
||||||
|
getRegisteredCIDsSubscriber = pluginInterface.GetIpcSubscriber<List<ulong>>("AutoRetainer.GetRegisteredCIDs");
|
||||||
|
getOfflineCharacterDataSubscriber = pluginInterface.GetIpcSubscriber<ulong, object>("AutoRetainer.GetOfflineCharacterData");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
relogProvider = pluginInterface.GetIpcProvider<string, object>("AutoRetainer.Relog");
|
||||||
|
log.Debug("[AutoRetainerIPC] Relog IPC provider initialized");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Debug("[AutoRetainerIPC] Failed to initialize Relog provider: " + ex.Message);
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
getMultiModeEnabledSubscriber = pluginInterface.GetIpcSubscriber<bool>("AutoRetainer.GetMultiModeEnabled");
|
||||||
|
setMultiModeEnabledProvider = pluginInterface.GetIpcProvider<bool, object>("AutoRetainer.SetMultiModeEnabled");
|
||||||
|
log.Debug("[AutoRetainerIPC] Multi-Mode IPC initialized");
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
log.Debug("[AutoRetainerIPC] Failed to initialize Multi-Mode IPC: " + ex2.Message);
|
||||||
|
}
|
||||||
|
subscribersInitialized = true;
|
||||||
|
log.Debug("[AutoRetainerIPC] IPC subscribers initialized (lazy-loading enabled)");
|
||||||
|
}
|
||||||
|
catch (Exception ex3)
|
||||||
|
{
|
||||||
|
IsAvailable = false;
|
||||||
|
subscribersInitialized = false;
|
||||||
|
log.Error("[AutoRetainerIPC] Failed to initialize subscribers: " + ex3.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryEnsureAvailable()
|
||||||
|
{
|
||||||
|
if (IsAvailable)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!subscribersInitialized)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
DateTime now = DateTime.Now;
|
||||||
|
if ((now - lastAvailabilityCheck).TotalSeconds < 5.0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
lastAvailabilityCheck = now;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (getRegisteredCIDsSubscriber == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
List<ulong> testCids = getRegisteredCIDsSubscriber.InvokeFunc();
|
||||||
|
if (!IsAvailable)
|
||||||
|
{
|
||||||
|
IsAvailable = true;
|
||||||
|
log.Information($"[AutoRetainerIPC] ✅ AutoRetainer is now available ({testCids?.Count ?? 0} characters)");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Debug("[AutoRetainerIPC] AutoRetainer not yet available: " + ex.Message);
|
||||||
|
IsAvailable = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryReinitialize()
|
||||||
|
{
|
||||||
|
log.Information("[AutoRetainerIPC] Manual IPC reinitialization requested");
|
||||||
|
lastAvailabilityCheck = DateTime.MinValue;
|
||||||
|
bool num = TryEnsureAvailable();
|
||||||
|
if (num)
|
||||||
|
{
|
||||||
|
log.Information("[AutoRetainerIPC] IPC reinitialization successful");
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
log.Warning("[AutoRetainerIPC] IPC still unavailable after reinitialization attempt");
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> GetRegisteredCharacters()
|
||||||
|
{
|
||||||
|
log.Debug("[AutoRetainerIPC] GetRegisteredCharacters called");
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || getRegisteredCIDsSubscriber == null)
|
||||||
|
{
|
||||||
|
log.Warning("[AutoRetainerIPC] Cannot get characters - IPC not available");
|
||||||
|
log.Warning($"[AutoRetainerIPC] IsAvailable: {IsAvailable}, Subscriber: {getRegisteredCIDsSubscriber != null}");
|
||||||
|
return new List<string>();
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<ulong> cids = getRegisteredCIDsSubscriber.InvokeFunc();
|
||||||
|
if (cids == null || cids.Count == 0)
|
||||||
|
{
|
||||||
|
log.Warning("[AutoRetainerIPC] No CIDs returned from AutoRetainer");
|
||||||
|
return new List<string>();
|
||||||
|
}
|
||||||
|
List<string> characters = new List<string>();
|
||||||
|
foreach (ulong cid in cids)
|
||||||
|
{
|
||||||
|
string charName = GetCharacterNameFromCID(cid);
|
||||||
|
if (!string.IsNullOrEmpty(charName))
|
||||||
|
{
|
||||||
|
characters.Add(charName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
log.Debug($"[AutoRetainerIPC] Could not resolve name for CID: {cid}");
|
||||||
|
}
|
||||||
|
if (characters.Count == 0)
|
||||||
|
{
|
||||||
|
log.Warning("[AutoRetainerIPC] No character names could be resolved from CIDs");
|
||||||
|
}
|
||||||
|
return characters;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[AutoRetainerIPC] GetRegisteredCharacters failed: " + ex.Message);
|
||||||
|
log.Error("[AutoRetainerIPC] Stack trace: " + ex.StackTrace);
|
||||||
|
return new List<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetCharacterNameFromCID(ulong cid)
|
||||||
|
{
|
||||||
|
if (characterCache.TryGetValue(cid, out string cachedName))
|
||||||
|
{
|
||||||
|
if (cachedName.Contains("@"))
|
||||||
|
{
|
||||||
|
return cachedName;
|
||||||
|
}
|
||||||
|
log.Debug($"[AutoRetainerIPC] Removing invalid cache entry for CID {cid}: '{cachedName}'");
|
||||||
|
characterCache.Remove(cid);
|
||||||
|
}
|
||||||
|
if (unknownCIDs.Contains(cid))
|
||||||
|
{
|
||||||
|
return $"Unknown (CID: {cid})";
|
||||||
|
}
|
||||||
|
if (getOfflineCharacterDataSubscriber == null)
|
||||||
|
{
|
||||||
|
log.Debug("[AutoRetainerIPC] OfflineCharacterData subscriber is null");
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
object data = getOfflineCharacterDataSubscriber.InvokeFunc(cid);
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
if (!unknownCIDs.Contains(cid))
|
||||||
|
{
|
||||||
|
log.Warning($"[AutoRetainerIPC] No data returned for CID {cid}");
|
||||||
|
unknownCIDs.Add(cid);
|
||||||
|
}
|
||||||
|
return $"Unknown (CID: {cid})";
|
||||||
|
}
|
||||||
|
string resolvedName = null;
|
||||||
|
FieldInfo nameField = data.GetType().GetField("Name");
|
||||||
|
FieldInfo worldField = data.GetType().GetField("World");
|
||||||
|
if (nameField != null && worldField != null)
|
||||||
|
{
|
||||||
|
string name = nameField.GetValue(data)?.ToString();
|
||||||
|
string world = worldField.GetValue(data)?.ToString();
|
||||||
|
log.Warning($"[AutoRetainerIPC] Field values for CID {cid} - Name: '{name}', World: '{world}'");
|
||||||
|
if (!string.IsNullOrEmpty(name) && name != "Unknown")
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(world) && clientState.IsLoggedIn && clientState.LocalPlayer != null && clientState.LocalPlayer.Name.ToString() == name)
|
||||||
|
{
|
||||||
|
world = clientState.LocalPlayer.HomeWorld.Value.Name.ToString();
|
||||||
|
log.Information("[AutoRetainerIPC] Resolved world from ClientState for " + name + ": " + world);
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(world))
|
||||||
|
{
|
||||||
|
resolvedName = name + "@" + world;
|
||||||
|
characterCache[cid] = resolvedName;
|
||||||
|
return resolvedName;
|
||||||
|
}
|
||||||
|
log.Warning($"[AutoRetainerIPC] World is empty for CID {cid}, cannot create full name");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Warning($"[AutoRetainerIPC] Name is empty/invalid for CID {cid}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PropertyInfo nameProperty = data.GetType().GetProperty("Name");
|
||||||
|
PropertyInfo worldProperty = data.GetType().GetProperty("World");
|
||||||
|
if (nameProperty != null && worldProperty != null)
|
||||||
|
{
|
||||||
|
string name2 = nameProperty.GetValue(data)?.ToString();
|
||||||
|
string world2 = worldProperty.GetValue(data)?.ToString();
|
||||||
|
if (!string.IsNullOrEmpty(name2) && !string.IsNullOrEmpty(world2) && name2 != "Unknown")
|
||||||
|
{
|
||||||
|
resolvedName = name2 + "@" + world2;
|
||||||
|
characterCache[cid] = resolvedName;
|
||||||
|
log.Debug($"[AutoRetainerIPC] CID {cid} resolved to {resolvedName} (via properties)");
|
||||||
|
return resolvedName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data is JToken jToken)
|
||||||
|
{
|
||||||
|
resolvedName = ParseJTokenCharacterData(jToken, cid);
|
||||||
|
if (!string.IsNullOrEmpty(resolvedName))
|
||||||
|
{
|
||||||
|
characterCache[cid] = resolvedName;
|
||||||
|
log.Debug($"[AutoRetainerIPC] CID {cid} resolved to {resolvedName} (via JSON)");
|
||||||
|
return resolvedName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!unknownCIDs.Contains(cid))
|
||||||
|
{
|
||||||
|
log.Warning($"[AutoRetainerIPC] Could not resolve name for CID {cid}");
|
||||||
|
LogDataStructure(data, cid);
|
||||||
|
unknownCIDs.Add(cid);
|
||||||
|
}
|
||||||
|
return $"Unknown (CID: {cid})";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (!unknownCIDs.Contains(cid))
|
||||||
|
{
|
||||||
|
log.Warning($"[AutoRetainerIPC] Exception resolving CID {cid}: {ex.Message}");
|
||||||
|
log.Debug("[AutoRetainerIPC] Stack trace: " + ex.StackTrace);
|
||||||
|
unknownCIDs.Add(cid);
|
||||||
|
}
|
||||||
|
return $"Unknown (CID: {cid})";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? ParseJTokenCharacterData(JToken jToken, ulong cid)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
JToken nameToken = jToken.SelectToken("Name") ?? jToken.SelectToken("name") ?? jToken.SelectToken("Value.Name");
|
||||||
|
JToken worldToken = jToken.SelectToken("World") ?? jToken.SelectToken("world") ?? jToken.SelectToken("Value.World");
|
||||||
|
if (nameToken != null && worldToken != null)
|
||||||
|
{
|
||||||
|
string name = nameToken.Value<string>();
|
||||||
|
string world = worldToken.Value<string>();
|
||||||
|
if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(world))
|
||||||
|
{
|
||||||
|
return name + "@" + world;
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(name))
|
||||||
|
{
|
||||||
|
log.Warning($"[AutoRetainerIPC] JSON has Name but World is empty for CID {cid}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
string[] array = new string[4] { "NameWithWorld", "nameWithWorld", "[\"NameWithWorld\"]", "Value.NameWithWorld" };
|
||||||
|
foreach (string path in array)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(path))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
JToken token = jToken.SelectToken(path);
|
||||||
|
if (token != null && token.Type == JTokenType.String)
|
||||||
|
{
|
||||||
|
string value = token.Value<string>();
|
||||||
|
if (!string.IsNullOrEmpty(value) && value.Contains("@"))
|
||||||
|
{
|
||||||
|
log.Information("[AutoRetainerIPC] Found name via JSON path '" + path + "': " + value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Warning($"[AutoRetainerIPC] Error parsing JToken for CID {cid}: {ex.Message}");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogDataStructure(object data, ulong cid)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (data is JToken jToken)
|
||||||
|
{
|
||||||
|
log.Debug($"[AutoRetainerIPC] JSON structure for CID {cid}:");
|
||||||
|
log.Debug(jToken.ToString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PropertyInfo[] properties = data.GetType().GetProperties();
|
||||||
|
log.Debug($"[AutoRetainerIPC] Object structure for CID {cid}:");
|
||||||
|
foreach (PropertyInfo prop in properties.Take(10))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
object value = prop.GetValue(data);
|
||||||
|
log.Debug($" {prop.Name} = {value ?? "(null)"}");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
log.Debug(" " + prop.Name + " = (error reading value)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? GetCurrentCharacter()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string result = null;
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!clientState.IsLoggedIn)
|
||||||
|
{
|
||||||
|
result = null;
|
||||||
|
}
|
||||||
|
else if (clientState.LocalPlayer == null)
|
||||||
|
{
|
||||||
|
result = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string text = clientState.LocalPlayer.Name.ToString();
|
||||||
|
string text2 = clientState.LocalPlayer.HomeWorld.Value.Name.ToString();
|
||||||
|
if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(text2))
|
||||||
|
{
|
||||||
|
result = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = text + "@" + text2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
log.Debug("[AutoRetainerIPC] GetCurrentCharacter inner failed: " + ex2.Message);
|
||||||
|
result = null;
|
||||||
|
}
|
||||||
|
}).Wait();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Debug("[AutoRetainerIPC] GetCurrentCharacter failed: " + ex.Message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SwitchCharacter(string characterNameWithWorld)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(characterNameWithWorld))
|
||||||
|
{
|
||||||
|
log.Warning("[AutoRetainerIPC] Character name is null or empty");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable)
|
||||||
|
{
|
||||||
|
log.Warning("[AutoRetainerIPC] AutoRetainer not available");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information("[AutoRetainerIPC] Requesting relog to: " + characterNameWithWorld);
|
||||||
|
string command = "/ays relog " + characterNameWithWorld;
|
||||||
|
bool success = false;
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand(command);
|
||||||
|
success = true;
|
||||||
|
log.Information("[AutoRetainerIPC] Relog command executed: " + command);
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
log.Error("[AutoRetainerIPC] Failed to execute relog command: " + ex2.Message);
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
}).Wait();
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[AutoRetainerIPC] Failed to switch character: " + ex.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool GetMultiModeEnabled()
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || getMultiModeEnabledSubscriber == null)
|
||||||
|
{
|
||||||
|
log.Debug("[AutoRetainerIPC] Multi-Mode IPC not available");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return getMultiModeEnabledSubscriber.InvokeFunc();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[AutoRetainerIPC] GetMultiModeEnabled failed: " + ex.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SetMultiModeEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || setMultiModeEnabledProvider == null)
|
||||||
|
{
|
||||||
|
log.Warning("[AutoRetainerIPC] Multi-Mode IPC not available");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
setMultiModeEnabledProvider.SendMessage(enabled);
|
||||||
|
log.Information($"[AutoRetainerIPC] Multi-Mode set to: {enabled}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[AutoRetainerIPC] SetMultiModeEnabled failed: " + ex.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
IsAvailable = false;
|
||||||
|
characterCache.Clear();
|
||||||
|
unknownCIDs.Clear();
|
||||||
|
log.Information("[AutoRetainerIPC] Service disposed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,233 @@
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dalamud.Game.ClientState.Conditions;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Control;
|
||||||
|
using FFXIVClientStructs.FFXIV.Common.Math;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class CharacterSafeWaitService
|
||||||
|
{
|
||||||
|
private readonly IClientState clientState;
|
||||||
|
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private readonly IFramework framework;
|
||||||
|
|
||||||
|
private readonly ICondition condition;
|
||||||
|
|
||||||
|
private readonly IGameGui gameGui;
|
||||||
|
|
||||||
|
public CharacterSafeWaitService(IClientState clientState, IPluginLog log, IFramework framework, ICondition condition, IGameGui gameGui)
|
||||||
|
{
|
||||||
|
this.clientState = clientState;
|
||||||
|
this.log = log;
|
||||||
|
this.framework = framework;
|
||||||
|
this.condition = condition;
|
||||||
|
this.gameGui = gameGui;
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe Task<bool> WaitForCharacterFullyLoadedAsync(int timeoutSeconds = 5, int checkIntervalMs = 200)
|
||||||
|
{
|
||||||
|
return Task.Run(delegate
|
||||||
|
{
|
||||||
|
Stopwatch stopwatch = Stopwatch.StartNew();
|
||||||
|
log.Information("[CharLoad] Waiting for character to fully load...");
|
||||||
|
TimeSpan timeSpan = TimeSpan.FromSeconds(timeoutSeconds);
|
||||||
|
while (stopwatch.Elapsed < timeSpan)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Control.GetLocalPlayer() == null || !clientState.IsLoggedIn)
|
||||||
|
{
|
||||||
|
Thread.Sleep(checkIntervalMs);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!condition[ConditionFlag.BetweenAreas] && !condition[ConditionFlag.BetweenAreas51] && !condition[ConditionFlag.OccupiedInCutSceneEvent])
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
log.Information($"[CharLoad] Character fully loaded in {stopwatch.ElapsedMilliseconds}ms!");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Thread.Sleep(checkIntervalMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[CharLoad] Error during load check: " + ex.Message);
|
||||||
|
Thread.Sleep(checkIntervalMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stopwatch.Stop();
|
||||||
|
log.Warning($"[CharLoad] Character load timeout after {stopwatch.ElapsedMilliseconds}ms");
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool WaitForCharacterFullyLoaded(int timeoutSeconds = 5)
|
||||||
|
{
|
||||||
|
return WaitForCharacterFullyLoadedAsync(timeoutSeconds).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PerformSafeWaitAsync()
|
||||||
|
{
|
||||||
|
Stopwatch sw = Stopwatch.StartNew();
|
||||||
|
log.Information("[SafeWait] Starting character stabilization...");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!(await WaitForCharacterFullyLoadedAsync(120)))
|
||||||
|
{
|
||||||
|
log.Warning("[SafeWait] Character not fully loaded, proceeding anyway");
|
||||||
|
}
|
||||||
|
await Task.Delay(250);
|
||||||
|
await WaitForMovementEndAsync();
|
||||||
|
await Task.Delay(250);
|
||||||
|
await WaitForActionsCompleteAsync();
|
||||||
|
await Task.Delay(500);
|
||||||
|
sw.Stop();
|
||||||
|
log.Information($"[SafeWait] Character stabilization complete in {sw.ElapsedMilliseconds}ms");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
sw.Stop();
|
||||||
|
log.Error($"[SafeWait] Error during safe wait after {sw.ElapsedMilliseconds}ms: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PerformSafeWait()
|
||||||
|
{
|
||||||
|
PerformSafeWaitAsync().GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WaitForMovementEndAsync(int maxWaitMs = 5000, int checkIntervalMs = 100)
|
||||||
|
{
|
||||||
|
Stopwatch sw = Stopwatch.StartNew();
|
||||||
|
while (sw.ElapsedMilliseconds < maxWaitMs)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!IsPlayerMoving())
|
||||||
|
{
|
||||||
|
log.Debug($"[SafeWait] Movement ended after {sw.ElapsedMilliseconds}ms");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await Task.Delay(checkIntervalMs);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[SafeWait] Error checking movement: " + ex.Message);
|
||||||
|
await Task.Delay(checkIntervalMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Warning($"[SafeWait] Movement wait timeout after {sw.ElapsedMilliseconds}ms");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WaitForMovementEnd()
|
||||||
|
{
|
||||||
|
WaitForMovementEndAsync().GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WaitForActionsCompleteAsync(int maxWaitMs = 10000, int checkIntervalMs = 100)
|
||||||
|
{
|
||||||
|
Stopwatch sw = Stopwatch.StartNew();
|
||||||
|
while (sw.ElapsedMilliseconds < maxWaitMs)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!IsPlayerInAction())
|
||||||
|
{
|
||||||
|
log.Debug($"[SafeWait] Actions completed after {sw.ElapsedMilliseconds}ms");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await Task.Delay(checkIntervalMs);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[SafeWait] Error checking actions: " + ex.Message);
|
||||||
|
await Task.Delay(checkIntervalMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Warning($"[SafeWait] Action wait timeout after {sw.ElapsedMilliseconds}ms");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WaitForActionsComplete()
|
||||||
|
{
|
||||||
|
WaitForActionsCompleteAsync().GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe bool IsPlayerMoving()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
BattleChara* player = Control.GetLocalPlayer();
|
||||||
|
if (player == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
FFXIVClientStructs.FFXIV.Common.Math.Vector3 currentPos = player->Character.GameObject.Position;
|
||||||
|
Thread.Sleep(50);
|
||||||
|
player = Control.GetLocalPlayer();
|
||||||
|
if (player == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
FFXIVClientStructs.FFXIV.Common.Math.Vector3 newPos = player->Character.GameObject.Position;
|
||||||
|
return System.Numerics.Vector3.Distance(currentPos, newPos) > 0.01f;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe bool IsPlayerInAction()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
BattleChara* player = Control.GetLocalPlayer();
|
||||||
|
if (player == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (player->Character.IsCasting)
|
||||||
|
{
|
||||||
|
log.Debug("[SafeWait] Player is casting");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PerformQuickSafeWaitAsync()
|
||||||
|
{
|
||||||
|
Stopwatch sw = Stopwatch.StartNew();
|
||||||
|
log.Debug("[SafeWait] Performing quick safe wait...");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await WaitForMovementEndAsync(3000);
|
||||||
|
await Task.Delay(1000);
|
||||||
|
sw.Stop();
|
||||||
|
log.Debug($"[SafeWait] Quick safe wait complete in {sw.ElapsedMilliseconds}ms");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
sw.Stop();
|
||||||
|
log.Error("[SafeWait] Error during quick safe wait: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PerformQuickSafeWait()
|
||||||
|
{
|
||||||
|
PerformQuickSafeWaitAsync().GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,368 @@
|
||||||
|
using System;
|
||||||
|
using Dalamud.Game.ClientState.Conditions;
|
||||||
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class CombatDutyDetectionService : IDisposable
|
||||||
|
{
|
||||||
|
private readonly ICondition condition;
|
||||||
|
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private readonly IClientState clientState;
|
||||||
|
|
||||||
|
private readonly ICommandManager commandManager;
|
||||||
|
|
||||||
|
private readonly IFramework framework;
|
||||||
|
|
||||||
|
private readonly Configuration config;
|
||||||
|
|
||||||
|
private bool wasInCombat;
|
||||||
|
|
||||||
|
private bool wasInDuty;
|
||||||
|
|
||||||
|
private DateTime dutyExitTime = DateTime.MinValue;
|
||||||
|
|
||||||
|
private DateTime dutyEntryTime = DateTime.MinValue;
|
||||||
|
|
||||||
|
private DateTime lastStateChange = DateTime.MinValue;
|
||||||
|
|
||||||
|
private bool combatCommandsActive;
|
||||||
|
|
||||||
|
private bool hasCombatCommandsForDuty;
|
||||||
|
|
||||||
|
private bool isInAutoDutyDungeon;
|
||||||
|
|
||||||
|
private uint currentQuestId;
|
||||||
|
|
||||||
|
private bool isRotationActive;
|
||||||
|
|
||||||
|
public bool JustEnteredDuty { get; private set; }
|
||||||
|
|
||||||
|
public bool JustExitedDuty { get; private set; }
|
||||||
|
|
||||||
|
public DateTime DutyExitTime => dutyExitTime;
|
||||||
|
|
||||||
|
public bool IsInCombat { get; private set; }
|
||||||
|
|
||||||
|
public bool IsInDuty { get; private set; }
|
||||||
|
|
||||||
|
public bool IsInDutyQueue { get; private set; }
|
||||||
|
|
||||||
|
public bool ShouldPauseAutomation
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!IsInCombat && !IsInDuty)
|
||||||
|
{
|
||||||
|
return IsInDutyQueue;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AcknowledgeDutyEntry()
|
||||||
|
{
|
||||||
|
JustEnteredDuty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AcknowledgeDutyExit()
|
||||||
|
{
|
||||||
|
JustExitedDuty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CombatDutyDetectionService(ICondition condition, IPluginLog log, IClientState clientState, ICommandManager commandManager, IFramework framework, Configuration config)
|
||||||
|
{
|
||||||
|
this.condition = condition;
|
||||||
|
this.log = log;
|
||||||
|
this.clientState = clientState;
|
||||||
|
this.commandManager = commandManager;
|
||||||
|
this.framework = framework;
|
||||||
|
this.config = config;
|
||||||
|
log.Information("[CombatDuty] Service initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetRotationActive(bool active)
|
||||||
|
{
|
||||||
|
isRotationActive = active;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetAutoDutyDungeon(bool isAutoDuty)
|
||||||
|
{
|
||||||
|
isInAutoDutyDungeon = isAutoDuty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetCurrentQuestId(uint questId)
|
||||||
|
{
|
||||||
|
currentQuestId = questId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update()
|
||||||
|
{
|
||||||
|
if (clientState.LocalPlayer == null || !clientState.IsLoggedIn)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isRotationActive)
|
||||||
|
{
|
||||||
|
bool inCombat = condition[ConditionFlag.InCombat];
|
||||||
|
if (inCombat != wasInCombat)
|
||||||
|
{
|
||||||
|
IsInCombat = inCombat;
|
||||||
|
wasInCombat = inCombat;
|
||||||
|
lastStateChange = DateTime.Now;
|
||||||
|
if (inCombat)
|
||||||
|
{
|
||||||
|
log.Information("[CombatDuty] Combat started - pausing automation");
|
||||||
|
if (currentQuestId == 811)
|
||||||
|
{
|
||||||
|
log.Information("[CombatDuty] Quest 811 - combat commands DISABLED (RSR off)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Information("[CombatDuty] Combat ended - resuming automation");
|
||||||
|
if (combatCommandsActive && !IsInDuty)
|
||||||
|
{
|
||||||
|
log.Information("[CombatDuty] Not in duty - disabling combat commands");
|
||||||
|
DisableCombatCommands();
|
||||||
|
}
|
||||||
|
else if (combatCommandsActive && IsInDuty)
|
||||||
|
{
|
||||||
|
log.Information("[CombatDuty] In duty - keeping combat commands active");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isRotationActive && config.EnableCombatHandling && IsInCombat && !combatCommandsActive && currentQuestId != 811)
|
||||||
|
{
|
||||||
|
IPlayerCharacter player = clientState.LocalPlayer;
|
||||||
|
if (player != null)
|
||||||
|
{
|
||||||
|
float hpPercent = (float)player.CurrentHp / (float)player.MaxHp * 100f;
|
||||||
|
if (hpPercent <= (float)config.CombatHPThreshold)
|
||||||
|
{
|
||||||
|
log.Warning($"[CombatDuty] HP at {hpPercent:F1}% (threshold: {config.CombatHPThreshold}%) - enabling combat commands");
|
||||||
|
EnableCombatCommands();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool inDuty = condition[ConditionFlag.BoundByDuty] || condition[ConditionFlag.BoundByDuty56] || condition[ConditionFlag.BoundByDuty95];
|
||||||
|
if (inDuty != wasInDuty)
|
||||||
|
{
|
||||||
|
IsInDuty = inDuty;
|
||||||
|
wasInDuty = inDuty;
|
||||||
|
lastStateChange = DateTime.Now;
|
||||||
|
if (inDuty)
|
||||||
|
{
|
||||||
|
log.Information("[CombatDuty] Duty started - pausing automation");
|
||||||
|
JustEnteredDuty = true;
|
||||||
|
JustExitedDuty = false;
|
||||||
|
dutyEntryTime = DateTime.Now;
|
||||||
|
hasCombatCommandsForDuty = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Information("[CombatDuty] Duty completed - resuming automation");
|
||||||
|
JustEnteredDuty = false;
|
||||||
|
JustExitedDuty = true;
|
||||||
|
dutyExitTime = DateTime.Now;
|
||||||
|
dutyEntryTime = DateTime.MinValue;
|
||||||
|
hasCombatCommandsForDuty = false;
|
||||||
|
if (combatCommandsActive)
|
||||||
|
{
|
||||||
|
log.Information("[CombatDuty] Duty ended - disabling combat commands");
|
||||||
|
DisableCombatCommands();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isRotationActive && IsInDuty && !isInAutoDutyDungeon && !hasCombatCommandsForDuty && dutyEntryTime != DateTime.MinValue)
|
||||||
|
{
|
||||||
|
if (currentQuestId == 811)
|
||||||
|
{
|
||||||
|
log.Information("[CombatDuty] Quest 811 - skipping combat commands (RSR disabled)");
|
||||||
|
hasCombatCommandsForDuty = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentQuestId == 4591)
|
||||||
|
{
|
||||||
|
log.Information("[CombatDuty] Quest 4591 (Steps of Faith) - skipping combat commands (handler does it)");
|
||||||
|
hasCombatCommandsForDuty = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((DateTime.Now - dutyEntryTime).TotalSeconds >= 8.0)
|
||||||
|
{
|
||||||
|
log.Information("[CombatDuty] 8 seconds in Solo Duty - enabling combat commands");
|
||||||
|
EnableCombatCommands();
|
||||||
|
hasCombatCommandsForDuty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool inQueue = condition[ConditionFlag.WaitingForDuty] || condition[ConditionFlag.WaitingForDutyFinder];
|
||||||
|
if (inQueue != IsInDutyQueue)
|
||||||
|
{
|
||||||
|
IsInDutyQueue = inQueue;
|
||||||
|
lastStateChange = DateTime.Now;
|
||||||
|
if (inQueue)
|
||||||
|
{
|
||||||
|
log.Information("[CombatDuty] Duty queue active - pausing automation");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Information("[CombatDuty] Duty queue ended - resuming automation");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeSpan TimeSinceLastStateChange()
|
||||||
|
{
|
||||||
|
if (lastStateChange == DateTime.MinValue)
|
||||||
|
{
|
||||||
|
return TimeSpan.Zero;
|
||||||
|
}
|
||||||
|
return DateTime.Now - lastStateChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnableCombatCommands()
|
||||||
|
{
|
||||||
|
if (combatCommandsActive)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information("[CombatDuty] ========================================");
|
||||||
|
log.Information("[CombatDuty] === ENABLING COMBAT AUTOMATION ===");
|
||||||
|
log.Information("[CombatDuty] ========================================");
|
||||||
|
framework.RunOnTick(delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/rsr auto");
|
||||||
|
log.Information("[CombatDuty] /rsr auto sent");
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
log.Error("[CombatDuty] Failed to send /rsr auto: " + ex2.Message);
|
||||||
|
}
|
||||||
|
}, TimeSpan.Zero);
|
||||||
|
framework.RunOnTick(delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/vbmai on");
|
||||||
|
log.Information("[CombatDuty] /vbmai on sent");
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
log.Error("[CombatDuty] Failed to send /vbmai on: " + ex2.Message);
|
||||||
|
}
|
||||||
|
}, TimeSpan.FromMilliseconds(100L, 0L));
|
||||||
|
framework.RunOnTick(delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/bmrai on");
|
||||||
|
log.Information("[CombatDuty] /bmrai on sent");
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
log.Error("[CombatDuty] Failed to send /bmrai on: " + ex2.Message);
|
||||||
|
}
|
||||||
|
}, TimeSpan.FromMilliseconds(200L, 0L));
|
||||||
|
combatCommandsActive = true;
|
||||||
|
log.Information("[CombatDuty] Combat automation enabled");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[CombatDuty] Error enabling combat commands: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DisableCombatCommands()
|
||||||
|
{
|
||||||
|
if (!combatCommandsActive)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information("[CombatDuty] ========================================");
|
||||||
|
log.Information("[CombatDuty] === DISABLING COMBAT AUTOMATION ===");
|
||||||
|
log.Information("[CombatDuty] ========================================");
|
||||||
|
framework.RunOnTick(delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/rsr off");
|
||||||
|
log.Information("[CombatDuty] /rsr off sent");
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
log.Error("[CombatDuty] Failed to send /rsr off: " + ex2.Message);
|
||||||
|
}
|
||||||
|
}, TimeSpan.Zero);
|
||||||
|
framework.RunOnTick(delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/vbmai off");
|
||||||
|
log.Information("[CombatDuty] /vbmai off sent");
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
log.Error("[CombatDuty] Failed to send /vbmai off: " + ex2.Message);
|
||||||
|
}
|
||||||
|
}, TimeSpan.FromMilliseconds(100L, 0L));
|
||||||
|
framework.RunOnTick(delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/bmrai off");
|
||||||
|
log.Information("[CombatDuty] /bmrai off sent");
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
log.Error("[CombatDuty] Failed to send /bmrai off: " + ex2.Message);
|
||||||
|
}
|
||||||
|
}, TimeSpan.FromMilliseconds(200L, 0L));
|
||||||
|
combatCommandsActive = false;
|
||||||
|
log.Information("[CombatDuty] Combat automation disabled");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[CombatDuty] Error disabling combat commands: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearDutyExitFlag()
|
||||||
|
{
|
||||||
|
JustExitedDuty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
IsInCombat = false;
|
||||||
|
IsInDuty = false;
|
||||||
|
IsInDutyQueue = false;
|
||||||
|
wasInCombat = false;
|
||||||
|
wasInDuty = false;
|
||||||
|
combatCommandsActive = false;
|
||||||
|
hasCombatCommandsForDuty = false;
|
||||||
|
JustEnteredDuty = false;
|
||||||
|
JustExitedDuty = false;
|
||||||
|
dutyExitTime = DateTime.MinValue;
|
||||||
|
dutyEntryTime = DateTime.MinValue;
|
||||||
|
log.Information("[CombatDuty] State reset");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (combatCommandsActive)
|
||||||
|
{
|
||||||
|
DisableCombatCommands();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,514 @@
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO.MemoryMappedFiles;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class CrossProcessIPC : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private readonly IFramework framework;
|
||||||
|
|
||||||
|
private readonly Configuration configuration;
|
||||||
|
|
||||||
|
private MemoryMappedFile? mmf;
|
||||||
|
|
||||||
|
private Thread? listenerThread;
|
||||||
|
|
||||||
|
private bool isRunning;
|
||||||
|
|
||||||
|
private const string MMF_NAME = "QSTCompanion_IPC";
|
||||||
|
|
||||||
|
private const int MMF_SIZE = 4096;
|
||||||
|
|
||||||
|
private const int POLLING_INTERVAL_MS = 10;
|
||||||
|
|
||||||
|
public event Action<string, ushort>? OnHelperAvailable;
|
||||||
|
|
||||||
|
public event Action<string, ushort>? OnHelperRequested;
|
||||||
|
|
||||||
|
public event Action? OnHelperDismissed;
|
||||||
|
|
||||||
|
public event Action<string>? OnChatMessageReceived;
|
||||||
|
|
||||||
|
public event Action<string>? OnCommandReceived;
|
||||||
|
|
||||||
|
public event Action<string, ushort>? OnHelperInParty;
|
||||||
|
|
||||||
|
public event Action<string, ushort>? OnHelperInDuty;
|
||||||
|
|
||||||
|
public event Action<string, ushort>? OnHelperReady;
|
||||||
|
|
||||||
|
public event Action? OnRequestHelperAnnouncements;
|
||||||
|
|
||||||
|
public event Action<string, ushort, uint, Vector3, Vector3, bool>? OnChauffeurSummonRequest;
|
||||||
|
|
||||||
|
public event Action<string>? OnChauffeurReadyForPickup;
|
||||||
|
|
||||||
|
public event Action<string, ushort>? OnChauffeurArrived;
|
||||||
|
|
||||||
|
public event Action<string, ushort, uint, string>? OnChauffeurZoneUpdate;
|
||||||
|
|
||||||
|
public event Action<string, ushort>? OnChauffeurMountReady;
|
||||||
|
|
||||||
|
public event Action? OnChauffeurPassengerMounted;
|
||||||
|
|
||||||
|
public event Action<string, ushort, string>? OnHelperStatusUpdate;
|
||||||
|
|
||||||
|
public event Action<string, ushort, uint, Vector3>? OnQuesterPositionUpdate;
|
||||||
|
|
||||||
|
public CrossProcessIPC(IPluginLog log, IFramework framework, Configuration configuration)
|
||||||
|
{
|
||||||
|
this.log = log;
|
||||||
|
this.framework = framework;
|
||||||
|
this.configuration = configuration;
|
||||||
|
InitializeIPC();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeIPC()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
mmf = MemoryMappedFile.CreateOrOpen("QSTCompanion_IPC", 4096L, MemoryMappedFileAccess.ReadWrite);
|
||||||
|
isRunning = true;
|
||||||
|
listenerThread = new Thread(ListenerLoop)
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Name = "QSTCompanion IPC Listener"
|
||||||
|
};
|
||||||
|
listenerThread.Start();
|
||||||
|
log.Information("[CrossProcessIPC] Initialized with Memory-Mapped File");
|
||||||
|
if (configuration.IsHighLevelHelper)
|
||||||
|
{
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
AnnounceHelper();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[CrossProcessIPC] Failed to initialize: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ListenerLoop()
|
||||||
|
{
|
||||||
|
string lastMessage = "";
|
||||||
|
while (isRunning)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (mmf == null)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
using (MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor(0L, 4096L, MemoryMappedFileAccess.Read))
|
||||||
|
{
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
accessor.ReadArray(0L, buffer, 0, 4096);
|
||||||
|
string message = Encoding.UTF8.GetString(buffer).TrimEnd('\0');
|
||||||
|
if (!string.IsNullOrEmpty(message) && message != lastMessage)
|
||||||
|
{
|
||||||
|
lastMessage = message;
|
||||||
|
ProcessMessage(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Thread.Sleep(10);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[CrossProcessIPC] Listener error: " + ex.Message);
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessMessage(string message)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string[] parts = message.Split('|');
|
||||||
|
if (parts.Length < 2)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
string command = parts[0];
|
||||||
|
_003C_003Ec__DisplayClass63_0 CS_0024_003C_003E8__locals0;
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string text = command;
|
||||||
|
if (text != null)
|
||||||
|
{
|
||||||
|
switch (text.Length)
|
||||||
|
{
|
||||||
|
case 16:
|
||||||
|
switch (text[0])
|
||||||
|
{
|
||||||
|
case 'H':
|
||||||
|
if (text == "HELPER_AVAILABLE" && parts.Length >= 3)
|
||||||
|
{
|
||||||
|
string text3 = parts[1];
|
||||||
|
if (ushort.TryParse(parts[2], out var result8))
|
||||||
|
{
|
||||||
|
log.Information($"[CrossProcessIPC] Helper available: {text3}@{result8}");
|
||||||
|
this.OnHelperAvailable?.Invoke(text3, result8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'C':
|
||||||
|
if (text == "CHAUFFEUR_SUMMON" && parts.Length >= 11)
|
||||||
|
{
|
||||||
|
ushort questerWorld = ushort.Parse(parts[2]);
|
||||||
|
uint zoneId = uint.Parse(parts[3]);
|
||||||
|
Vector3 targetPos = new Vector3(float.Parse(parts[4]), float.Parse(parts[5]), float.Parse(parts[6]));
|
||||||
|
Vector3 questerPos = new Vector3(float.Parse(parts[7]), float.Parse(parts[8]), float.Parse(parts[9]));
|
||||||
|
bool isAttuneAetheryte = bool.Parse(parts[10]);
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
this.OnChauffeurSummonRequest?.Invoke((string)(object)CS_0024_003C_003E8__locals0, questerWorld, zoneId, targetPos, questerPos, isAttuneAetheryte);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Q':
|
||||||
|
if (text == "QUESTER_POSITION" && parts.Length >= 7)
|
||||||
|
{
|
||||||
|
string arg3 = parts[1];
|
||||||
|
if (ushort.TryParse(parts[2], out var result3) && uint.TryParse(parts[3], out var result4) && float.TryParse(parts[4], NumberStyles.Float, CultureInfo.InvariantCulture, out var result5) && float.TryParse(parts[5], NumberStyles.Float, CultureInfo.InvariantCulture, out var result6) && float.TryParse(parts[6], NumberStyles.Float, CultureInfo.InvariantCulture, out var result7))
|
||||||
|
{
|
||||||
|
Vector3 arg4 = new Vector3(result5, result6, result7);
|
||||||
|
this.OnQuesterPositionUpdate?.Invoke(arg3, result3, result4, arg4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 14:
|
||||||
|
switch (text[7])
|
||||||
|
{
|
||||||
|
case 'R':
|
||||||
|
if (text == "HELPER_REQUEST" && parts.Length >= 3)
|
||||||
|
{
|
||||||
|
string text12 = parts[1];
|
||||||
|
if (ushort.TryParse(parts[2], out var result14))
|
||||||
|
{
|
||||||
|
log.Information($"[CrossProcessIPC] Helper request: {text12}@{result14}");
|
||||||
|
this.OnHelperRequested?.Invoke(text12, result14);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'D':
|
||||||
|
if (text == "HELPER_DISMISS")
|
||||||
|
{
|
||||||
|
log.Information("[CrossProcessIPC] Helper dismiss");
|
||||||
|
this.OnHelperDismissed?.Invoke();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'I':
|
||||||
|
if (text == "HELPER_IN_DUTY" && parts.Length >= 3)
|
||||||
|
{
|
||||||
|
string text11 = parts[1];
|
||||||
|
if (ushort.TryParse(parts[2], out var result13))
|
||||||
|
{
|
||||||
|
log.Information($"[CrossProcessIPC] Helper in duty: {text11}@{result13}");
|
||||||
|
this.OnHelperInDuty?.Invoke(text11, result13);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 15:
|
||||||
|
switch (text[0])
|
||||||
|
{
|
||||||
|
case 'H':
|
||||||
|
if (text == "HELPER_IN_PARTY" && parts.Length >= 3)
|
||||||
|
{
|
||||||
|
string text9 = parts[1];
|
||||||
|
if (ushort.TryParse(parts[2], out var result12))
|
||||||
|
{
|
||||||
|
log.Information($"[CrossProcessIPC] Helper in party: {text9}@{result12}");
|
||||||
|
this.OnHelperInParty?.Invoke(text9, result12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'C':
|
||||||
|
if (text == "CHAUFFEUR_READY" && parts.Length >= 2)
|
||||||
|
{
|
||||||
|
string text8 = parts[1];
|
||||||
|
log.Information("[CrossProcessIPC] Chauffeur ready: " + text8);
|
||||||
|
this.OnChauffeurReadyForPickup?.Invoke(text8);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 21:
|
||||||
|
switch (text[10])
|
||||||
|
{
|
||||||
|
case 'Z':
|
||||||
|
if (text == "CHAUFFEUR_ZONE_UPDATE" && parts.Length >= 5)
|
||||||
|
{
|
||||||
|
string text5 = parts[1];
|
||||||
|
if (ushort.TryParse(parts[2], out var result10) && uint.TryParse(parts[3], out var result11))
|
||||||
|
{
|
||||||
|
string text6 = parts[4];
|
||||||
|
log.Information($"[CrossProcessIPC] Zone update: {text5}@{result10} -> {text6} ({result11})");
|
||||||
|
this.OnChauffeurZoneUpdate?.Invoke(text5, result10, result11, text6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'M':
|
||||||
|
if (text == "CHAUFFEUR_MOUNT_READY" && parts.Length >= 3)
|
||||||
|
{
|
||||||
|
string text4 = parts[1];
|
||||||
|
if (ushort.TryParse(parts[2], out var result9))
|
||||||
|
{
|
||||||
|
log.Information($"[CrossProcessIPC] Chauffeur mount ready for: {text4}@{result9}");
|
||||||
|
IPluginLog pluginLog = log;
|
||||||
|
DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(53, 1);
|
||||||
|
defaultInterpolatedStringHandler.AppendLiteral("[CrossProcessIPC] OnChauffeurMountReady subscribers: ");
|
||||||
|
Action<string, ushort>? action = this.OnChauffeurMountReady;
|
||||||
|
defaultInterpolatedStringHandler.AppendFormatted((action != null) ? action.GetInvocationList().Length : 0);
|
||||||
|
pluginLog.Information(defaultInterpolatedStringHandler.ToStringAndClear());
|
||||||
|
this.OnChauffeurMountReady?.Invoke(text4, result9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
if (text == "CHAT" && parts.Length >= 2)
|
||||||
|
{
|
||||||
|
string text7 = parts[1];
|
||||||
|
log.Information("[CrossProcessIPC] Chat: " + text7);
|
||||||
|
this.OnChatMessageReceived?.Invoke(text7);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
|
if (text == "COMMAND" && parts.Length >= 2)
|
||||||
|
{
|
||||||
|
string text10 = parts[1];
|
||||||
|
log.Information("[CrossProcessIPC] Command: " + text10);
|
||||||
|
this.OnCommandReceived?.Invoke(text10);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 12:
|
||||||
|
if (text == "HELPER_READY" && parts.Length >= 3)
|
||||||
|
{
|
||||||
|
string text13 = parts[1];
|
||||||
|
if (ushort.TryParse(parts[2], out var result15))
|
||||||
|
{
|
||||||
|
log.Information($"[CrossProcessIPC] Helper ready: {text13}@{result15}");
|
||||||
|
this.OnHelperReady?.Invoke(text13, result15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 28:
|
||||||
|
if (text == "REQUEST_HELPER_ANNOUNCEMENTS")
|
||||||
|
{
|
||||||
|
log.Information("[CrossProcessIPC] Request for helper announcements received");
|
||||||
|
this.OnRequestHelperAnnouncements?.Invoke();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 17:
|
||||||
|
if (text == "CHAUFFEUR_ARRIVED" && parts.Length >= 3)
|
||||||
|
{
|
||||||
|
string text2 = parts[1];
|
||||||
|
if (ushort.TryParse(parts[2], out var result2))
|
||||||
|
{
|
||||||
|
log.Information($"[CrossProcessIPC] Chauffeur arrived for: {text2}@{result2}");
|
||||||
|
this.OnChauffeurArrived?.Invoke(text2, result2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 27:
|
||||||
|
if (text == "CHAUFFEUR_PASSENGER_MOUNTED")
|
||||||
|
{
|
||||||
|
log.Information("[CrossProcessIPC] Chauffeur passenger mounted signal received");
|
||||||
|
IPluginLog pluginLog2 = log;
|
||||||
|
DefaultInterpolatedStringHandler defaultInterpolatedStringHandler2 = new DefaultInterpolatedStringHandler(59, 1);
|
||||||
|
defaultInterpolatedStringHandler2.AppendLiteral("[CrossProcessIPC] OnChauffeurPassengerMounted subscribers: ");
|
||||||
|
Action? action2 = this.OnChauffeurPassengerMounted;
|
||||||
|
defaultInterpolatedStringHandler2.AppendFormatted((action2 != null) ? action2.GetInvocationList().Length : 0);
|
||||||
|
pluginLog2.Information(defaultInterpolatedStringHandler2.ToStringAndClear());
|
||||||
|
this.OnChauffeurPassengerMounted?.Invoke();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 13:
|
||||||
|
if (text == "HELPER_STATUS" && parts.Length >= 4)
|
||||||
|
{
|
||||||
|
string arg = parts[1];
|
||||||
|
if (ushort.TryParse(parts[2], out var result))
|
||||||
|
{
|
||||||
|
string arg2 = parts[3];
|
||||||
|
this.OnHelperStatusUpdate?.Invoke(arg, result, arg2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
log.Error("[CrossProcessIPC] Error in event handler: " + ex2.Message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[CrossProcessIPC] Error processing message: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendMessage(string message)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (mmf == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
using MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor(0L, 4096L, MemoryMappedFileAccess.Write);
|
||||||
|
byte[] buffer = Encoding.UTF8.GetBytes(message);
|
||||||
|
if (buffer.Length > 4095)
|
||||||
|
{
|
||||||
|
log.Warning($"[CrossProcessIPC] Message too large: {buffer.Length} bytes");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
byte[] clearBuffer = new byte[4096];
|
||||||
|
accessor.WriteArray(0L, clearBuffer, 0, 4096);
|
||||||
|
accessor.WriteArray(0L, buffer, 0, buffer.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[CrossProcessIPC] Failed to send message: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AnnounceHelper()
|
||||||
|
{
|
||||||
|
if (configuration.IsHighLevelHelper)
|
||||||
|
{
|
||||||
|
IPlayerCharacter localPlayer = Plugin.ClientState?.LocalPlayer;
|
||||||
|
if (localPlayer != null)
|
||||||
|
{
|
||||||
|
string name = localPlayer.Name.ToString();
|
||||||
|
ushort worldId = (ushort)localPlayer.HomeWorld.RowId;
|
||||||
|
SendMessage($"HELPER_AVAILABLE|{name}|{worldId}");
|
||||||
|
log.Information($"[CrossProcessIPC] Announced as helper: {name}@{worldId}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RequestHelper(string characterName, ushort worldId)
|
||||||
|
{
|
||||||
|
SendMessage($"HELPER_REQUEST|{characterName}|{worldId}");
|
||||||
|
log.Information($"[CrossProcessIPC] Requested helper: {characterName}@{worldId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DismissHelper()
|
||||||
|
{
|
||||||
|
SendMessage("HELPER_DISMISS");
|
||||||
|
log.Information("[CrossProcessIPC] Dismissed helper");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendChatMessage(string message)
|
||||||
|
{
|
||||||
|
SendMessage("CHAT|" + message);
|
||||||
|
log.Information("[CrossProcessIPC] Chat: " + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendCommand(string command)
|
||||||
|
{
|
||||||
|
SendMessage("COMMAND|" + command);
|
||||||
|
log.Information("[CrossProcessIPC] Command: " + command);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NotifyHelperInParty(string name, ushort worldId)
|
||||||
|
{
|
||||||
|
SendMessage($"HELPER_IN_PARTY|{name}|{worldId}");
|
||||||
|
log.Information($"[CrossProcessIPC] Notified: Helper in party {name}@{worldId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NotifyHelperInDuty(string name, ushort worldId)
|
||||||
|
{
|
||||||
|
SendMessage($"HELPER_IN_DUTY|{name}|{worldId}");
|
||||||
|
log.Information($"[CrossProcessIPC] Notified: Helper in duty {name}@{worldId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NotifyHelperReady(string name, ushort worldId)
|
||||||
|
{
|
||||||
|
SendMessage($"HELPER_READY|{name}|{worldId}");
|
||||||
|
log.Information($"[CrossProcessIPC] Notified: Helper ready {name}@{worldId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RequestHelperAnnouncements()
|
||||||
|
{
|
||||||
|
SendMessage("REQUEST_HELPER_ANNOUNCEMENTS");
|
||||||
|
log.Information("[CrossProcessIPC] Requesting helper announcements from all clients");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendChauffeurSummonRequest(string questerName, ushort questerWorld, uint zoneId, Vector3 targetPos, Vector3 questerPos, bool isAttuneAetheryte)
|
||||||
|
{
|
||||||
|
SendMessage($"CHAUFFEUR_SUMMON|{questerName}|{questerWorld}|{zoneId}|{targetPos.X}|{targetPos.Y}|{targetPos.Z}|{questerPos.X}|{questerPos.Y}|{questerPos.Z}|{isAttuneAetheryte}");
|
||||||
|
log.Information($"[CrossProcessIPC] Chauffeur summon: {questerName}@{questerWorld} zone {zoneId} quester@({questerPos.X:F2},{questerPos.Y:F2},{questerPos.Z:F2}) AttuneAetheryte={isAttuneAetheryte}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendChauffeurMountReady(string questerName, ushort questerWorld)
|
||||||
|
{
|
||||||
|
SendMessage($"CHAUFFEUR_MOUNT_READY|{questerName}|{questerWorld}");
|
||||||
|
log.Information($"[CrossProcessIPC] Chauffeur mount ready for RidePillion: {questerName}@{questerWorld}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendChauffeurPassengerMounted()
|
||||||
|
{
|
||||||
|
SendMessage("CHAUFFEUR_PASSENGER_MOUNTED");
|
||||||
|
log.Debug("[CrossProcessIPC] Sent: CHAUFFEUR_PASSENGER_MOUNTED");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendChauffeurReadyForPickup(string helperName)
|
||||||
|
{
|
||||||
|
SendMessage("CHAUFFEUR_READY|" + helperName);
|
||||||
|
log.Information("[CrossProcessIPC] Chauffeur ready: " + helperName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendChauffeurArrived(string questerName, ushort questerWorld)
|
||||||
|
{
|
||||||
|
SendMessage($"CHAUFFEUR_ARRIVED|{questerName}|{questerWorld}");
|
||||||
|
log.Information($"[CrossProcessIPC] Chauffeur arrived for: {questerName}@{questerWorld}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendChauffeurZoneUpdate(string characterName, ushort worldId, uint zoneId, string zoneName)
|
||||||
|
{
|
||||||
|
SendMessage($"CHAUFFEUR_ZONE_UPDATE|{characterName}|{worldId}|{zoneId}|{zoneName}");
|
||||||
|
log.Information($"[CrossProcessIPC] Zone update: {characterName}@{worldId} -> {zoneName} ({zoneId})");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void BroadcastHelperStatus(string helperName, ushort helperWorld, string status)
|
||||||
|
{
|
||||||
|
SendMessage($"HELPER_STATUS|{helperName}|{helperWorld}|{status}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void BroadcastQuesterPosition(string questerName, ushort questerWorld, uint zoneId, Vector3 position)
|
||||||
|
{
|
||||||
|
SendMessage($"QUESTER_POSITION|{questerName}|{questerWorld}|{zoneId}|{position.X.ToString(CultureInfo.InvariantCulture)}|{position.Y.ToString(CultureInfo.InvariantCulture)}|{position.Z.ToString(CultureInfo.InvariantCulture)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
isRunning = false;
|
||||||
|
listenerThread?.Join(1000);
|
||||||
|
mmf?.Dispose();
|
||||||
|
log.Information("[CrossProcessIPC] Disposed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class CurrentTask
|
||||||
|
{
|
||||||
|
public required string Type { get; init; }
|
||||||
|
|
||||||
|
public ushort TerritoryId { get; init; }
|
||||||
|
|
||||||
|
public string? InteractionType { get; init; }
|
||||||
|
|
||||||
|
public Vector3? Position { get; init; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,374 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class DCTravelService : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private readonly Configuration config;
|
||||||
|
|
||||||
|
private readonly IClientState clientState;
|
||||||
|
|
||||||
|
private readonly LifestreamIPC lifestreamIPC;
|
||||||
|
|
||||||
|
private readonly QuestionableIPC questionableIPC;
|
||||||
|
|
||||||
|
private readonly CharacterSafeWaitService safeWaitService;
|
||||||
|
|
||||||
|
private readonly ICommandManager commandManager;
|
||||||
|
|
||||||
|
private readonly IFramework framework;
|
||||||
|
|
||||||
|
private bool dcTravelCompleted;
|
||||||
|
|
||||||
|
private bool dcTravelInProgress;
|
||||||
|
|
||||||
|
public DCTravelService(IPluginLog log, Configuration config, LifestreamIPC lifestreamIPC, QuestionableIPC questionableIPC, CharacterSafeWaitService safeWaitService, IClientState clientState, ICommandManager commandManager, IFramework framework)
|
||||||
|
{
|
||||||
|
this.log = log;
|
||||||
|
this.config = config;
|
||||||
|
this.lifestreamIPC = lifestreamIPC;
|
||||||
|
this.questionableIPC = questionableIPC;
|
||||||
|
this.safeWaitService = safeWaitService;
|
||||||
|
this.clientState = clientState;
|
||||||
|
this.commandManager = commandManager;
|
||||||
|
this.framework = framework;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShouldPerformDCTravel()
|
||||||
|
{
|
||||||
|
log.Information("[DCTravel] ========================================");
|
||||||
|
log.Information("[DCTravel] === DC TRAVEL CHECK START ===");
|
||||||
|
log.Information("[DCTravel] ========================================");
|
||||||
|
log.Information($"[DCTravel] Config.EnableDCTravel: {config.EnableDCTravel}");
|
||||||
|
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");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (string.IsNullOrEmpty(config.DCTravelWorld))
|
||||||
|
{
|
||||||
|
log.Warning("[DCTravel] SKIP: No target world configured");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (clientState.LocalPlayer == null)
|
||||||
|
{
|
||||||
|
log.Error("[DCTravel] SKIP: LocalPlayer is NULL");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
string currentWorld = clientState.LocalPlayer.CurrentWorld.Value.Name.ToString();
|
||||||
|
log.Information("[DCTravel] Current World: '" + currentWorld + "'");
|
||||||
|
log.Information("[DCTravel] Target World: '" + config.DCTravelWorld + "'");
|
||||||
|
if (currentWorld.Equals(config.DCTravelWorld, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
log.Warning("[DCTravel] SKIP: Already on target world '" + config.DCTravelWorld + "'");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log.Information("[DCTravel] ========================================");
|
||||||
|
log.Information("[DCTravel] DC TRAVEL WILL BE PERFORMED!");
|
||||||
|
log.Information("[DCTravel] ========================================");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> PerformDCTravel()
|
||||||
|
{
|
||||||
|
if (dcTravelInProgress)
|
||||||
|
{
|
||||||
|
log.Warning("[DCTravel] DC Travel already in progress");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (string.IsNullOrEmpty(config.DCTravelWorld))
|
||||||
|
{
|
||||||
|
log.Error("[DCTravel] No target world configured");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log.Information("[DCTravel] ========================================");
|
||||||
|
log.Information("[DCTravel] === CHECKING LIFESTREAM AVAILABILITY ===");
|
||||||
|
log.Information("[DCTravel] ========================================");
|
||||||
|
log.Information($"[DCTravel] lifestreamIPC.IsAvailable (cached): {lifestreamIPC.IsAvailable}");
|
||||||
|
bool isAvailable = lifestreamIPC.ForceCheckAvailability();
|
||||||
|
log.Information($"[DCTravel] lifestreamIPC.ForceCheckAvailability() result: {isAvailable}");
|
||||||
|
if (!isAvailable)
|
||||||
|
{
|
||||||
|
log.Error("[DCTravel] ========================================");
|
||||||
|
log.Error("[DCTravel] ======= LIFESTREAM NOT AVAILABLE! ======");
|
||||||
|
log.Error("[DCTravel] ========================================");
|
||||||
|
log.Error("[DCTravel] Possible reasons:");
|
||||||
|
log.Error("[DCTravel] 1. Lifestream plugin is not installed");
|
||||||
|
log.Error("[DCTravel] 2. Lifestream plugin is not enabled");
|
||||||
|
log.Error("[DCTravel] 3. Lifestream plugin failed to load");
|
||||||
|
log.Error("[DCTravel] 4. IPC communication error");
|
||||||
|
log.Error("[DCTravel] ========================================");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log.Information("[DCTravel] Lifestream is available!");
|
||||||
|
dcTravelInProgress = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information("[DCTravel] === INITIATING DATA CENTER TRAVEL ===");
|
||||||
|
log.Information("[DCTravel] Target World: " + config.DCTravelWorld);
|
||||||
|
log.Information("[DCTravel] ========================================");
|
||||||
|
log.Information("[DCTravel] === STOPPING QUESTIONABLE ===");
|
||||||
|
log.Information("[DCTravel] ========================================");
|
||||||
|
log.Information($"[DCTravel] Questionable IsRunning: {questionableIPC.IsRunning()}");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/qst stop");
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
});
|
||||||
|
log.Information("[DCTravel] /qst stop command sent on Framework Thread");
|
||||||
|
log.Information($"[DCTravel] Questionable IsRunning after stop: {questionableIPC.IsRunning()}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[DCTravel] Error stopping Questionable: " + ex.Message);
|
||||||
|
}
|
||||||
|
log.Information("[DCTravel] Initiating travel to " + config.DCTravelWorld + "...");
|
||||||
|
log.Information("[DCTravel] Checking Lifestream status before travel...");
|
||||||
|
log.Information($"[DCTravel] Lifestream.IsAvailable: {lifestreamIPC.IsAvailable}");
|
||||||
|
log.Information($"[DCTravel] Lifestream.IsBusy(): {lifestreamIPC.IsBusy()}");
|
||||||
|
if (lifestreamIPC.IsBusy())
|
||||||
|
{
|
||||||
|
log.Error("[DCTravel] Lifestream is BUSY! Cannot start travel!");
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/qst start");
|
||||||
|
}).Wait();
|
||||||
|
log.Information("[DCTravel] /qst start command sent on Framework Thread (recovery)");
|
||||||
|
dcTravelInProgress = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log.Information("[DCTravel] Sending /li " + config.DCTravelWorld + " command on Framework Thread...");
|
||||||
|
bool commandSuccess = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/li " + config.DCTravelWorld);
|
||||||
|
commandSuccess = true;
|
||||||
|
log.Information("[DCTravel] /li " + config.DCTravelWorld + " command executed on Framework Thread");
|
||||||
|
}
|
||||||
|
catch (Exception ex5)
|
||||||
|
{
|
||||||
|
log.Error("[DCTravel] Failed to execute /li command: " + ex5.Message);
|
||||||
|
commandSuccess = false;
|
||||||
|
}
|
||||||
|
}).Wait();
|
||||||
|
if (!commandSuccess)
|
||||||
|
{
|
||||||
|
log.Error("[DCTravel] Failed to send /li command");
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/qst start");
|
||||||
|
}).Wait();
|
||||||
|
log.Information("[DCTravel] /qst start command sent (recovery)");
|
||||||
|
dcTravelInProgress = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
bool isBusy = lifestreamIPC.IsBusy();
|
||||||
|
log.Information($"[DCTravel] Lifestream.IsBusy() after command: {isBusy}");
|
||||||
|
if (!isBusy)
|
||||||
|
{
|
||||||
|
log.Warning("[DCTravel] Lifestream did not become busy after command!");
|
||||||
|
log.Warning("[DCTravel] Travel may not have started - check Lifestream manually");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
log.Error("[DCTravel] Error sending /li command: " + ex2.Message);
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/qst start");
|
||||||
|
}).Wait();
|
||||||
|
log.Information("[DCTravel] /qst start command sent (recovery)");
|
||||||
|
dcTravelInProgress = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log.Information("[DCTravel] Waiting for travel completion...");
|
||||||
|
if (!(await WaitForTravelCompletion(120)))
|
||||||
|
{
|
||||||
|
log.Warning("[DCTravel] Travel timeout - proceeding anyway");
|
||||||
|
}
|
||||||
|
log.Information("[DCTravel] ========================================");
|
||||||
|
log.Information("[DCTravel] === RESUMING QUESTIONABLE ===");
|
||||||
|
log.Information("[DCTravel] ========================================");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/qst start");
|
||||||
|
}).Wait();
|
||||||
|
log.Information("[DCTravel] /qst start command sent on Framework Thread");
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
log.Information($"[DCTravel] Questionable IsRunning after start: {questionableIPC.IsRunning()}");
|
||||||
|
}
|
||||||
|
catch (Exception ex3)
|
||||||
|
{
|
||||||
|
log.Error("[DCTravel] Error starting Questionable: " + ex3.Message);
|
||||||
|
}
|
||||||
|
dcTravelCompleted = true;
|
||||||
|
dcTravelInProgress = false;
|
||||||
|
log.Information("[DCTravel] === DATA CENTER TRAVEL COMPLETE ===");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex4)
|
||||||
|
{
|
||||||
|
log.Error("[DCTravel] Error during DC travel: " + ex4.Message);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/qst start");
|
||||||
|
}).Wait();
|
||||||
|
log.Information("[DCTravel] /qst start command sent on Framework Thread (error recovery)");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
dcTravelInProgress = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> WaitForTravelCompletion(int timeoutSeconds)
|
||||||
|
{
|
||||||
|
DateTime startTime = DateTime.Now;
|
||||||
|
TimeSpan timeout = TimeSpan.FromSeconds(timeoutSeconds);
|
||||||
|
log.Information("[DCTravel] ========================================");
|
||||||
|
log.Information("[DCTravel] === WAITING FOR TRAVEL TO START ===");
|
||||||
|
log.Information("[DCTravel] ========================================");
|
||||||
|
DateTime phase1Start = DateTime.Now;
|
||||||
|
TimeSpan phase1Timeout = TimeSpan.FromSeconds(30L);
|
||||||
|
bool travelStarted = false;
|
||||||
|
log.Information("[DCTravel] Waiting 5 seconds for Lifestream to queue travel tasks...");
|
||||||
|
await Task.Delay(5000);
|
||||||
|
while (DateTime.Now - phase1Start < phase1Timeout)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (lifestreamIPC.IsBusy())
|
||||||
|
{
|
||||||
|
log.Information("[DCTravel] Lifestream travel has STARTED!");
|
||||||
|
travelStarted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
double elapsed = (DateTime.Now - phase1Start).TotalSeconds;
|
||||||
|
if (elapsed % 5.0 < 0.5)
|
||||||
|
{
|
||||||
|
log.Information($"[DCTravel] Waiting for travel to start... ({elapsed:F1}s)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Debug("[DCTravel] Error checking Lifestream status: " + ex.Message);
|
||||||
|
}
|
||||||
|
await Task.Delay(1000);
|
||||||
|
}
|
||||||
|
if (!travelStarted)
|
||||||
|
{
|
||||||
|
log.Error("[DCTravel] ❌ Travel did not start within 30 seconds!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log.Information("[DCTravel] ========================================");
|
||||||
|
log.Information("[DCTravel] === WAITING FOR TRAVEL TO COMPLETE ===");
|
||||||
|
log.Information("[DCTravel] ========================================");
|
||||||
|
while (DateTime.Now - startTime < timeout)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!lifestreamIPC.IsBusy())
|
||||||
|
{
|
||||||
|
log.Information("[DCTravel] Lifestream is no longer busy!");
|
||||||
|
log.Information("[DCTravel] Waiting 30 seconds for character to stabilize...");
|
||||||
|
await Task.Delay(30000);
|
||||||
|
log.Information("[DCTravel] Travel complete!");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
double elapsed2 = (DateTime.Now - startTime).TotalSeconds;
|
||||||
|
if (elapsed2 % 10.0 < 0.5)
|
||||||
|
{
|
||||||
|
log.Information($"[DCTravel] Lifestream is busy (traveling)... ({elapsed2:F0}s)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
log.Debug("[DCTravel] Error checking Lifestream status: " + ex2.Message);
|
||||||
|
}
|
||||||
|
await Task.Delay(1000);
|
||||||
|
}
|
||||||
|
log.Warning($"[DCTravel] Travel timeout after {timeoutSeconds}s");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> ReturnToHomeworld()
|
||||||
|
{
|
||||||
|
if (!lifestreamIPC.IsAvailable)
|
||||||
|
{
|
||||||
|
log.Warning("[DCTravel] Lifestream not available for homeworld return");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (clientState.LocalPlayer == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
string homeWorld = clientState.LocalPlayer.HomeWorld.Value.Name.ToString();
|
||||||
|
string currentWorld = clientState.LocalPlayer.CurrentWorld.Value.Name.ToString();
|
||||||
|
if (homeWorld.Equals(currentWorld, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
log.Information("[DCTravel] Already on homeworld");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
log.Information("[DCTravel] Returning to homeworld: " + homeWorld);
|
||||||
|
bool success = lifestreamIPC.ChangeWorld(homeWorld);
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
await Task.Delay(2000);
|
||||||
|
log.Information("[DCTravel] Homeworld return initiated");
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetDCTravelState()
|
||||||
|
{
|
||||||
|
dcTravelCompleted = false;
|
||||||
|
dcTravelInProgress = false;
|
||||||
|
log.Information("[DCTravel] DC Travel state reset");
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsDCTravelCompleted()
|
||||||
|
{
|
||||||
|
return dcTravelCompleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsDCTravelInProgress()
|
||||||
|
{
|
||||||
|
return dcTravelInProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
log.Information("[DCTravel] Service disposed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,206 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Lumina.Excel;
|
||||||
|
using Lumina.Excel.Sheets;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class DataCenterService
|
||||||
|
{
|
||||||
|
private readonly IDataManager dataManager;
|
||||||
|
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private readonly Dictionary<string, string> worldToDCCache = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
private readonly Dictionary<string, string> dataCenterToRegion = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "Chaos", "EU" },
|
||||||
|
{ "Light", "EU" },
|
||||||
|
{ "Shadow", "EU" },
|
||||||
|
{ "Aether", "NA" },
|
||||||
|
{ "Primal", "NA" },
|
||||||
|
{ "Crystal", "NA" },
|
||||||
|
{ "Dynamis", "NA" },
|
||||||
|
{ "Elemental", "JP" },
|
||||||
|
{ "Gaia", "JP" },
|
||||||
|
{ "Mana", "JP" },
|
||||||
|
{ "Meteor", "JP" },
|
||||||
|
{ "Materia", "OCE" },
|
||||||
|
{ "陆行鸟", "Others" },
|
||||||
|
{ "莫古力", "Others" },
|
||||||
|
{ "猫小胖", "Others" },
|
||||||
|
{ "豆豆柴", "Others" }
|
||||||
|
};
|
||||||
|
|
||||||
|
public DataCenterService(IDataManager dataManager, IPluginLog log)
|
||||||
|
{
|
||||||
|
this.dataManager = dataManager;
|
||||||
|
this.log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InitializeWorldMapping()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ExcelSheet<World> worldSheet = dataManager.GetExcelSheet<World>();
|
||||||
|
if (worldSheet == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int worldCount = 0;
|
||||||
|
int skippedCount = 0;
|
||||||
|
foreach (World world in worldSheet)
|
||||||
|
{
|
||||||
|
if (world.RowId == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
string worldName = world.Name.ExtractText();
|
||||||
|
if (string.IsNullOrEmpty(worldName))
|
||||||
|
{
|
||||||
|
skippedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
WorldDCGroupType? dataCenterGroup = world.DataCenter.ValueNullable;
|
||||||
|
if (!dataCenterGroup.HasValue)
|
||||||
|
{
|
||||||
|
skippedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
string dataCenterName = dataCenterGroup.Value.Name.ExtractText();
|
||||||
|
if (string.IsNullOrEmpty(dataCenterName))
|
||||||
|
{
|
||||||
|
skippedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!world.IsPublic)
|
||||||
|
{
|
||||||
|
skippedCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
string region = GetRegionForDataCenter(dataCenterName);
|
||||||
|
worldToDCCache[worldName.ToLower()] = region;
|
||||||
|
worldCount++;
|
||||||
|
if (worldCount > 10)
|
||||||
|
{
|
||||||
|
_ = region != "Others";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetRegionForDataCenter(string dataCenterName)
|
||||||
|
{
|
||||||
|
if (dataCenterToRegion.TryGetValue(dataCenterName, out string region))
|
||||||
|
{
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
return "Others";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetDataCenterForWorld(string worldName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(worldName))
|
||||||
|
{
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
string key = worldName.ToLower();
|
||||||
|
if (worldToDCCache.TryGetValue(key, out string dataCenter))
|
||||||
|
{
|
||||||
|
return dataCenter;
|
||||||
|
}
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, List<string>> GroupCharactersByDataCenter(List<string> characters)
|
||||||
|
{
|
||||||
|
Dictionary<string, List<string>> grouped = new Dictionary<string, List<string>>
|
||||||
|
{
|
||||||
|
{
|
||||||
|
"EU",
|
||||||
|
new List<string>()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NA",
|
||||||
|
new List<string>()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"JP",
|
||||||
|
new List<string>()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"OCE",
|
||||||
|
new List<string>()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Others",
|
||||||
|
new List<string>()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Unknown",
|
||||||
|
new List<string>()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
foreach (string character in characters)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string[] parts = character.Split('@');
|
||||||
|
if (parts.Length != 2)
|
||||||
|
{
|
||||||
|
grouped["Unknown"].Add(character);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
string worldName = parts[1];
|
||||||
|
string dataCenter = GetDataCenterForWorld(worldName);
|
||||||
|
if (!grouped.ContainsKey(dataCenter))
|
||||||
|
{
|
||||||
|
grouped[dataCenter] = new List<string>();
|
||||||
|
}
|
||||||
|
grouped[dataCenter].Add(character);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
grouped["Unknown"].Add(character);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (KeyValuePair<string, List<string>> item in grouped.Where((KeyValuePair<string, List<string>> g) => g.Value.Count > 0))
|
||||||
|
{
|
||||||
|
_ = item;
|
||||||
|
}
|
||||||
|
return grouped;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> GetAvailableDataCenters(Dictionary<string, List<string>> charactersByDataCenter)
|
||||||
|
{
|
||||||
|
List<string> dataCenters = new List<string> { "All" };
|
||||||
|
string[] array = new string[6] { "EU", "NA", "JP", "OCE", "Others", "Unknown" };
|
||||||
|
foreach (string dc in array)
|
||||||
|
{
|
||||||
|
if (charactersByDataCenter.TryGetValue(dc, out List<string> chars) && chars.Count > 0)
|
||||||
|
{
|
||||||
|
dataCenters.Add(dc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dataCenters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> GetCharactersForDataCenter(List<string> allCharacters, string dataCenterName, Dictionary<string, List<string>> charactersByDataCenter)
|
||||||
|
{
|
||||||
|
if (dataCenterName == "All")
|
||||||
|
{
|
||||||
|
return allCharacters;
|
||||||
|
}
|
||||||
|
if (charactersByDataCenter.TryGetValue(dataCenterName, out List<string> characters))
|
||||||
|
{
|
||||||
|
return characters;
|
||||||
|
}
|
||||||
|
return new List<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,252 @@
|
||||||
|
using System;
|
||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
|
using Dalamud.Game.NativeWrapper;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
|
using Lumina.Excel;
|
||||||
|
using Lumina.Excel.Sheets;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class DeathHandlerService : IDisposable
|
||||||
|
{
|
||||||
|
private readonly ICondition condition;
|
||||||
|
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private readonly IClientState clientState;
|
||||||
|
|
||||||
|
private readonly ICommandManager commandManager;
|
||||||
|
|
||||||
|
private readonly IFramework framework;
|
||||||
|
|
||||||
|
private readonly Configuration config;
|
||||||
|
|
||||||
|
private readonly IGameGui gameGui;
|
||||||
|
|
||||||
|
private readonly IDataManager dataManager;
|
||||||
|
|
||||||
|
private bool wasDead;
|
||||||
|
|
||||||
|
private Vector3 deathPosition = Vector3.Zero;
|
||||||
|
|
||||||
|
private uint deathTerritoryId;
|
||||||
|
|
||||||
|
private DateTime deathTime = DateTime.MinValue;
|
||||||
|
|
||||||
|
private bool hasClickedYesNo;
|
||||||
|
|
||||||
|
private bool needsTeleportBack;
|
||||||
|
|
||||||
|
private bool isRotationActive;
|
||||||
|
|
||||||
|
public bool IsDead { get; private set; }
|
||||||
|
|
||||||
|
public TimeSpan TimeSinceDeath
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!IsDead || !(deathTime != DateTime.MinValue))
|
||||||
|
{
|
||||||
|
return TimeSpan.Zero;
|
||||||
|
}
|
||||||
|
return DateTime.Now - deathTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DeathHandlerService(ICondition condition, IPluginLog log, IClientState clientState, ICommandManager commandManager, IFramework framework, Configuration config, IGameGui gameGui, IDataManager dataManager)
|
||||||
|
{
|
||||||
|
this.condition = condition;
|
||||||
|
this.log = log;
|
||||||
|
this.clientState = clientState;
|
||||||
|
this.commandManager = commandManager;
|
||||||
|
this.framework = framework;
|
||||||
|
this.config = config;
|
||||||
|
this.gameGui = gameGui;
|
||||||
|
this.dataManager = dataManager;
|
||||||
|
log.Information("[DeathHandler] Service initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetRotationActive(bool active)
|
||||||
|
{
|
||||||
|
isRotationActive = active;
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe void Update()
|
||||||
|
{
|
||||||
|
if (clientState.LocalPlayer == null || !clientState.IsLoggedIn || !config.EnableDeathHandling)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IPlayerCharacter player = clientState.LocalPlayer;
|
||||||
|
uint currentHp = player.CurrentHp;
|
||||||
|
if (currentHp == 0 && !wasDead)
|
||||||
|
{
|
||||||
|
IsDead = true;
|
||||||
|
wasDead = true;
|
||||||
|
deathTime = DateTime.Now;
|
||||||
|
deathPosition = player.Position;
|
||||||
|
deathTerritoryId = clientState.TerritoryType;
|
||||||
|
hasClickedYesNo = false;
|
||||||
|
needsTeleportBack = false;
|
||||||
|
string territoryName = GetTerritoryName(deathTerritoryId);
|
||||||
|
log.Warning("[DeathHandler] ========================================");
|
||||||
|
log.Warning("[DeathHandler] === PLAYER DIED (0% HP) ===");
|
||||||
|
log.Warning("[DeathHandler] ========================================");
|
||||||
|
log.Information($"[DeathHandler] Death Position: {deathPosition}");
|
||||||
|
log.Information($"[DeathHandler] Death Territory: {territoryName} ({deathTerritoryId})");
|
||||||
|
}
|
||||||
|
if (IsDead && !hasClickedYesNo)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AtkUnitBasePtr addonPtr = gameGui.GetAddonByName("SelectYesno");
|
||||||
|
if (addonPtr != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
log.Information("[DeathHandler] SelectYesNo addon detected - clicking YES");
|
||||||
|
AtkUnitBase* addon = (AtkUnitBase*)(nint)addonPtr;
|
||||||
|
if (addon != null && addon->IsVisible)
|
||||||
|
{
|
||||||
|
AtkValue* values = stackalloc AtkValue[1];
|
||||||
|
*values = new AtkValue
|
||||||
|
{
|
||||||
|
Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int,
|
||||||
|
Int = 0
|
||||||
|
};
|
||||||
|
addon->FireCallback(1u, values);
|
||||||
|
hasClickedYesNo = true;
|
||||||
|
log.Information("[DeathHandler] ✓ SelectYesNo callback fired (YES)");
|
||||||
|
log.Information("[DeathHandler] Waiting for respawn...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[DeathHandler] Error clicking SelectYesNo: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (wasDead && currentHp != 0 && !player.IsDead)
|
||||||
|
{
|
||||||
|
log.Information("[DeathHandler] ========================================");
|
||||||
|
log.Information("[DeathHandler] === PLAYER RESPAWNED ===");
|
||||||
|
log.Information("[DeathHandler] ========================================");
|
||||||
|
IsDead = false;
|
||||||
|
wasDead = false;
|
||||||
|
needsTeleportBack = true;
|
||||||
|
log.Information("[DeathHandler] Preparing to teleport back to death location...");
|
||||||
|
}
|
||||||
|
if (needsTeleportBack && !IsDead && currentHp != 0 && (DateTime.Now - deathTime).TotalSeconds >= (double)config.DeathRespawnDelay)
|
||||||
|
{
|
||||||
|
TeleportBackToDeathLocation();
|
||||||
|
needsTeleportBack = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetTerritoryName(uint territoryId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ExcelSheet<TerritoryType> territorySheet = dataManager.GetExcelSheet<TerritoryType>();
|
||||||
|
if (territorySheet == null)
|
||||||
|
{
|
||||||
|
return territoryId.ToString();
|
||||||
|
}
|
||||||
|
if (!territorySheet.HasRow(territoryId))
|
||||||
|
{
|
||||||
|
return territoryId.ToString();
|
||||||
|
}
|
||||||
|
RowRef<PlaceName> placeNameRef = territorySheet.GetRow(territoryId).PlaceName;
|
||||||
|
if (placeNameRef.RowId == 0)
|
||||||
|
{
|
||||||
|
return territoryId.ToString();
|
||||||
|
}
|
||||||
|
PlaceName? placeName = placeNameRef.ValueNullable;
|
||||||
|
if (!placeName.HasValue)
|
||||||
|
{
|
||||||
|
return territoryId.ToString();
|
||||||
|
}
|
||||||
|
string name = placeName.Value.Name.ExtractText();
|
||||||
|
if (!string.IsNullOrEmpty(name))
|
||||||
|
{
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[DeathHandler] Error getting territory name: " + ex.Message);
|
||||||
|
}
|
||||||
|
return territoryId.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TeleportBackToDeathLocation()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string territoryName = GetTerritoryName(deathTerritoryId);
|
||||||
|
log.Information("[DeathHandler] ========================================");
|
||||||
|
log.Information("[DeathHandler] === TELEPORTING BACK TO DEATH LOCATION ===");
|
||||||
|
log.Information("[DeathHandler] ========================================");
|
||||||
|
log.Information($"[DeathHandler] Target Territory: {territoryName} ({deathTerritoryId})");
|
||||||
|
log.Information($"[DeathHandler] Target Position: {deathPosition}");
|
||||||
|
if (clientState.TerritoryType == deathTerritoryId)
|
||||||
|
{
|
||||||
|
log.Information("[DeathHandler] Already in death territory - using /li to teleport to position");
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string text = $"/li {deathPosition.X:F2}, {deathPosition.Y:F2}, {deathPosition.Z:F2}";
|
||||||
|
commandManager.ProcessCommand(text);
|
||||||
|
log.Information("[DeathHandler] ✓ Teleport to position: " + text);
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
log.Error("[DeathHandler] Failed to send teleport command: " + ex2.Message);
|
||||||
|
}
|
||||||
|
}).Wait();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Information("[DeathHandler] Different territory - teleporting to death territory");
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string text = "/li " + territoryName;
|
||||||
|
commandManager.ProcessCommand(text);
|
||||||
|
log.Information("[DeathHandler] ✓ Teleport to territory: " + text);
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
log.Error("[DeathHandler] Failed to send teleport command: " + ex2.Message);
|
||||||
|
}
|
||||||
|
}).Wait();
|
||||||
|
}
|
||||||
|
deathPosition = Vector3.Zero;
|
||||||
|
deathTerritoryId = 0u;
|
||||||
|
deathTime = DateTime.MinValue;
|
||||||
|
log.Information("[DeathHandler] Death handling complete");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[DeathHandler] Error during teleport: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
IsDead = false;
|
||||||
|
wasDead = false;
|
||||||
|
needsTeleportBack = false;
|
||||||
|
hasClickedYesNo = false;
|
||||||
|
deathPosition = Vector3.Zero;
|
||||||
|
deathTerritoryId = 0u;
|
||||||
|
deathTime = DateTime.MinValue;
|
||||||
|
log.Information("[DeathHandler] State reset");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,378 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dalamud.Game.ClientState.Conditions;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Group;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class DungeonAutomationService : IDisposable
|
||||||
|
{
|
||||||
|
private readonly ICondition condition;
|
||||||
|
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private readonly IClientState clientState;
|
||||||
|
|
||||||
|
private readonly ICommandManager commandManager;
|
||||||
|
|
||||||
|
private readonly IFramework framework;
|
||||||
|
|
||||||
|
private readonly IGameGui gameGui;
|
||||||
|
|
||||||
|
private readonly Configuration config;
|
||||||
|
|
||||||
|
private readonly HelperManager helperManager;
|
||||||
|
|
||||||
|
private readonly MemoryHelper memoryHelper;
|
||||||
|
|
||||||
|
private readonly QuestionableIPC questionableIPC;
|
||||||
|
|
||||||
|
private bool isWaitingForParty;
|
||||||
|
|
||||||
|
private DateTime partyInviteTime = DateTime.MinValue;
|
||||||
|
|
||||||
|
private int inviteAttempts;
|
||||||
|
|
||||||
|
private bool isInvitingHelpers;
|
||||||
|
|
||||||
|
private DateTime helperInviteTime = DateTime.MinValue;
|
||||||
|
|
||||||
|
private bool isInDuty;
|
||||||
|
|
||||||
|
private bool hasStoppedAD;
|
||||||
|
|
||||||
|
private DateTime dutyEntryTime = DateTime.MinValue;
|
||||||
|
|
||||||
|
private bool pendingAutomationStop;
|
||||||
|
|
||||||
|
private DateTime lastDutyExitTime = DateTime.MinValue;
|
||||||
|
|
||||||
|
private DateTime lastDutyEntryTime = DateTime.MinValue;
|
||||||
|
|
||||||
|
private bool expectingDutyEntry;
|
||||||
|
|
||||||
|
private bool isAutomationActive;
|
||||||
|
|
||||||
|
private int originalDutyMode;
|
||||||
|
|
||||||
|
private bool hasSentAtY;
|
||||||
|
|
||||||
|
public bool IsWaitingForParty => isWaitingForParty;
|
||||||
|
|
||||||
|
public int CurrentPartySize { get; private set; } = 1;
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
this.condition = condition;
|
||||||
|
this.log = log;
|
||||||
|
this.clientState = clientState;
|
||||||
|
this.commandManager = commandManager;
|
||||||
|
this.framework = framework;
|
||||||
|
this.gameGui = gameGui;
|
||||||
|
this.config = config;
|
||||||
|
this.helperManager = helperManager;
|
||||||
|
this.memoryHelper = memoryHelper;
|
||||||
|
this.questionableIPC = questionableIPC;
|
||||||
|
condition.ConditionChange += OnConditionChanged;
|
||||||
|
log.Information("[DungeonAutomation] Service initialized with ConditionChange event");
|
||||||
|
log.Information($"[DungeonAutomation] Config - Required Party Size: {config.AutoDutyPartySize}");
|
||||||
|
log.Information($"[DungeonAutomation] Config - Party Wait Time: {config.AutoDutyMaxWaitForParty}s");
|
||||||
|
log.Information($"[DungeonAutomation] Config - Dungeon Automation Enabled: {config.EnableAutoDutyUnsynced}");
|
||||||
|
SetDutyModeBasedOnConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartDungeonAutomation()
|
||||||
|
{
|
||||||
|
if (!isAutomationActive)
|
||||||
|
{
|
||||||
|
log.Information("[DungeonAutomation] ========================================");
|
||||||
|
log.Information("[DungeonAutomation] === STARTING DUNGEON AUTOMATION ===");
|
||||||
|
log.Information("[DungeonAutomation] ========================================");
|
||||||
|
isAutomationActive = true;
|
||||||
|
expectingDutyEntry = true;
|
||||||
|
log.Information("[DungeonAutomation] Inviting helpers via HelperManager...");
|
||||||
|
helperManager.InviteHelpers();
|
||||||
|
isInvitingHelpers = true;
|
||||||
|
helperInviteTime = DateTime.Now;
|
||||||
|
inviteAttempts = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetDutyModeBasedOnConfig()
|
||||||
|
{
|
||||||
|
if (config.EnableAutoDutyUnsynced)
|
||||||
|
{
|
||||||
|
questionableIPC.SetDefaultDutyMode(2);
|
||||||
|
log.Information("[DungeonAutomation] Set Duty Mode to Unsync Party (2) - Automation Enabled");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
questionableIPC.SetDefaultDutyMode(0);
|
||||||
|
log.Information("[DungeonAutomation] Set Duty Mode to Support (0) - Automation Disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopDungeonAutomation()
|
||||||
|
{
|
||||||
|
if (isAutomationActive)
|
||||||
|
{
|
||||||
|
log.Information("[DungeonAutomation] ========================================");
|
||||||
|
log.Information("[DungeonAutomation] === STOPPING DUNGEON AUTOMATION ===");
|
||||||
|
log.Information("[DungeonAutomation] ========================================");
|
||||||
|
isAutomationActive = false;
|
||||||
|
Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateHelperInvite()
|
||||||
|
{
|
||||||
|
double timeSinceInvite = (DateTime.Now - helperInviteTime).TotalSeconds;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (timeSinceInvite >= 2.0)
|
||||||
|
{
|
||||||
|
isInvitingHelpers = false;
|
||||||
|
isWaitingForParty = true;
|
||||||
|
partyInviteTime = DateTime.Now;
|
||||||
|
log.Information("[DungeonAutomation] Helper invites sent, waiting for party...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[DungeonAutomation] Error in helper invite: " + ex.Message);
|
||||||
|
isInvitingHelpers = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update()
|
||||||
|
{
|
||||||
|
if (config.EnableAutoDutyUnsynced && !isAutomationActive)
|
||||||
|
{
|
||||||
|
CheckWaitForPartyTask();
|
||||||
|
}
|
||||||
|
if (!hasStoppedAD && dutyEntryTime != DateTime.MinValue && (DateTime.Now - dutyEntryTime).TotalSeconds >= 1.0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/ad stop");
|
||||||
|
log.Information("[DungeonAutomation] /ad stop (1s after duty entry)");
|
||||||
|
hasStoppedAD = true;
|
||||||
|
dutyEntryTime = DateTime.MinValue;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[DungeonAutomation] Failed to stop AD: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isInvitingHelpers)
|
||||||
|
{
|
||||||
|
UpdateHelperInvite();
|
||||||
|
}
|
||||||
|
else if (pendingAutomationStop && (DateTime.Now - dutyEntryTime).TotalSeconds >= 5.0)
|
||||||
|
{
|
||||||
|
log.Information("[DungeonAutomation] 5s delay complete - stopping automation now");
|
||||||
|
StopDungeonAutomation();
|
||||||
|
pendingAutomationStop = false;
|
||||||
|
}
|
||||||
|
else if (isWaitingForParty)
|
||||||
|
{
|
||||||
|
UpdatePartySize();
|
||||||
|
if (CurrentPartySize >= config.AutoDutyPartySize)
|
||||||
|
{
|
||||||
|
log.Information("[DungeonAutomation] ========================================");
|
||||||
|
log.Information("[DungeonAutomation] === PARTY FULL ===");
|
||||||
|
log.Information("[DungeonAutomation] ========================================");
|
||||||
|
log.Information($"[DungeonAutomation] Party Size: {CurrentPartySize}/{config.AutoDutyPartySize}");
|
||||||
|
isWaitingForParty = false;
|
||||||
|
partyInviteTime = DateTime.MinValue;
|
||||||
|
inviteAttempts = 0;
|
||||||
|
log.Information("[DungeonAutomation] Party full - ready for dungeon!");
|
||||||
|
}
|
||||||
|
else if ((DateTime.Now - partyInviteTime).TotalSeconds >= (double)config.AutoDutyMaxWaitForParty)
|
||||||
|
{
|
||||||
|
log.Warning($"[DungeonAutomation] Party not full after {config.AutoDutyMaxWaitForParty}s - retrying invite (Attempt #{inviteAttempts + 1})");
|
||||||
|
log.Information($"[DungeonAutomation] Current Party Size: {CurrentPartySize}/{config.AutoDutyPartySize}");
|
||||||
|
log.Information("[DungeonAutomation] Retrying helper invites...");
|
||||||
|
helperManager.InviteHelpers();
|
||||||
|
partyInviteTime = DateTime.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckWaitForPartyTask()
|
||||||
|
{
|
||||||
|
if (questionableIPC.GetCurrentTask() is JObject jObject)
|
||||||
|
{
|
||||||
|
JToken taskNameToken = jObject["TaskName"];
|
||||||
|
if (taskNameToken != null && taskNameToken.ToString() == "WaitForParty")
|
||||||
|
{
|
||||||
|
StartDungeonAutomation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void UpdatePartySize()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int partySize = 0;
|
||||||
|
GroupManager* groupManager = GroupManager.Instance();
|
||||||
|
if (groupManager != null)
|
||||||
|
{
|
||||||
|
GroupManager.Group* group = groupManager->GetGroup();
|
||||||
|
if (group != null)
|
||||||
|
{
|
||||||
|
partySize = group->MemberCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (partySize == 0)
|
||||||
|
{
|
||||||
|
partySize = 1;
|
||||||
|
}
|
||||||
|
if (partySize != CurrentPartySize)
|
||||||
|
{
|
||||||
|
CurrentPartySize = partySize;
|
||||||
|
log.Information($"[DungeonAutomation] Party Size updated: {CurrentPartySize}/{config.AutoDutyPartySize}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[DungeonAutomation] Error updating party size: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnConditionChanged(ConditionFlag flag, bool value)
|
||||||
|
{
|
||||||
|
if (flag == ConditionFlag.BoundByDuty)
|
||||||
|
{
|
||||||
|
if (value && !isInDuty)
|
||||||
|
{
|
||||||
|
isInDuty = true;
|
||||||
|
OnDutyEntered();
|
||||||
|
}
|
||||||
|
else if (!value && isInDuty)
|
||||||
|
{
|
||||||
|
isInDuty = false;
|
||||||
|
OnDutyExited();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnDutyEntered()
|
||||||
|
{
|
||||||
|
if ((DateTime.Now - lastDutyEntryTime).TotalSeconds < 5.0)
|
||||||
|
{
|
||||||
|
log.Debug("[DungeonAutomation] OnDutyEntered called too soon - ignoring spam");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastDutyEntryTime = DateTime.Now;
|
||||||
|
log.Information("[DungeonAutomation] Entered duty");
|
||||||
|
if (expectingDutyEntry)
|
||||||
|
{
|
||||||
|
log.Information("[DungeonAutomation] Duty started by DungeonAutomation - enabling automation commands");
|
||||||
|
expectingDutyEntry = false;
|
||||||
|
hasStoppedAD = false;
|
||||||
|
dutyEntryTime = DateTime.Now;
|
||||||
|
if (!hasSentAtY)
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/at y");
|
||||||
|
log.Information("[DungeonAutomation] Sent /at y (duty entered)");
|
||||||
|
hasSentAtY = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Information("[DungeonAutomation] Duty NOT started by DungeonAutomation (Solo Duty/Quest Battle) - skipping automation commands");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnDutyExited()
|
||||||
|
{
|
||||||
|
if ((DateTime.Now - lastDutyExitTime).TotalSeconds < 2.0)
|
||||||
|
{
|
||||||
|
log.Debug("[DungeonAutomation] OnDutyExited called too soon - ignoring spam");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastDutyExitTime = DateTime.Now;
|
||||||
|
log.Information("[DungeonAutomation] Exited duty");
|
||||||
|
if (isAutomationActive)
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/at n");
|
||||||
|
log.Information("[DungeonAutomation] Sent /at n (duty exited)");
|
||||||
|
hasSentAtY = false;
|
||||||
|
log.Information("[DungeonAutomation] Waiting 8s, then disband + restart quest");
|
||||||
|
Task.Run(async delegate
|
||||||
|
{
|
||||||
|
await EnsureSoloPartyAsync();
|
||||||
|
});
|
||||||
|
StopDungeonAutomation();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Information("[DungeonAutomation] Exited non-automated duty - no cleanup needed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureSoloPartyAsync()
|
||||||
|
{
|
||||||
|
TimeSpan timeout = TimeSpan.FromSeconds(60L);
|
||||||
|
DateTime start = DateTime.Now;
|
||||||
|
while (CurrentPartySize > 1 && DateTime.Now - start < timeout)
|
||||||
|
{
|
||||||
|
await framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/leave");
|
||||||
|
});
|
||||||
|
log.Information("[DungeonAutomation] Forced /leave sent, rechecking party size...");
|
||||||
|
await Task.Delay(1500);
|
||||||
|
UpdatePartySize();
|
||||||
|
}
|
||||||
|
if (CurrentPartySize > 1)
|
||||||
|
{
|
||||||
|
log.Warning("[DungeonAutomation] Still not solo after leave spam!");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Information("[DungeonAutomation] Party reduced to solo after duty exit.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DisbandParty()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information("[DungeonAutomation] Disbanding party");
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
memoryHelper.SendChatMessage("/leave");
|
||||||
|
log.Information("[DungeonAutomation] /leave command sent via UIModule");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[DungeonAutomation] Failed to disband party: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
isWaitingForParty = false;
|
||||||
|
partyInviteTime = DateTime.MinValue;
|
||||||
|
inviteAttempts = 0;
|
||||||
|
CurrentPartySize = 1;
|
||||||
|
isInvitingHelpers = false;
|
||||||
|
helperInviteTime = DateTime.MinValue;
|
||||||
|
isAutomationActive = false;
|
||||||
|
log.Information("[DungeonAutomation] State reset");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Reset();
|
||||||
|
condition.ConditionChange -= OnConditionChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,609 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Dalamud.Game.ClientState.Conditions;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class EventQuestExecutionService : IDisposable
|
||||||
|
{
|
||||||
|
private readonly AutoRetainerIPC autoRetainerIpc;
|
||||||
|
|
||||||
|
private readonly QuestionableIPC questionableIPC;
|
||||||
|
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private readonly IFramework framework;
|
||||||
|
|
||||||
|
private readonly ICommandManager commandManager;
|
||||||
|
|
||||||
|
private readonly ICondition condition;
|
||||||
|
|
||||||
|
private readonly Configuration configuration;
|
||||||
|
|
||||||
|
private readonly EventQuestResolver eventQuestResolver;
|
||||||
|
|
||||||
|
private EventQuestState currentState = new EventQuestState();
|
||||||
|
|
||||||
|
private Dictionary<string, List<string>> eventQuestCompletionByCharacter = new Dictionary<string, List<string>>();
|
||||||
|
|
||||||
|
private DateTime lastCheckTime = DateTime.MinValue;
|
||||||
|
|
||||||
|
private const double CheckIntervalMs = 250.0;
|
||||||
|
|
||||||
|
private bool isRotationActive;
|
||||||
|
|
||||||
|
private string? lastTerritoryWaitDetected;
|
||||||
|
|
||||||
|
private DateTime lastTerritoryTeleportTime = DateTime.MinValue;
|
||||||
|
|
||||||
|
private Action? onDataChanged;
|
||||||
|
|
||||||
|
public bool IsRotationActive => isRotationActive;
|
||||||
|
|
||||||
|
public EventQuestExecutionService(AutoRetainerIPC autoRetainerIpc, QuestionableIPC questionableIPC, IPluginLog log, IFramework framework, ICommandManager commandManager, ICondition condition, Configuration configuration, IDataManager dataManager, Action? onDataChanged = null)
|
||||||
|
{
|
||||||
|
this.autoRetainerIpc = autoRetainerIpc;
|
||||||
|
this.questionableIPC = questionableIPC;
|
||||||
|
this.log = log;
|
||||||
|
this.framework = framework;
|
||||||
|
this.commandManager = commandManager;
|
||||||
|
this.condition = condition;
|
||||||
|
this.configuration = configuration;
|
||||||
|
this.onDataChanged = onDataChanged;
|
||||||
|
eventQuestResolver = new EventQuestResolver(dataManager, log);
|
||||||
|
framework.Update += OnFrameworkUpdate;
|
||||||
|
log.Information("[EventQuest] Service initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool StartEventQuestRotation(string eventQuestId, List<string> characters)
|
||||||
|
{
|
||||||
|
if (characters == null || characters.Count == 0)
|
||||||
|
{
|
||||||
|
log.Error("[EventQuest] Cannot start rotation: No characters selected");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (string.IsNullOrEmpty(eventQuestId))
|
||||||
|
{
|
||||||
|
log.Error("[EventQuest] Cannot start rotation: Event Quest ID is empty");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
List<string> dependencies = eventQuestResolver.ResolveEventQuestDependencies(eventQuestId);
|
||||||
|
List<string> remainingChars = new List<string>();
|
||||||
|
List<string> completedChars = new List<string>();
|
||||||
|
foreach (string character in characters)
|
||||||
|
{
|
||||||
|
if (HasCharacterCompletedEventQuest(eventQuestId, character))
|
||||||
|
{
|
||||||
|
completedChars.Add(character);
|
||||||
|
log.Debug("[EventQuest] " + character + " already completed event quest " + eventQuestId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
remainingChars.Add(character);
|
||||||
|
log.Debug("[EventQuest] " + character + " needs to complete event quest " + eventQuestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (remainingChars.Count == 0)
|
||||||
|
{
|
||||||
|
log.Information("[EventQuest] All characters have already completed event quest " + eventQuestId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
string currentLoggedInChar = autoRetainerIpc.GetCurrentCharacter();
|
||||||
|
bool isAlreadyLoggedIn = !string.IsNullOrEmpty(currentLoggedInChar) && remainingChars.Contains(currentLoggedInChar);
|
||||||
|
currentState = new EventQuestState
|
||||||
|
{
|
||||||
|
EventQuestId = eventQuestId,
|
||||||
|
EventQuestName = eventQuestResolver.GetQuestName(eventQuestId),
|
||||||
|
SelectedCharacters = new List<string>(characters),
|
||||||
|
RemainingCharacters = remainingChars,
|
||||||
|
CompletedCharacters = completedChars,
|
||||||
|
DependencyQuests = dependencies,
|
||||||
|
Phase = ((!isAlreadyLoggedIn) ? EventQuestPhase.InitializingFirstCharacter : EventQuestPhase.CheckingQuestCompletion),
|
||||||
|
CurrentCharacter = (isAlreadyLoggedIn ? currentLoggedInChar : ""),
|
||||||
|
PhaseStartTime = DateTime.Now,
|
||||||
|
RotationStartTime = DateTime.Now
|
||||||
|
};
|
||||||
|
isRotationActive = true;
|
||||||
|
log.Information("[EventQuest] ═══ Starting Event Quest Rotation ═══");
|
||||||
|
log.Information($"[EventQuest] Event Quest: {currentState.EventQuestName} ({eventQuestId})");
|
||||||
|
log.Information($"[EventQuest] Total Characters: {characters.Count}");
|
||||||
|
log.Information($"[EventQuest] Remaining: {remainingChars.Count} | Completed: {completedChars.Count}");
|
||||||
|
log.Information($"[EventQuest] Dependencies to resolve: {dependencies.Count}");
|
||||||
|
if (dependencies.Count > 0)
|
||||||
|
{
|
||||||
|
log.Information("[EventQuest] Prerequisites: " + string.Join(", ", dependencies.Select((string id) => eventQuestResolver.GetQuestName(id))));
|
||||||
|
}
|
||||||
|
if (isAlreadyLoggedIn)
|
||||||
|
{
|
||||||
|
log.Information("[EventQuest] User already logged in as " + currentLoggedInChar + " - starting immediately");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EventQuestState GetCurrentState()
|
||||||
|
{
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadEventQuestCompletionData(Dictionary<string, List<string>> data)
|
||||||
|
{
|
||||||
|
if (data != null && data.Count > 0)
|
||||||
|
{
|
||||||
|
eventQuestCompletionByCharacter = new Dictionary<string, List<string>>(data);
|
||||||
|
log.Information($"[EventQuest] Loaded completion data for {data.Count} event quests");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, List<string>> GetEventQuestCompletionData()
|
||||||
|
{
|
||||||
|
return new Dictionary<string, List<string>>(eventQuestCompletionByCharacter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AbortRotation()
|
||||||
|
{
|
||||||
|
log.Information("[EventQuest] Aborting Event Quest rotation");
|
||||||
|
currentState = new EventQuestState
|
||||||
|
{
|
||||||
|
Phase = EventQuestPhase.Idle
|
||||||
|
};
|
||||||
|
isRotationActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MarkEventQuestCompleted(string eventQuestId, string characterName)
|
||||||
|
{
|
||||||
|
if (!eventQuestCompletionByCharacter.ContainsKey(eventQuestId))
|
||||||
|
{
|
||||||
|
eventQuestCompletionByCharacter[eventQuestId] = new List<string>();
|
||||||
|
}
|
||||||
|
if (!eventQuestCompletionByCharacter[eventQuestId].Contains(characterName))
|
||||||
|
{
|
||||||
|
eventQuestCompletionByCharacter[eventQuestId].Add(characterName);
|
||||||
|
log.Debug("[EventQuest] Marked " + characterName + " as completed event quest " + eventQuestId);
|
||||||
|
onDataChanged?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HasCharacterCompletedEventQuest(string eventQuestId, string characterName)
|
||||||
|
{
|
||||||
|
if (eventQuestCompletionByCharacter.TryGetValue(eventQuestId, out List<string> characters))
|
||||||
|
{
|
||||||
|
return characters.Contains(characterName);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFrameworkUpdate(IFramework framework)
|
||||||
|
{
|
||||||
|
if (!isRotationActive)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DateTime now = DateTime.Now;
|
||||||
|
if (!((now - lastCheckTime).TotalMilliseconds < 250.0))
|
||||||
|
{
|
||||||
|
lastCheckTime = now;
|
||||||
|
CheckForTerritoryWait();
|
||||||
|
switch (currentState.Phase)
|
||||||
|
{
|
||||||
|
case EventQuestPhase.InitializingFirstCharacter:
|
||||||
|
HandleInitializingFirstCharacter();
|
||||||
|
break;
|
||||||
|
case EventQuestPhase.WaitingForCharacterLogin:
|
||||||
|
HandleWaitingForCharacterLogin();
|
||||||
|
break;
|
||||||
|
case EventQuestPhase.CheckingQuestCompletion:
|
||||||
|
HandleCheckingQuestCompletion();
|
||||||
|
break;
|
||||||
|
case EventQuestPhase.ResolvingDependencies:
|
||||||
|
HandleResolvingDependencies();
|
||||||
|
break;
|
||||||
|
case EventQuestPhase.ExecutingDependencies:
|
||||||
|
HandleExecutingDependencies();
|
||||||
|
break;
|
||||||
|
case EventQuestPhase.WaitingForQuestStart:
|
||||||
|
case EventQuestPhase.QuestActive:
|
||||||
|
HandleQuestMonitoring();
|
||||||
|
break;
|
||||||
|
case EventQuestPhase.WaitingBeforeCharacterSwitch:
|
||||||
|
HandleWaitingBeforeCharacterSwitch();
|
||||||
|
break;
|
||||||
|
case EventQuestPhase.Completed:
|
||||||
|
HandleCompleted();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckForTerritoryWait()
|
||||||
|
{
|
||||||
|
if (!questionableIPC.IsRunning())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
object task = questionableIPC.GetCurrentTask();
|
||||||
|
if (task == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!(task is JObject jObject))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JToken taskNameToken = jObject["TaskName"];
|
||||||
|
if (taskNameToken == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
string territoryKey = $"{territoryName}_{territoryId}";
|
||||||
|
double timeSinceLastTeleport = (DateTime.Now - lastTerritoryTeleportTime).TotalSeconds;
|
||||||
|
if (lastTerritoryWaitDetected == territoryKey && timeSinceLastTeleport < 60.0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.Information($"[EventQuest] Wait(territory) detected: {territoryName} (ID: {territoryId})");
|
||||||
|
log.Information("[EventQuest] Auto-teleporting via Lifestream...");
|
||||||
|
lastTerritoryWaitDetected = territoryKey;
|
||||||
|
lastTerritoryTeleportTime = DateTime.Now;
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string text = "/li " + territoryName;
|
||||||
|
commandManager.ProcessCommand(text);
|
||||||
|
log.Information("[EventQuest] Sent teleport command: " + text);
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
log.Error("[EventQuest] Failed to teleport to " + territoryName + ": " + ex2.Message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[EventQuest] Error checking Wait(territory) task: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleInitializingFirstCharacter()
|
||||||
|
{
|
||||||
|
if (currentState.RemainingCharacters.Count == 0)
|
||||||
|
{
|
||||||
|
log.Information("[EventQuest] No remaining characters - rotation complete");
|
||||||
|
currentState.Phase = EventQuestPhase.Completed;
|
||||||
|
isRotationActive = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
string firstChar = currentState.RemainingCharacters[0];
|
||||||
|
currentState.CurrentCharacter = firstChar;
|
||||||
|
log.Information("[EventQuest] >>> Initializing first character: " + firstChar);
|
||||||
|
if (autoRetainerIpc.SwitchCharacter(firstChar))
|
||||||
|
{
|
||||||
|
currentState.Phase = EventQuestPhase.WaitingForCharacterLogin;
|
||||||
|
currentState.PhaseStartTime = DateTime.Now;
|
||||||
|
log.Information("[EventQuest] Character switch initiated to " + firstChar);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Error("[EventQuest] Failed to switch to " + firstChar);
|
||||||
|
currentState.Phase = EventQuestPhase.Error;
|
||||||
|
currentState.ErrorMessage = "Failed to switch to " + firstChar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleWaitingForCharacterLogin()
|
||||||
|
{
|
||||||
|
if ((DateTime.Now - currentState.PhaseStartTime).TotalSeconds > 60.0)
|
||||||
|
{
|
||||||
|
log.Error("[EventQuest] Login timeout for " + currentState.CurrentCharacter);
|
||||||
|
SkipToNextCharacter();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
string currentLoggedInChar = autoRetainerIpc.GetCurrentCharacter();
|
||||||
|
if (!string.IsNullOrEmpty(currentLoggedInChar) && currentLoggedInChar == currentState.CurrentCharacter && !((DateTime.Now - currentState.PhaseStartTime).TotalSeconds < 5.0))
|
||||||
|
{
|
||||||
|
log.Information("[EventQuest] Successfully logged in as " + currentLoggedInChar);
|
||||||
|
currentState.Phase = EventQuestPhase.CheckingQuestCompletion;
|
||||||
|
currentState.PhaseStartTime = DateTime.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleCheckingQuestCompletion()
|
||||||
|
{
|
||||||
|
string eventQuestId = currentState.EventQuestId;
|
||||||
|
string rawId = QuestIdParser.ParseQuestId(eventQuestId).rawId;
|
||||||
|
QuestIdType questType = QuestIdParser.ClassifyQuestId(eventQuestId);
|
||||||
|
log.Debug($"[EventQuest] Checking completion for {eventQuestId} (Type: {questType}, RawId: {rawId})");
|
||||||
|
if (!uint.TryParse(rawId, out var questIdUint))
|
||||||
|
{
|
||||||
|
log.Error($"[EventQuest] Invalid quest ID: {eventQuestId} (cannot parse numeric part: {rawId})");
|
||||||
|
SkipToNextCharacter();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bool isQuestComplete = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
isQuestComplete = QuestManager.IsQuestComplete(questIdUint);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[EventQuest] Error checking quest completion: " + ex.Message);
|
||||||
|
}
|
||||||
|
if (isQuestComplete)
|
||||||
|
{
|
||||||
|
log.Information("[EventQuest] " + currentState.CurrentCharacter + " already completed event quest " + eventQuestId);
|
||||||
|
List<string> completedList = currentState.CompletedCharacters;
|
||||||
|
if (!completedList.Contains(currentState.CurrentCharacter))
|
||||||
|
{
|
||||||
|
completedList.Add(currentState.CurrentCharacter);
|
||||||
|
currentState.CompletedCharacters = completedList;
|
||||||
|
}
|
||||||
|
MarkEventQuestCompleted(eventQuestId, currentState.CurrentCharacter);
|
||||||
|
SkipToNextCharacter();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Information("[EventQuest] " + currentState.CurrentCharacter + " needs to complete event quest " + eventQuestId);
|
||||||
|
log.Information($"[EventQuest] >>> Starting event quest with {currentState.DependencyQuests.Count} prerequisites");
|
||||||
|
StartEventQuest();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleResolvingDependencies()
|
||||||
|
{
|
||||||
|
log.Information("[EventQuest] All prerequisites completed - starting event quest");
|
||||||
|
StartEventQuest();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleExecutingDependencies()
|
||||||
|
{
|
||||||
|
string depQuestId = currentState.CurrentExecutingQuest;
|
||||||
|
if (!uint.TryParse(depQuestId, out var questIdUint))
|
||||||
|
{
|
||||||
|
log.Error("[EventQuest] Invalid dependency quest ID: " + depQuestId);
|
||||||
|
currentState.DependencyIndex++;
|
||||||
|
currentState.Phase = EventQuestPhase.ResolvingDependencies;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bool isDependencyComplete = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
isDependencyComplete = QuestManager.IsQuestComplete(questIdUint);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
if (isDependencyComplete)
|
||||||
|
{
|
||||||
|
log.Information("[EventQuest] Dependency " + eventQuestResolver.GetQuestName(depQuestId) + " already completed");
|
||||||
|
currentState.DependencyIndex++;
|
||||||
|
currentState.Phase = EventQuestPhase.ResolvingDependencies;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/qst start");
|
||||||
|
log.Information("[EventQuest] Started dependency quest: " + eventQuestResolver.GetQuestName(depQuestId));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[EventQuest] Failed to start dependency: " + ex.Message);
|
||||||
|
}
|
||||||
|
currentState.Phase = EventQuestPhase.QuestActive;
|
||||||
|
currentState.HasEventQuestBeenAccepted = false;
|
||||||
|
currentState.PhaseStartTime = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleQuestMonitoring()
|
||||||
|
{
|
||||||
|
string eventQuestId = currentState.EventQuestId;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (questionableIPC.IsQuestComplete(eventQuestId))
|
||||||
|
{
|
||||||
|
log.Information("[EventQuest] Event quest " + eventQuestId + " completed by " + currentState.CurrentCharacter);
|
||||||
|
MarkEventQuestCompleted(currentState.EventQuestId, currentState.CurrentCharacter);
|
||||||
|
List<string> completedList = currentState.CompletedCharacters;
|
||||||
|
if (!completedList.Contains(currentState.CurrentCharacter))
|
||||||
|
{
|
||||||
|
completedList.Add(currentState.CurrentCharacter);
|
||||||
|
currentState.CompletedCharacters = completedList;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/qst stop");
|
||||||
|
log.Information("[EventQuest] Sent /qst stop");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
currentState.Phase = EventQuestPhase.WaitingBeforeCharacterSwitch;
|
||||||
|
currentState.PhaseStartTime = DateTime.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[EventQuest] Error checking quest completion via IPC: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleWaitingBeforeCharacterSwitch()
|
||||||
|
{
|
||||||
|
if (!condition[ConditionFlag.BetweenAreas] && (DateTime.Now - currentState.PhaseStartTime).TotalSeconds >= 2.0)
|
||||||
|
{
|
||||||
|
PerformCharacterSwitch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleCompleted()
|
||||||
|
{
|
||||||
|
log.Information("[EventQuest] ═══ EVENT QUEST ROTATION COMPLETED ═══");
|
||||||
|
log.Information($"[EventQuest] All {currentState.CompletedCharacters.Count} characters completed the event quest");
|
||||||
|
if (questionableIPC.IsAvailable)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
questionableIPC.ClearQuestPriority();
|
||||||
|
log.Information("[EventQuest] Cleared quest priority queue after completion");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Warning("[EventQuest] Failed to clear quest priority: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isRotationActive = false;
|
||||||
|
currentState.Phase = EventQuestPhase.Idle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartEventQuest()
|
||||||
|
{
|
||||||
|
List<string> allQuests = new List<string>();
|
||||||
|
if (currentState.DependencyQuests.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (string dep in currentState.DependencyQuests)
|
||||||
|
{
|
||||||
|
allQuests.Add(dep);
|
||||||
|
QuestIdType questType = QuestIdParser.ClassifyQuestId(dep);
|
||||||
|
log.Information($"[EventQuest] Adding dependency: {dep} (Type: {questType})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
string mainQuestId = currentState.EventQuestId;
|
||||||
|
allQuests.Add(mainQuestId);
|
||||||
|
QuestIdType mainQuestType = QuestIdParser.ClassifyQuestId(mainQuestId);
|
||||||
|
log.Information($"[EventQuest] Adding main event quest: {mainQuestId} (Type: {mainQuestType})");
|
||||||
|
log.Information($"[EventQuest] Setting {allQuests.Count} quests as Questionable priority");
|
||||||
|
if (questionableIPC.IsAvailable)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
questionableIPC.ClearQuestPriority();
|
||||||
|
log.Information("[EventQuest] Cleared existing quest priority queue");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Warning("[EventQuest] Failed to clear quest priority: " + ex.Message);
|
||||||
|
}
|
||||||
|
foreach (string questId in allQuests)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool result = questionableIPC.AddQuestPriority(questId);
|
||||||
|
log.Information($"[EventQuest] Added quest {questId} to priority: {result}");
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
log.Warning("[EventQuest] Failed to add quest " + questId + " to priority: " + ex2.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Warning("[EventQuest] Questionable IPC not available - cannot set priority");
|
||||||
|
}
|
||||||
|
if (condition[ConditionFlag.BetweenAreas])
|
||||||
|
{
|
||||||
|
log.Debug("[EventQuest] Character is between areas - waiting before starting quest");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (questionableIPC.IsAvailable && questionableIPC.IsRunning())
|
||||||
|
{
|
||||||
|
log.Debug("[EventQuest] Questionable is busy - waiting before starting quest");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/qst start");
|
||||||
|
log.Information("[EventQuest] Sent /qst start for event quest");
|
||||||
|
currentState.Phase = EventQuestPhase.QuestActive;
|
||||||
|
currentState.CurrentExecutingQuest = currentState.EventQuestId;
|
||||||
|
currentState.HasEventQuestBeenAccepted = false;
|
||||||
|
currentState.PhaseStartTime = DateTime.Now;
|
||||||
|
}
|
||||||
|
catch (Exception ex3)
|
||||||
|
{
|
||||||
|
log.Error("[EventQuest] Failed to start quest: " + ex3.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SkipToNextCharacter()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/qst stop");
|
||||||
|
log.Information("[EventQuest] Sent /qst stop before character switch");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
List<string> remainingList = currentState.RemainingCharacters;
|
||||||
|
List<string> completedList = currentState.CompletedCharacters;
|
||||||
|
if (remainingList.Contains(currentState.CurrentCharacter))
|
||||||
|
{
|
||||||
|
remainingList.Remove(currentState.CurrentCharacter);
|
||||||
|
currentState.RemainingCharacters = remainingList;
|
||||||
|
}
|
||||||
|
if (!completedList.Contains(currentState.CurrentCharacter))
|
||||||
|
{
|
||||||
|
completedList.Add(currentState.CurrentCharacter);
|
||||||
|
currentState.CompletedCharacters = completedList;
|
||||||
|
log.Information("[EventQuest] Character " + currentState.CurrentCharacter + " marked as completed (skipped)");
|
||||||
|
}
|
||||||
|
currentState.Phase = EventQuestPhase.WaitingBeforeCharacterSwitch;
|
||||||
|
currentState.PhaseStartTime = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PerformCharacterSwitch()
|
||||||
|
{
|
||||||
|
List<string> remainingList = currentState.RemainingCharacters;
|
||||||
|
if (remainingList.Contains(currentState.CurrentCharacter))
|
||||||
|
{
|
||||||
|
remainingList.Remove(currentState.CurrentCharacter);
|
||||||
|
currentState.RemainingCharacters = remainingList;
|
||||||
|
}
|
||||||
|
if (currentState.RemainingCharacters.Count == 0)
|
||||||
|
{
|
||||||
|
currentState.Phase = EventQuestPhase.Completed;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
string nextChar = currentState.RemainingCharacters[0];
|
||||||
|
currentState.CurrentCharacter = nextChar;
|
||||||
|
currentState.NextCharacter = nextChar;
|
||||||
|
log.Information("[EventQuest] Switching to next character: " + nextChar);
|
||||||
|
log.Information($"[EventQuest] Progress: {currentState.CompletedCharacters.Count}/{currentState.SelectedCharacters.Count} completed");
|
||||||
|
if (autoRetainerIpc.SwitchCharacter(nextChar))
|
||||||
|
{
|
||||||
|
currentState.Phase = EventQuestPhase.WaitingForCharacterLogin;
|
||||||
|
currentState.PhaseStartTime = DateTime.Now;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Error("[EventQuest] Failed to switch to " + nextChar);
|
||||||
|
currentState.Phase = EventQuestPhase.Error;
|
||||||
|
currentState.ErrorMessage = "Failed to switch character";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
framework.Update -= OnFrameworkUpdate;
|
||||||
|
log.Information("[EventQuest] Service disposed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public enum EventQuestPhase
|
||||||
|
{
|
||||||
|
Idle,
|
||||||
|
InitializingFirstCharacter,
|
||||||
|
WaitingForCharacterLogin,
|
||||||
|
CheckingQuestCompletion,
|
||||||
|
ResolvingDependencies,
|
||||||
|
ExecutingDependencies,
|
||||||
|
WaitingForQuestStart,
|
||||||
|
QuestActive,
|
||||||
|
WaitingBeforeCharacterSwitch,
|
||||||
|
Completed,
|
||||||
|
Error
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,230 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Lumina.Excel;
|
||||||
|
using Lumina.Excel.Sheets;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class EventQuestResolver
|
||||||
|
{
|
||||||
|
private readonly IDataManager dataManager;
|
||||||
|
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
public EventQuestResolver(IDataManager dataManager, IPluginLog log)
|
||||||
|
{
|
||||||
|
this.dataManager = dataManager;
|
||||||
|
this.log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> ResolveEventQuestDependencies(string eventQuestId)
|
||||||
|
{
|
||||||
|
List<string> dependencies = new List<string>();
|
||||||
|
ExcelSheet<Quest> questSheet = dataManager.GetExcelSheet<Quest>();
|
||||||
|
log.Information("[EventQuestResolver] Searching for quest with ID string: '" + eventQuestId + "'");
|
||||||
|
Quest? foundQuest = null;
|
||||||
|
foreach (Quest q in questSheet)
|
||||||
|
{
|
||||||
|
if (q.RowId != 0)
|
||||||
|
{
|
||||||
|
string questIdField = q.Id.ExtractText();
|
||||||
|
if (questIdField == eventQuestId || questIdField.EndsWith("_" + eventQuestId) || questIdField.EndsWith("_" + eventQuestId.PadLeft(5, '0')))
|
||||||
|
{
|
||||||
|
foundQuest = q;
|
||||||
|
log.Information($"[EventQuestResolver] Found quest by ID field: '{questIdField}' (searched for '{eventQuestId}')");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!foundQuest.HasValue || foundQuest.Value.RowId == 0)
|
||||||
|
{
|
||||||
|
log.Error("[EventQuestResolver] Quest with ID '" + eventQuestId + "' not found in Lumina");
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
Quest quest = foundQuest.Value;
|
||||||
|
string questName = quest.Name.ExtractText();
|
||||||
|
log.Information($"[EventQuestResolver] Found quest: RowId={quest.RowId}, Name='{questName}', ID='{quest.Id.ExtractText()}'");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (RowRef<Quest> prevQuestRef in quest.PreviousQuest)
|
||||||
|
{
|
||||||
|
if (prevQuestRef.RowId == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Quest prevQuest = questSheet.GetRow(prevQuestRef.RowId);
|
||||||
|
if (prevQuest.RowId != 0)
|
||||||
|
{
|
||||||
|
string prevQuestName = prevQuest.Name.ExtractText();
|
||||||
|
string prevQuestIdString = prevQuest.Id.ExtractText();
|
||||||
|
string[] idParts = prevQuestIdString.Split('_');
|
||||||
|
string questIdNumber = ((idParts.Length > 1) ? idParts[1].TrimStart('0') : prevQuestIdString);
|
||||||
|
if (string.IsNullOrEmpty(questIdNumber))
|
||||||
|
{
|
||||||
|
questIdNumber = "0";
|
||||||
|
}
|
||||||
|
dependencies.Add(questIdNumber);
|
||||||
|
log.Information($"[EventQuestResolver] Found previous quest: RowId={prevQuestRef.RowId}, Name='{prevQuestName}', ID='{prevQuestIdString}' -> '{questIdNumber}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Warning("[EventQuestResolver] Error reading PreviousQuest: " + ex.Message);
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (RowRef<Quest> questLockRef in quest.QuestLock)
|
||||||
|
{
|
||||||
|
if (questLockRef.RowId == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Quest lockQuest = questSheet.GetRow(questLockRef.RowId);
|
||||||
|
if (lockQuest.RowId != 0)
|
||||||
|
{
|
||||||
|
string lockQuestIdString = lockQuest.Id.ExtractText();
|
||||||
|
string[] idParts2 = lockQuestIdString.Split('_');
|
||||||
|
string questIdNumber2 = ((idParts2.Length > 1) ? idParts2[1].TrimStart('0') : lockQuestIdString);
|
||||||
|
if (string.IsNullOrEmpty(questIdNumber2))
|
||||||
|
{
|
||||||
|
questIdNumber2 = "0";
|
||||||
|
}
|
||||||
|
dependencies.Add(questIdNumber2);
|
||||||
|
log.Information($"[EventQuestResolver] Found quest lock: RowId={questLockRef.RowId}, ID='{lockQuestIdString}' -> '{questIdNumber2}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
log.Warning("[EventQuestResolver] Error reading QuestLock: " + ex2.Message);
|
||||||
|
}
|
||||||
|
dependencies = dependencies.Distinct().ToList();
|
||||||
|
log.Information($"[EventQuestResolver] Found {dependencies.Count} direct prerequisites");
|
||||||
|
if (dependencies.Count > 0)
|
||||||
|
{
|
||||||
|
log.Information("[EventQuestResolver] Event Quest " + eventQuestId + " requires: " + string.Join(", ", dependencies));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Information("[EventQuestResolver] Event Quest " + eventQuestId + " has no prerequisites");
|
||||||
|
}
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsValidQuest(string questId, out string classification)
|
||||||
|
{
|
||||||
|
string rawId = QuestIdParser.ParseQuestId(questId).rawId;
|
||||||
|
if (QuestIdParser.ClassifyQuestId(questId) == QuestIdType.EventQuest)
|
||||||
|
{
|
||||||
|
classification = "EventQuest";
|
||||||
|
log.Debug("[EventQuestResolver] Quest " + questId + " recognized as Event Quest (prefix detected)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!uint.TryParse(rawId, out var questIdUint))
|
||||||
|
{
|
||||||
|
classification = "Invalid";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Quest quest = dataManager.GetExcelSheet<Quest>().GetRow(questIdUint);
|
||||||
|
if (quest.RowId != 0)
|
||||||
|
{
|
||||||
|
classification = "Standard";
|
||||||
|
log.Debug($"[EventQuestResolver] Quest {questId} found in Excel Sheet (RowId: {quest.RowId})");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
classification = "NotFound";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Debug("[EventQuestResolver] Error checking quest availability: " + ex.Message);
|
||||||
|
classification = "Error";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsQuestAvailable(string questId)
|
||||||
|
{
|
||||||
|
string classification;
|
||||||
|
return IsValidQuest(questId, out classification);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetQuestName(string questId)
|
||||||
|
{
|
||||||
|
string rawId = QuestIdParser.ParseQuestId(questId).rawId;
|
||||||
|
QuestIdType questType = QuestIdParser.ClassifyQuestId(questId);
|
||||||
|
if (!uint.TryParse(rawId, out var questIdUint))
|
||||||
|
{
|
||||||
|
if (questType == QuestIdType.EventQuest)
|
||||||
|
{
|
||||||
|
return "Event Quest " + questId;
|
||||||
|
}
|
||||||
|
return "Unknown Quest (" + questId + ")";
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Quest quest = dataManager.GetExcelSheet<Quest>().GetRow(questIdUint);
|
||||||
|
if (quest.RowId != 0)
|
||||||
|
{
|
||||||
|
string name = quest.Name.ExtractText();
|
||||||
|
if (!string.IsNullOrEmpty(name))
|
||||||
|
{
|
||||||
|
if (questType == QuestIdType.EventQuest)
|
||||||
|
{
|
||||||
|
return name + " (" + questId + ")";
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (questType == QuestIdType.EventQuest)
|
||||||
|
{
|
||||||
|
return "Event Quest " + questId;
|
||||||
|
}
|
||||||
|
return "Quest " + questId;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
if (questType == QuestIdType.EventQuest)
|
||||||
|
{
|
||||||
|
return "Event Quest " + questId;
|
||||||
|
}
|
||||||
|
return "Quest " + questId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<(string QuestId, string QuestName)> GetAvailableEventQuests()
|
||||||
|
{
|
||||||
|
List<(string, string)> eventQuests = new List<(string, string)>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ExcelSheet<Quest> questSheet = dataManager.GetExcelSheet<Quest>();
|
||||||
|
if (questSheet == null)
|
||||||
|
{
|
||||||
|
log.Error("[EventQuestResolver] Failed to load Quest sheet");
|
||||||
|
return eventQuests;
|
||||||
|
}
|
||||||
|
foreach (Quest quest in questSheet)
|
||||||
|
{
|
||||||
|
if (quest.RowId != 0 && quest.JournalGenre.RowId == 9)
|
||||||
|
{
|
||||||
|
string questName = quest.Name.ExtractText();
|
||||||
|
if (!string.IsNullOrEmpty(questName))
|
||||||
|
{
|
||||||
|
eventQuests.Add((quest.RowId.ToString(), questName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Information($"[EventQuestResolver] Found {eventQuests.Count} event quests");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[EventQuestResolver] Error getting event quests: " + ex.Message);
|
||||||
|
}
|
||||||
|
return eventQuests.OrderBy(((string, string) q) => q.Item2).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class EventQuestState
|
||||||
|
{
|
||||||
|
public EventQuestPhase Phase { get; set; }
|
||||||
|
|
||||||
|
public string EventQuestId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string EventQuestName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public List<string> SelectedCharacters { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
public List<string> RemainingCharacters { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
public List<string> CompletedCharacters { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
public string CurrentCharacter { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string NextCharacter { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public List<string> DependencyQuests { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
public string CurrentExecutingQuest { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public int DependencyIndex { get; set; }
|
||||||
|
|
||||||
|
public DateTime PhaseStartTime { get; set; } = DateTime.Now;
|
||||||
|
|
||||||
|
public DateTime RotationStartTime { get; set; } = DateTime.Now;
|
||||||
|
|
||||||
|
public string ErrorMessage { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public bool HasEventQuestBeenAccepted { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,655 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dalamud.Game.NativeWrapper;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
|
using QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class ExecutionService : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private readonly Configuration config;
|
||||||
|
|
||||||
|
private readonly QuestionableIPC questionableIPC;
|
||||||
|
|
||||||
|
private readonly AutoRetainerIPC autoRetainerIPC;
|
||||||
|
|
||||||
|
private readonly QuestDetectionService questDetection;
|
||||||
|
|
||||||
|
private readonly IClientState clientState;
|
||||||
|
|
||||||
|
private readonly IGameGui gameGui;
|
||||||
|
|
||||||
|
private readonly IFramework framework;
|
||||||
|
|
||||||
|
private CharacterSafeWaitService? safeWaitService;
|
||||||
|
|
||||||
|
private QuestPreCheckService? preCheckService;
|
||||||
|
|
||||||
|
private DCTravelService? dcTravelService;
|
||||||
|
|
||||||
|
private int currentCharacterIndex;
|
||||||
|
|
||||||
|
private readonly HashSet<string> completedCharacters = new HashSet<string>();
|
||||||
|
|
||||||
|
private readonly HashSet<string> failedCharacters = new HashSet<string>();
|
||||||
|
|
||||||
|
private bool waitingForRelog;
|
||||||
|
|
||||||
|
private string targetRelogCharacter = string.Empty;
|
||||||
|
|
||||||
|
private DateTime relogStartTime = DateTime.MinValue;
|
||||||
|
|
||||||
|
private bool wasLoggedOutDuringRelog;
|
||||||
|
|
||||||
|
public ExecutionState CurrentState { get; private set; } = new ExecutionState();
|
||||||
|
|
||||||
|
public bool IsRunning { get; private set; }
|
||||||
|
|
||||||
|
public bool IsPaused { get; private set; }
|
||||||
|
|
||||||
|
public List<LogEntry> Logs { get; } = new List<LogEntry>();
|
||||||
|
|
||||||
|
public event Action<LogEntry>? LogAdded;
|
||||||
|
|
||||||
|
public event Action<ExecutionState>? StateChanged;
|
||||||
|
|
||||||
|
public ExecutionService(IPluginLog log, Configuration config, QuestionableIPC questionableIPC, AutoRetainerIPC autoRetainerIPC, QuestDetectionService questDetection, IClientState clientState, IGameGui gameGui, IFramework framework)
|
||||||
|
{
|
||||||
|
this.log = log;
|
||||||
|
this.config = config;
|
||||||
|
this.questionableIPC = questionableIPC;
|
||||||
|
this.autoRetainerIPC = autoRetainerIPC;
|
||||||
|
this.questDetection = questDetection;
|
||||||
|
this.clientState = clientState;
|
||||||
|
this.gameGui = gameGui;
|
||||||
|
this.framework = framework;
|
||||||
|
questDetection.QuestAccepted += OnQuestAccepted;
|
||||||
|
questDetection.QuestCompleted += OnQuestCompleted;
|
||||||
|
framework.Update += OnFrameworkUpdate;
|
||||||
|
AddLog(LogLevel.Info, "Execution service initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFrameworkUpdate(IFramework framework)
|
||||||
|
{
|
||||||
|
if (!waitingForRelog)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if ((DateTime.Now - relogStartTime).TotalSeconds > 60.0)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Warning, "Relog timeout - moving to next character");
|
||||||
|
waitingForRelog = false;
|
||||||
|
failedCharacters.Add(targetRelogCharacter);
|
||||||
|
SwitchToNextCharacter();
|
||||||
|
}
|
||||||
|
else if (!clientState.IsLoggedIn)
|
||||||
|
{
|
||||||
|
if (!wasLoggedOutDuringRelog)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Info, "[ExecutionService] Character logged out, waiting for relog...");
|
||||||
|
wasLoggedOutDuringRelog = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!clientState.IsLoggedIn || clientState.LocalPlayer == null || !IsNamePlateReady())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
string currentChar = autoRetainerIPC.GetCurrentCharacter();
|
||||||
|
if (string.IsNullOrEmpty(currentChar) || !(currentChar == targetRelogCharacter))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AddLog(LogLevel.Success, "[ExecutionService] Relog confirmed: " + currentChar);
|
||||||
|
AddLog(LogLevel.Info, "[ExecutionService] NamePlate ready, character fully loaded");
|
||||||
|
if (config.EnableSafeWaitAfterCharacterSwitch && safeWaitService != null)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Info, "[SafeWait] Stabilizing after character switch...");
|
||||||
|
safeWaitService.PerformQuickSafeWait();
|
||||||
|
AddLog(LogLevel.Info, "[SafeWait] Post-switch stabilization complete");
|
||||||
|
}
|
||||||
|
if (config.EnableQuestPreCheck && preCheckService != null)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Info, "[PreCheck] Scanning quest status for current character...");
|
||||||
|
preCheckService.ScanCurrentCharacterQuestStatus();
|
||||||
|
}
|
||||||
|
waitingForRelog = false;
|
||||||
|
CurrentState.CurrentCharacter = currentChar;
|
||||||
|
questDetection.ResetTracking();
|
||||||
|
AddLog(LogLevel.Info, "[ExecutionService] Refreshing quest cache...");
|
||||||
|
questDetection.RefreshQuestCache();
|
||||||
|
NotifyStateChanged();
|
||||||
|
if (dcTravelService != null && dcTravelService.ShouldPerformDCTravel())
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Warning, "[DCTravel] DC Travel required for this character!");
|
||||||
|
AddLog(LogLevel.Info, "[DCTravel] Initiating DC travel before quest execution...");
|
||||||
|
Task.Run(async delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (await dcTravelService.PerformDCTravel())
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Success, "[DCTravel] DC travel completed - starting quests");
|
||||||
|
ExecuteQuestsForCurrentCharacter();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Error, "[DCTravel] DC travel failed - switching character");
|
||||||
|
SwitchToNextCharacter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Error, "[DCTravel] Error: " + ex2.Message);
|
||||||
|
SwitchToNextCharacter();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ExecuteQuestsForCurrentCharacter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[ExecutionService] Error in relog monitoring: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Start()
|
||||||
|
{
|
||||||
|
QuestProfile profile = config.GetActiveProfile();
|
||||||
|
if (profile == null)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Error, "No active profile selected");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (profile.Characters.Count == 0)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Error, "No characters configured in profile");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
IsRunning = true;
|
||||||
|
IsPaused = false;
|
||||||
|
CurrentState.ActiveProfile = profile.Name;
|
||||||
|
CurrentState.Status = ExecutionStatus.Running;
|
||||||
|
AddLog(LogLevel.Success, "Started profile: " + profile.Name);
|
||||||
|
AddLog(LogLevel.Info, $"Characters in rotation: {profile.Characters.Count}");
|
||||||
|
currentCharacterIndex = 0;
|
||||||
|
SwitchToCharacter(profile.Characters[0]);
|
||||||
|
NotifyStateChanged();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
IsRunning = false;
|
||||||
|
IsPaused = false;
|
||||||
|
waitingForRelog = false;
|
||||||
|
if (questionableIPC.IsRunning())
|
||||||
|
{
|
||||||
|
questionableIPC.Stop();
|
||||||
|
}
|
||||||
|
CurrentState.Status = ExecutionStatus.Idle;
|
||||||
|
CurrentState.CurrentQuestId = 0u;
|
||||||
|
CurrentState.CurrentQuestName = string.Empty;
|
||||||
|
CurrentState.CurrentSequence = string.Empty;
|
||||||
|
AddLog(LogLevel.Info, "Execution stopped");
|
||||||
|
NotifyStateChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Pause()
|
||||||
|
{
|
||||||
|
IsPaused = true;
|
||||||
|
questionableIPC.Stop();
|
||||||
|
CurrentState.Status = ExecutionStatus.Waiting;
|
||||||
|
AddLog(LogLevel.Warning, "Execution paused");
|
||||||
|
NotifyStateChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Resume()
|
||||||
|
{
|
||||||
|
IsPaused = false;
|
||||||
|
CurrentState.Status = ExecutionStatus.Running;
|
||||||
|
AddLog(LogLevel.Info, "Execution resumed");
|
||||||
|
NotifyStateChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnQuestAccepted(uint questId, string questName)
|
||||||
|
{
|
||||||
|
if (!IsRunning || IsPaused)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Debug, $"Quest {questId} accepted but execution not running");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QuestProfile profile = config.GetActiveProfile();
|
||||||
|
if (profile == null)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Warning, "No active profile found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QuestConfig questConfig = profile.Quests.FirstOrDefault((QuestConfig q) => q.QuestId == questId && q.TriggerType == TriggerType.OnAccept);
|
||||||
|
if (questConfig != null)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Success, $"Quest accepted trigger matched: {questName} (ID: {questId})");
|
||||||
|
ExecuteSequence(questConfig);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Info, $"Quest {questId} ({questName}) accepted but no OnAccept trigger configured");
|
||||||
|
AddLog(LogLevel.Info, "Add this quest to your profile with TriggerType=OnAccept to auto-execute");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnQuestCompleted(uint questId, string questName)
|
||||||
|
{
|
||||||
|
if (!IsRunning || IsPaused)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QuestProfile profile = config.GetActiveProfile();
|
||||||
|
if (profile != null)
|
||||||
|
{
|
||||||
|
QuestConfig questConfig = profile.Quests.FirstOrDefault((QuestConfig q) => q.QuestId == questId && q.TriggerType == TriggerType.OnComplete);
|
||||||
|
if (questConfig != null)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Success, "Quest completed trigger matched: " + questName);
|
||||||
|
ExecuteSequence(questConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void ExecuteSequence(QuestConfig questConfig)
|
||||||
|
{
|
||||||
|
CurrentState.CurrentQuestId = questConfig.QuestId;
|
||||||
|
CurrentState.CurrentQuestName = questConfig.QuestName;
|
||||||
|
CurrentState.CurrentSequence = questConfig.SequenceAfterQuest.Value;
|
||||||
|
CurrentState.Status = ExecutionStatus.Running;
|
||||||
|
NotifyStateChanged();
|
||||||
|
if (config.EnableDryRun)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Debug, "[DRY RUN] Would execute sequence: " + questConfig.SequenceAfterQuest.Value);
|
||||||
|
await Task.Delay(2000);
|
||||||
|
OnSequenceComplete(questConfig);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (questConfig.SequenceAfterQuest.Type)
|
||||||
|
{
|
||||||
|
case SequenceType.QuestionableProfile:
|
||||||
|
await ExecuteQuestionableProfile(questConfig);
|
||||||
|
break;
|
||||||
|
case SequenceType.InternalAction:
|
||||||
|
await ExecuteInternalAction(questConfig);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteQuestionableProfile(QuestConfig questConfig)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Info, $"Monitoring Quest {questConfig.QuestId} for completion...");
|
||||||
|
AddLog(LogLevel.Info, "Questionable will handle quest progression automatically");
|
||||||
|
CurrentState.CurrentSequence = $"Monitoring Quest {questConfig.QuestId}";
|
||||||
|
NotifyStateChanged();
|
||||||
|
await WaitForQuestCompletion(questConfig.QuestId);
|
||||||
|
OnSequenceComplete(questConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WaitForQuestAcceptanceThenNext(QuestConfig questConfig)
|
||||||
|
{
|
||||||
|
int maxWaitSeconds = 3600;
|
||||||
|
int waited = 0;
|
||||||
|
AddLog(LogLevel.Info, $"Waiting for quest {questConfig.QuestId} to be accepted...");
|
||||||
|
while (waited < maxWaitSeconds && IsRunning)
|
||||||
|
{
|
||||||
|
await Task.Delay(5000);
|
||||||
|
waited += 5;
|
||||||
|
if (questDetection.IsQuestAccepted(questConfig.QuestId))
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Success, $"Quest {questConfig.QuestId} accepted!");
|
||||||
|
OnSequenceComplete(questConfig);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (questDetection.IsQuestCompletedDirect(questConfig.QuestId))
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Success, $"Quest {questConfig.QuestId} already completed!");
|
||||||
|
OnSequenceComplete(questConfig);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (waited % 60 == 0)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Debug, $"Still waiting for quest {questConfig.QuestId} acceptance... ({waited}s)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!IsRunning)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Info, "Monitoring stopped - execution not running");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AddLog(LogLevel.Warning, $"Quest {questConfig.QuestId} acceptance timeout after {maxWaitSeconds}s");
|
||||||
|
OnSequenceFailed(questConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WaitForQuestCompletion(uint questId)
|
||||||
|
{
|
||||||
|
int maxWaitSeconds = 600;
|
||||||
|
int waited = 0;
|
||||||
|
while (waited < maxWaitSeconds && IsRunning)
|
||||||
|
{
|
||||||
|
await Task.Delay(5000);
|
||||||
|
waited += 5;
|
||||||
|
if (questDetection.IsQuestCompletedDirect(questId))
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Success, $"Quest {questId} completed!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (waited % 30 == 0)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Debug, $"Still waiting for quest {questId}... ({waited}s)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!IsRunning)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Info, "Monitoring stopped - execution not running");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AddLog(LogLevel.Warning, $"Quest {questId} completion timeout after {maxWaitSeconds}s");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WaitForQuestionableCompletion()
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Warning, "Waiting for Questionable to complete...");
|
||||||
|
while (questionableIPC.IsRunning())
|
||||||
|
{
|
||||||
|
await Task.Delay(1000);
|
||||||
|
}
|
||||||
|
AddLog(LogLevel.Success, "Questionable completed");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteInternalAction(QuestConfig questConfig)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Warning, "Internal action not yet implemented: " + questConfig.SequenceAfterQuest.Value);
|
||||||
|
await Task.Delay(1000);
|
||||||
|
OnSequenceComplete(questConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSequenceComplete(QuestConfig questConfig)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Success, "Sequence completed: " + questConfig.SequenceAfterQuest.Value);
|
||||||
|
CurrentState.Status = ExecutionStatus.Complete;
|
||||||
|
CurrentState.Progress = 100;
|
||||||
|
NotifyStateChanged();
|
||||||
|
if (questConfig.NextCharacter == "auto_next")
|
||||||
|
{
|
||||||
|
SwitchToNextCharacter();
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(questConfig.NextCharacter))
|
||||||
|
{
|
||||||
|
SwitchToCharacter(questConfig.NextCharacter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSequenceFailed(QuestConfig questConfig)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Error, "Sequence failed: " + questConfig.SequenceAfterQuest.Value);
|
||||||
|
CurrentState.Status = ExecutionStatus.Failed;
|
||||||
|
NotifyStateChanged();
|
||||||
|
if (!string.IsNullOrEmpty(CurrentState.CurrentCharacter))
|
||||||
|
{
|
||||||
|
failedCharacters.Add(CurrentState.CurrentCharacter);
|
||||||
|
}
|
||||||
|
SwitchToNextCharacter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SwitchToCharacter(string characterName)
|
||||||
|
{
|
||||||
|
if (config.EnableDryRun)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Debug, "[DRY RUN] Would switch to character: " + characterName);
|
||||||
|
CurrentState.CurrentCharacter = characterName;
|
||||||
|
NotifyStateChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AddLog(LogLevel.Info, "Switching to character: " + characterName);
|
||||||
|
if (questionableIPC.IsRunning())
|
||||||
|
{
|
||||||
|
questionableIPC.Stop();
|
||||||
|
}
|
||||||
|
string originalChar = autoRetainerIPC.GetCurrentCharacter();
|
||||||
|
AddLog(LogLevel.Debug, "Current character before switch: " + (originalChar ?? "null"));
|
||||||
|
if (!string.IsNullOrEmpty(originalChar) && originalChar == characterName)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Info, "Already on character: " + characterName);
|
||||||
|
AddLog(LogLevel.Info, "Refreshing quest cache...");
|
||||||
|
questDetection.RefreshQuestCache();
|
||||||
|
CurrentState.CurrentCharacter = characterName;
|
||||||
|
NotifyStateChanged();
|
||||||
|
await Task.Delay(2000);
|
||||||
|
ExecuteQuestsForCurrentCharacter();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (string.IsNullOrEmpty(originalChar))
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Warning, "Could not get current character - might be in transition");
|
||||||
|
}
|
||||||
|
AddLog(LogLevel.Info, "[AutoRetainerIPC] Relog request sent for: " + characterName);
|
||||||
|
if (autoRetainerIPC.SwitchCharacter(characterName))
|
||||||
|
{
|
||||||
|
waitingForRelog = true;
|
||||||
|
targetRelogCharacter = characterName;
|
||||||
|
relogStartTime = DateTime.Now;
|
||||||
|
wasLoggedOutDuringRelog = false;
|
||||||
|
AddLog(LogLevel.Info, "Relog command sent, monitoring via Framework.Update...");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Error, "Failed to send relog request for character: " + characterName);
|
||||||
|
failedCharacters.Add(characterName);
|
||||||
|
SwitchToNextCharacter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExecuteQuestsForCurrentCharacter()
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Info, "=== ExecuteQuestsForCurrentCharacter called ===");
|
||||||
|
QuestProfile profile = config.GetActiveProfile();
|
||||||
|
if (profile == null)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Error, "No active profile");
|
||||||
|
SwitchToNextCharacter();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
string currentChar = CurrentState.CurrentCharacter;
|
||||||
|
if (string.IsNullOrEmpty(currentChar))
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Warning, "No current character set");
|
||||||
|
SwitchToNextCharacter();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AddLog(LogLevel.Info, "Current character: " + currentChar);
|
||||||
|
AddLog(LogLevel.Info, $"Total quests in profile: {profile.Quests.Count}");
|
||||||
|
List<QuestConfig> characterQuests = (from q in profile.Quests
|
||||||
|
where string.IsNullOrEmpty(q.AssignedCharacter) || q.AssignedCharacter == currentChar
|
||||||
|
orderby q.QuestId
|
||||||
|
select q).ToList();
|
||||||
|
if (characterQuests.Count == 0)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Warning, "No quests configured for " + currentChar);
|
||||||
|
AddLog(LogLevel.Info, "Please add quests to your profile using /qstcomp");
|
||||||
|
completedCharacters.Add(currentChar);
|
||||||
|
SwitchToNextCharacter();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AddLog(LogLevel.Info, $"Found {characterQuests.Count} quests for {currentChar}");
|
||||||
|
foreach (QuestConfig quest in characterQuests)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Debug, $"Checking quest {quest.QuestId}: {quest.QuestName}");
|
||||||
|
bool isCompleted = questDetection.IsQuestCompletedDirect(quest.QuestId);
|
||||||
|
AddLog(LogLevel.Debug, $"Quest {quest.QuestId} completed status: {isCompleted}");
|
||||||
|
if (!isCompleted)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Success, $"Starting quest {quest.QuestId}: {quest.QuestName}");
|
||||||
|
CurrentState.CurrentQuestId = quest.QuestId;
|
||||||
|
CurrentState.CurrentQuestName = quest.QuestName;
|
||||||
|
CurrentState.CurrentSequence = $"Quest {quest.QuestId}";
|
||||||
|
NotifyStateChanged();
|
||||||
|
if (quest.TriggerType == TriggerType.OnAccept)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Info, $"Quest {quest.QuestId} has OnAccept trigger - waiting for acceptance");
|
||||||
|
WaitForQuestAcceptanceThenNext(quest);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Info, $"Quest {quest.QuestId} has OnComplete trigger - monitoring for completion");
|
||||||
|
ExecuteSequence(quest);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AddLog(LogLevel.Info, $"Quest {quest.QuestId} already completed, skipping");
|
||||||
|
}
|
||||||
|
AddLog(LogLevel.Success, "All quests completed for " + currentChar + "!");
|
||||||
|
completedCharacters.Add(currentChar);
|
||||||
|
SwitchToNextCharacter();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetSafeWaitService(CharacterSafeWaitService service)
|
||||||
|
{
|
||||||
|
safeWaitService = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetPreCheckService(QuestPreCheckService service)
|
||||||
|
{
|
||||||
|
preCheckService = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetDCTravelService(DCTravelService service)
|
||||||
|
{
|
||||||
|
dcTravelService = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SwitchToNextCharacter()
|
||||||
|
{
|
||||||
|
QuestProfile profile = config.GetActiveProfile();
|
||||||
|
if (profile == null || profile.Characters.Count == 0)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Error, "No profile or characters available");
|
||||||
|
Stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (config.EnableQuestPreCheck && preCheckService != null)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Info, "Logging completed quests before logout...");
|
||||||
|
preCheckService.LogCompletedQuestsBeforeLogout();
|
||||||
|
}
|
||||||
|
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.EnableSafeWaitBeforeCharacterSwitch && safeWaitService != null)
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Info, "[SafeWait] Stabilizing character before switch...");
|
||||||
|
safeWaitService.PerformSafeWait();
|
||||||
|
AddLog(LogLevel.Info, "[SafeWait] Stabilization complete");
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrEmpty(CurrentState.CurrentCharacter))
|
||||||
|
{
|
||||||
|
completedCharacters.Add(CurrentState.CurrentCharacter);
|
||||||
|
}
|
||||||
|
int attempts = 0;
|
||||||
|
while (attempts < profile.Characters.Count)
|
||||||
|
{
|
||||||
|
currentCharacterIndex = (currentCharacterIndex + 1) % profile.Characters.Count;
|
||||||
|
string nextChar = profile.Characters[currentCharacterIndex];
|
||||||
|
if (config.EnableQuestPreCheck && preCheckService != null)
|
||||||
|
{
|
||||||
|
List<StopPoint> stopPoints = config.StopPoints;
|
||||||
|
if (stopPoints != null && stopPoints.Count > 0)
|
||||||
|
{
|
||||||
|
uint firstStopQuest = stopPoints[0].QuestId;
|
||||||
|
if (preCheckService.ShouldSkipCharacter(nextChar, firstStopQuest))
|
||||||
|
{
|
||||||
|
AddLog(LogLevel.Info, "[PreCheck] Skipping " + nextChar + " - quest already completed");
|
||||||
|
completedCharacters.Add(nextChar);
|
||||||
|
attempts++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!completedCharacters.Contains(nextChar) && !failedCharacters.Contains(nextChar))
|
||||||
|
{
|
||||||
|
SwitchToCharacter(nextChar);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
AddLog(LogLevel.Success, "All characters completed!");
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddLog(LogLevel level, string message)
|
||||||
|
{
|
||||||
|
LogEntry entry = new LogEntry
|
||||||
|
{
|
||||||
|
Level = level,
|
||||||
|
Message = message,
|
||||||
|
Timestamp = DateTime.Now
|
||||||
|
};
|
||||||
|
Logs.Add(entry);
|
||||||
|
while (Logs.Count > config.MaxLogEntries)
|
||||||
|
{
|
||||||
|
Logs.RemoveAt(0);
|
||||||
|
}
|
||||||
|
log.Information("[ExecutionService] " + message);
|
||||||
|
this.LogAdded?.Invoke(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NotifyStateChanged()
|
||||||
|
{
|
||||||
|
CurrentState.LastUpdate = DateTime.Now;
|
||||||
|
this.StateChanged?.Invoke(CurrentState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe bool IsNamePlateReady()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AtkUnitBasePtr namePlatePtr = gameGui.GetAddonByName("NamePlate");
|
||||||
|
if (namePlatePtr == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
AddonNamePlate* namePlate = (AddonNamePlate*)namePlatePtr.Address;
|
||||||
|
if (namePlate != null && namePlate->AtkUnitBase.IsVisible && namePlate->AtkUnitBase.IsReady)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
questDetection.QuestAccepted -= OnQuestAccepted;
|
||||||
|
questDetection.QuestCompleted -= OnQuestCompleted;
|
||||||
|
framework.Update -= OnFrameworkUpdate;
|
||||||
|
Stop();
|
||||||
|
AddLog(LogLevel.Info, "Execution service disposed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class ExpansionInfo
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
|
||||||
|
public string ShortName { get; set; } = "";
|
||||||
|
|
||||||
|
public uint MinQuestId { get; set; }
|
||||||
|
|
||||||
|
public uint MaxQuestId { get; set; }
|
||||||
|
|
||||||
|
public int ExpectedQuestCount { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class ExpansionProgressInfo
|
||||||
|
{
|
||||||
|
public string ExpansionName { get; set; } = "";
|
||||||
|
|
||||||
|
public string ShortName { get; set; } = "";
|
||||||
|
|
||||||
|
public int TotalQuests { get; set; }
|
||||||
|
|
||||||
|
public int CompletedQuests { get; set; }
|
||||||
|
|
||||||
|
public float Percentage { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,444 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dalamud.Game.ClientState.Conditions;
|
||||||
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Group;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class HelperManager : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Configuration configuration;
|
||||||
|
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private readonly ICommandManager commandManager;
|
||||||
|
|
||||||
|
private readonly ICondition condition;
|
||||||
|
|
||||||
|
private readonly IClientState clientState;
|
||||||
|
|
||||||
|
private readonly IFramework framework;
|
||||||
|
|
||||||
|
private readonly PartyInviteService partyInviteService;
|
||||||
|
|
||||||
|
private readonly MultiClientIPC multiClientIPC;
|
||||||
|
|
||||||
|
private readonly CrossProcessIPC crossProcessIPC;
|
||||||
|
|
||||||
|
private readonly PartyInviteAutoAccept partyInviteAutoAccept;
|
||||||
|
|
||||||
|
private readonly MemoryHelper memoryHelper;
|
||||||
|
|
||||||
|
private bool isInDuty;
|
||||||
|
|
||||||
|
private List<(string Name, ushort WorldId)> availableHelpers = new List<(string, ushort)>();
|
||||||
|
|
||||||
|
private Dictionary<(string, ushort), bool> helperReadyStatus = new Dictionary<(string, ushort), bool>();
|
||||||
|
|
||||||
|
public HelperManager(Configuration configuration, IPluginLog log, ICommandManager commandManager, ICondition condition, IClientState clientState, IFramework framework, PartyInviteService partyInviteService, MultiClientIPC multiClientIPC, CrossProcessIPC crossProcessIPC, PartyInviteAutoAccept partyInviteAutoAccept, MemoryHelper memoryHelper)
|
||||||
|
{
|
||||||
|
this.configuration = configuration;
|
||||||
|
this.log = log;
|
||||||
|
this.commandManager = commandManager;
|
||||||
|
this.condition = condition;
|
||||||
|
this.clientState = clientState;
|
||||||
|
this.framework = framework;
|
||||||
|
this.partyInviteService = partyInviteService;
|
||||||
|
this.multiClientIPC = multiClientIPC;
|
||||||
|
this.crossProcessIPC = crossProcessIPC;
|
||||||
|
this.memoryHelper = memoryHelper;
|
||||||
|
this.partyInviteAutoAccept = partyInviteAutoAccept;
|
||||||
|
condition.ConditionChange += OnConditionChanged;
|
||||||
|
multiClientIPC.OnHelperRequested += OnHelperRequested;
|
||||||
|
multiClientIPC.OnHelperDismissed += OnHelperDismissed;
|
||||||
|
multiClientIPC.OnHelperAvailable += OnHelperAvailable;
|
||||||
|
crossProcessIPC.OnHelperRequested += OnHelperRequested;
|
||||||
|
crossProcessIPC.OnHelperDismissed += OnHelperDismissed;
|
||||||
|
crossProcessIPC.OnHelperAvailable += OnHelperAvailable;
|
||||||
|
crossProcessIPC.OnHelperReady += OnHelperReady;
|
||||||
|
crossProcessIPC.OnHelperInParty += OnHelperInParty;
|
||||||
|
crossProcessIPC.OnHelperInDuty += OnHelperInDuty;
|
||||||
|
crossProcessIPC.OnRequestHelperAnnouncements += OnRequestHelperAnnouncements;
|
||||||
|
if (configuration.IsHighLevelHelper)
|
||||||
|
{
|
||||||
|
log.Information("[HelperManager] Will announce helper availability on next frame");
|
||||||
|
}
|
||||||
|
log.Information("[HelperManager] Initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AnnounceIfHelper()
|
||||||
|
{
|
||||||
|
if (configuration.IsHighLevelHelper)
|
||||||
|
{
|
||||||
|
IPlayerCharacter localPlayer = clientState.LocalPlayer;
|
||||||
|
if (localPlayer == null)
|
||||||
|
{
|
||||||
|
log.Warning("[HelperManager] LocalPlayer is null, cannot announce helper");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
string localName = localPlayer.Name.ToString();
|
||||||
|
ushort localWorldId = (ushort)localPlayer.HomeWorld.RowId;
|
||||||
|
multiClientIPC.AnnounceHelperAvailable(localName, localWorldId);
|
||||||
|
crossProcessIPC.AnnounceHelper();
|
||||||
|
log.Information($"[HelperManager] Announced as helper: {localName}@{localWorldId} (both IPC systems)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InviteHelpers()
|
||||||
|
{
|
||||||
|
if (!configuration.IsQuester)
|
||||||
|
{
|
||||||
|
log.Debug("[HelperManager] Not a Quester, skipping helper invites");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.Information("[HelperManager] Requesting helper announcements...");
|
||||||
|
RequestHelperAnnouncements();
|
||||||
|
Task.Run(async delegate
|
||||||
|
{
|
||||||
|
await Task.Delay(1000);
|
||||||
|
if (availableHelpers.Count == 0)
|
||||||
|
{
|
||||||
|
log.Warning("[HelperManager] No helpers available via IPC!");
|
||||||
|
log.Warning("[HelperManager] Make sure helper clients are running with 'I'm a High-Level Helper' enabled");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Information($"[HelperManager] Inviting {availableHelpers.Count} AUTO-DISCOVERED helper(s)...");
|
||||||
|
DisbandParty();
|
||||||
|
await Task.Delay(500);
|
||||||
|
foreach (var (name, worldId) in availableHelpers)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(name) || worldId == 0)
|
||||||
|
{
|
||||||
|
log.Warning($"[HelperManager] Invalid helper: {name}@{worldId}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Information($"[HelperManager] Requesting helper: {name}@{worldId}");
|
||||||
|
helperReadyStatus[(name, worldId)] = false;
|
||||||
|
multiClientIPC.RequestHelper(name, worldId);
|
||||||
|
crossProcessIPC.RequestHelper(name, worldId);
|
||||||
|
log.Information("[HelperManager] Waiting for " + name + " to be ready...");
|
||||||
|
DateTime timeout = DateTime.Now.AddSeconds(10.0);
|
||||||
|
while (!helperReadyStatus.GetValueOrDefault((name, worldId), defaultValue: false) && DateTime.Now < timeout)
|
||||||
|
{
|
||||||
|
await Task.Delay(100);
|
||||||
|
}
|
||||||
|
if (!helperReadyStatus.GetValueOrDefault((name, worldId), defaultValue: false))
|
||||||
|
{
|
||||||
|
log.Warning("[HelperManager] Timeout waiting for " + name + " to be ready!");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Information("[HelperManager] " + name + " is ready! Sending invite...");
|
||||||
|
if (partyInviteService.InviteToParty(name, worldId))
|
||||||
|
{
|
||||||
|
log.Information("[HelperManager] Successfully invited " + name);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Error("[HelperManager] Failed to invite " + name);
|
||||||
|
}
|
||||||
|
await Task.Delay(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<(string Name, ushort WorldId)> GetAvailableHelpers()
|
||||||
|
{
|
||||||
|
return new List<(string, ushort)>(availableHelpers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LeaveParty()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information("[HelperManager] Leaving party");
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
memoryHelper.SendChatMessage("/leave");
|
||||||
|
log.Information("[HelperManager] /leave command sent via UIModule");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[HelperManager] Failed to leave party: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DisbandParty()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information("[HelperManager] Disbanding party");
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
memoryHelper.SendChatMessage("/leave");
|
||||||
|
log.Information("[HelperManager] /leave command sent via UIModule");
|
||||||
|
});
|
||||||
|
multiClientIPC.DismissHelper();
|
||||||
|
crossProcessIPC.DismissHelper();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[HelperManager] Failed to disband party: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnConditionChanged(ConditionFlag flag, bool value)
|
||||||
|
{
|
||||||
|
if (flag == ConditionFlag.BoundByDuty)
|
||||||
|
{
|
||||||
|
if (value && !isInDuty)
|
||||||
|
{
|
||||||
|
isInDuty = true;
|
||||||
|
OnDutyEnter();
|
||||||
|
}
|
||||||
|
else if (!value && isInDuty)
|
||||||
|
{
|
||||||
|
isInDuty = false;
|
||||||
|
OnDutyLeave();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDutyEnter()
|
||||||
|
{
|
||||||
|
log.Information("[HelperManager] Entered duty");
|
||||||
|
if (!configuration.IsHighLevelHelper)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
configuration.CurrentHelperStatus = HelperStatus.InDungeon;
|
||||||
|
configuration.Save();
|
||||||
|
log.Information("[HelperManager] Helper status: InDungeon");
|
||||||
|
IPlayerCharacter localPlayer = clientState.LocalPlayer;
|
||||||
|
if (localPlayer != null)
|
||||||
|
{
|
||||||
|
string helperName = localPlayer.Name.ToString();
|
||||||
|
ushort helperWorld = (ushort)localPlayer.HomeWorld.RowId;
|
||||||
|
crossProcessIPC.BroadcastHelperStatus(helperName, helperWorld, "InDungeon");
|
||||||
|
}
|
||||||
|
log.Information("[HelperManager] Starting AutoDuty (High-Level Helper)");
|
||||||
|
Task.Run(async delegate
|
||||||
|
{
|
||||||
|
log.Information("[HelperManager] Waiting 5s before starting AutoDuty...");
|
||||||
|
await Task.Delay(5000);
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/ad start");
|
||||||
|
log.Information("[HelperManager] AutoDuty started");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[HelperManager] Failed to start AutoDuty: " + ex.Message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void OnDutyLeave()
|
||||||
|
{
|
||||||
|
log.Information("[HelperManager] Left duty");
|
||||||
|
if (configuration.IsHighLevelHelper)
|
||||||
|
{
|
||||||
|
if (configuration.CurrentHelperStatus == HelperStatus.InDungeon)
|
||||||
|
{
|
||||||
|
configuration.CurrentHelperStatus = HelperStatus.Available;
|
||||||
|
configuration.Save();
|
||||||
|
log.Information("[HelperManager] Helper status: Available");
|
||||||
|
IPlayerCharacter localPlayer = clientState.LocalPlayer;
|
||||||
|
if (localPlayer != null)
|
||||||
|
{
|
||||||
|
string helperName = localPlayer.Name.ToString();
|
||||||
|
ushort helperWorld = (ushort)localPlayer.HomeWorld.RowId;
|
||||||
|
crossProcessIPC.BroadcastHelperStatus(helperName, helperWorld, "Available");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Information("[HelperManager] Stopping AutoDuty (High-Level Helper)");
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/ad stop");
|
||||||
|
log.Information("[HelperManager] AutoDuty stopped");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[HelperManager] Failed to stop AutoDuty: " + ex.Message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log.Information("[HelperManager] Leaving party after duty (High-Level Helper)");
|
||||||
|
Task.Run(async delegate
|
||||||
|
{
|
||||||
|
log.Information("[HelperManager] Waiting 4 seconds for duty to fully complete...");
|
||||||
|
await Task.Delay(4000);
|
||||||
|
for (int attempt = 1; attempt <= 3; attempt++)
|
||||||
|
{
|
||||||
|
bool inParty = false;
|
||||||
|
GroupManager* groupManager = GroupManager.Instance();
|
||||||
|
if (groupManager != null)
|
||||||
|
{
|
||||||
|
GroupManager.Group* group = groupManager->GetGroup();
|
||||||
|
if (group != null && group->MemberCount > 1)
|
||||||
|
{
|
||||||
|
inParty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!inParty)
|
||||||
|
{
|
||||||
|
log.Information("[HelperManager] Successfully left party or already solo");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
log.Information($"[HelperManager] Attempt {attempt}/3: Still in party - sending /leave command");
|
||||||
|
LeaveParty();
|
||||||
|
if (attempt < 3)
|
||||||
|
{
|
||||||
|
await Task.Delay(2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (configuration.IsQuester)
|
||||||
|
{
|
||||||
|
log.Information("[HelperManager] Disbanding party after duty (Quester)");
|
||||||
|
DisbandParty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void OnHelperRequested(string characterName, ushort worldId)
|
||||||
|
{
|
||||||
|
if (!configuration.IsHighLevelHelper)
|
||||||
|
{
|
||||||
|
log.Debug("[HelperManager] Not a High-Level Helper, ignoring request");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IPlayerCharacter localPlayer = clientState.LocalPlayer;
|
||||||
|
if (localPlayer == null)
|
||||||
|
{
|
||||||
|
log.Warning("[HelperManager] Local player is null!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
string localName = localPlayer.Name.ToString();
|
||||||
|
ushort localWorldId = (ushort)localPlayer.HomeWorld.RowId;
|
||||||
|
if (!(localName == characterName) || localWorldId != worldId)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.Information("[HelperManager] Helper request is for me! Checking status...");
|
||||||
|
Task.Run(async delegate
|
||||||
|
{
|
||||||
|
bool needsToLeaveParty = false;
|
||||||
|
bool isInDuty = false;
|
||||||
|
GroupManager* groupManager = GroupManager.Instance();
|
||||||
|
if (groupManager != null)
|
||||||
|
{
|
||||||
|
GroupManager.Group* group = groupManager->GetGroup();
|
||||||
|
if (group != null && group->MemberCount > 0)
|
||||||
|
{
|
||||||
|
needsToLeaveParty = true;
|
||||||
|
log.Information("[HelperManager] Currently in party, notifying quester...");
|
||||||
|
crossProcessIPC.NotifyHelperInParty(localName, localWorldId);
|
||||||
|
if (condition[ConditionFlag.BoundByDuty])
|
||||||
|
{
|
||||||
|
isInDuty = true;
|
||||||
|
log.Information("[HelperManager] Currently in duty, notifying quester...");
|
||||||
|
crossProcessIPC.NotifyHelperInDuty(localName, localWorldId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isInDuty)
|
||||||
|
{
|
||||||
|
if (needsToLeaveParty)
|
||||||
|
{
|
||||||
|
LeaveParty();
|
||||||
|
await Task.Delay(1000);
|
||||||
|
}
|
||||||
|
log.Information("[HelperManager] Ready to accept invite!");
|
||||||
|
partyInviteAutoAccept.EnableAutoAccept();
|
||||||
|
crossProcessIPC.NotifyHelperReady(localName, localWorldId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnHelperDismissed()
|
||||||
|
{
|
||||||
|
if (configuration.IsHighLevelHelper)
|
||||||
|
{
|
||||||
|
log.Information("[HelperManager] Received dismiss signal, leaving party...");
|
||||||
|
DisbandParty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnHelperAvailable(string characterName, ushort worldId)
|
||||||
|
{
|
||||||
|
if (configuration.IsQuester && !availableHelpers.Any<(string, ushort)>(((string Name, ushort WorldId) h) => h.Name == characterName && h.WorldId == worldId))
|
||||||
|
{
|
||||||
|
availableHelpers.Add((characterName, worldId));
|
||||||
|
log.Information($"[HelperManager] Helper discovered: {characterName}@{worldId} (Total: {availableHelpers.Count})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnHelperReady(string characterName, ushort worldId)
|
||||||
|
{
|
||||||
|
if (configuration.IsQuester)
|
||||||
|
{
|
||||||
|
log.Information($"[HelperManager] Helper {characterName}@{worldId} is ready!");
|
||||||
|
helperReadyStatus[(characterName, worldId)] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnHelperInParty(string characterName, ushort worldId)
|
||||||
|
{
|
||||||
|
if (configuration.IsQuester)
|
||||||
|
{
|
||||||
|
log.Information($"[HelperManager] Helper {characterName}@{worldId} is in a party, waiting for them to leave...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnHelperInDuty(string characterName, ushort worldId)
|
||||||
|
{
|
||||||
|
if (configuration.IsQuester)
|
||||||
|
{
|
||||||
|
log.Warning($"[HelperManager] Helper {characterName}@{worldId} is in a duty! Cannot invite until they leave.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRequestHelperAnnouncements()
|
||||||
|
{
|
||||||
|
if (configuration.IsHighLevelHelper)
|
||||||
|
{
|
||||||
|
log.Information("[HelperManager] Received request for helper announcements, announcing...");
|
||||||
|
AnnounceIfHelper();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RequestHelperAnnouncements()
|
||||||
|
{
|
||||||
|
crossProcessIPC.RequestHelperAnnouncements();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
condition.ConditionChange -= OnConditionChanged;
|
||||||
|
multiClientIPC.OnHelperRequested -= OnHelperRequested;
|
||||||
|
multiClientIPC.OnHelperDismissed -= OnHelperDismissed;
|
||||||
|
multiClientIPC.OnHelperAvailable -= OnHelperAvailable;
|
||||||
|
crossProcessIPC.OnHelperRequested -= OnHelperRequested;
|
||||||
|
crossProcessIPC.OnHelperDismissed -= OnHelperDismissed;
|
||||||
|
crossProcessIPC.OnHelperAvailable -= OnHelperAvailable;
|
||||||
|
crossProcessIPC.OnHelperReady -= OnHelperReady;
|
||||||
|
crossProcessIPC.OnHelperInParty -= OnHelperInParty;
|
||||||
|
crossProcessIPC.OnHelperInDuty -= OnHelperInDuty;
|
||||||
|
crossProcessIPC.OnRequestHelperAnnouncements -= OnRequestHelperAnnouncements;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,262 @@
|
||||||
|
using System;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Dalamud.Plugin.Ipc;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class LifestreamIPC : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private readonly IDalamudPluginInterface pluginInterface;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<bool>? isBusySubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<string, bool>? changeWorldSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<uint, bool>? changeWorldByIdSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<object>? abortSubscriber;
|
||||||
|
|
||||||
|
private bool _isAvailable;
|
||||||
|
|
||||||
|
private bool _ipcInitialized;
|
||||||
|
|
||||||
|
private DateTime lastAvailabilityCheck = DateTime.MinValue;
|
||||||
|
|
||||||
|
private const int AvailabilityCheckCooldownSeconds = 5;
|
||||||
|
|
||||||
|
private bool hasPerformedInitialCheck;
|
||||||
|
|
||||||
|
public bool IsAvailable
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _isAvailable;
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
_isAvailable = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public LifestreamIPC(IPluginLog log, IDalamudPluginInterface pluginInterface)
|
||||||
|
{
|
||||||
|
this.log = log;
|
||||||
|
this.pluginInterface = pluginInterface;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeIPC()
|
||||||
|
{
|
||||||
|
if (_ipcInitialized)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
isBusySubscriber = pluginInterface.GetIpcSubscriber<bool>("Lifestream.IsBusy");
|
||||||
|
changeWorldSubscriber = pluginInterface.GetIpcSubscriber<string, bool>("Lifestream.ChangeWorld");
|
||||||
|
changeWorldByIdSubscriber = pluginInterface.GetIpcSubscriber<uint, bool>("Lifestream.ChangeWorldById");
|
||||||
|
abortSubscriber = pluginInterface.GetIpcSubscriber<object>("Lifestream.Abort");
|
||||||
|
_ipcInitialized = true;
|
||||||
|
log.Debug("[LifestreamIPC] IPC subscribers initialized (lazy-loading enabled)");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[LifestreamIPC] Failed to initialize subscribers: " + ex.Message);
|
||||||
|
_isAvailable = false;
|
||||||
|
_ipcInitialized = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryEnsureAvailable(bool forceCheck = false)
|
||||||
|
{
|
||||||
|
if (_isAvailable)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!_ipcInitialized)
|
||||||
|
{
|
||||||
|
InitializeIPC();
|
||||||
|
}
|
||||||
|
if (!_ipcInitialized)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
DateTime now = DateTime.Now;
|
||||||
|
if (!forceCheck && hasPerformedInitialCheck && (now - lastAvailabilityCheck).TotalSeconds < 5.0)
|
||||||
|
{
|
||||||
|
log.Debug($"[LifestreamIPC] Cooldown active - skipping check (last check: {(now - lastAvailabilityCheck).TotalSeconds:F1}s ago)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (forceCheck)
|
||||||
|
{
|
||||||
|
log.Information("[LifestreamIPC] FORCED availability check requested");
|
||||||
|
}
|
||||||
|
lastAvailabilityCheck = now;
|
||||||
|
hasPerformedInitialCheck = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (isBusySubscriber == null)
|
||||||
|
{
|
||||||
|
log.Debug("[LifestreamIPC] isBusySubscriber is NULL - cannot check availability");
|
||||||
|
_isAvailable = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log.Debug("[LifestreamIPC] Attempting to invoke Lifestream.IsBusy()...");
|
||||||
|
bool testBusy = isBusySubscriber.InvokeFunc();
|
||||||
|
if (!_isAvailable)
|
||||||
|
{
|
||||||
|
_isAvailable = true;
|
||||||
|
log.Information($"[LifestreamIPC] Lifestream is now available (Busy: {testBusy})");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Debug($"[LifestreamIPC] Lifestream still available (Busy: {testBusy})");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (!hasPerformedInitialCheck)
|
||||||
|
{
|
||||||
|
log.Warning("[LifestreamIPC] First availability check FAILED: " + ex.GetType().Name + ": " + ex.Message);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Debug("[LifestreamIPC] Lifestream not yet available: " + ex.Message);
|
||||||
|
}
|
||||||
|
_isAvailable = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsBusy()
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!_isAvailable || isBusySubscriber == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return isBusySubscriber.InvokeFunc();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[LifestreamIPC] Error checking busy status: " + ex.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ForceCheckAvailability()
|
||||||
|
{
|
||||||
|
log.Information("[LifestreamIPC] ========================================");
|
||||||
|
log.Information("[LifestreamIPC] === FORCING AVAILABILITY CHECK ===");
|
||||||
|
log.Information("[LifestreamIPC] ========================================");
|
||||||
|
bool result = TryEnsureAvailable(forceCheck: true);
|
||||||
|
log.Information($"[LifestreamIPC] Force check result: {result}");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ChangeWorld(string worldName)
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
log.Information("[LifestreamIPC] ========================================");
|
||||||
|
log.Information("[LifestreamIPC] === CHANGE WORLD REQUEST ===");
|
||||||
|
log.Information("[LifestreamIPC] ========================================");
|
||||||
|
log.Information("[LifestreamIPC] Target World: '" + worldName + "'");
|
||||||
|
log.Information($"[LifestreamIPC] IsAvailable: {_isAvailable}");
|
||||||
|
log.Information($"[LifestreamIPC] changeWorldSubscriber != null: {changeWorldSubscriber != null}");
|
||||||
|
if (!_isAvailable || changeWorldSubscriber == null)
|
||||||
|
{
|
||||||
|
log.Error("[LifestreamIPC] CANNOT CHANGE WORLD - Lifestream not available!");
|
||||||
|
log.Error("[LifestreamIPC] Make sure Lifestream plugin is installed and enabled!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information("[LifestreamIPC] Invoking Lifestream.ChangeWorld('" + worldName + "')...");
|
||||||
|
bool num = changeWorldSubscriber.InvokeFunc(worldName);
|
||||||
|
if (num)
|
||||||
|
{
|
||||||
|
log.Information("[LifestreamIPC] ========================================");
|
||||||
|
log.Information("[LifestreamIPC] WORLD CHANGE ACCEPTED: " + worldName);
|
||||||
|
log.Information("[LifestreamIPC] ========================================");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Warning("[LifestreamIPC] ========================================");
|
||||||
|
log.Warning("[LifestreamIPC] WORLD CHANGE REJECTED: " + worldName);
|
||||||
|
log.Warning("[LifestreamIPC] ========================================");
|
||||||
|
log.Warning("[LifestreamIPC] Possible reasons:");
|
||||||
|
log.Warning("[LifestreamIPC] - Lifestream is busy");
|
||||||
|
log.Warning("[LifestreamIPC] - World name is invalid");
|
||||||
|
log.Warning("[LifestreamIPC] - Cannot visit this world");
|
||||||
|
}
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[LifestreamIPC] ========================================");
|
||||||
|
log.Error("[LifestreamIPC] ERROR REQUESTING WORLD CHANGE!");
|
||||||
|
log.Error("[LifestreamIPC] ========================================");
|
||||||
|
log.Error("[LifestreamIPC] Error: " + ex.Message);
|
||||||
|
log.Error("[LifestreamIPC] Stack: " + ex.StackTrace);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ChangeWorldById(uint worldId)
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!_isAvailable || changeWorldByIdSubscriber == null)
|
||||||
|
{
|
||||||
|
log.Warning("[LifestreamIPC] Lifestream not available for world change");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information($"[LifestreamIPC] Requesting world change to ID: {worldId}");
|
||||||
|
bool num = changeWorldByIdSubscriber.InvokeFunc(worldId);
|
||||||
|
if (num)
|
||||||
|
{
|
||||||
|
log.Information($"[LifestreamIPC] World change request accepted for ID: {worldId}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Warning($"[LifestreamIPC] World change request rejected for ID: {worldId}");
|
||||||
|
}
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[LifestreamIPC] Error requesting world change by ID: " + ex.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Abort()
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!_isAvailable || abortSubscriber == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
abortSubscriber.InvokeAction();
|
||||||
|
log.Information("[LifestreamIPC] Abort request sent to Lifestream");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[LifestreamIPC] Error aborting Lifestream: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
log.Information("[LifestreamIPC] Service disposed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,602 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
|
using Lumina.Excel;
|
||||||
|
using Lumina.Excel.Sheets;
|
||||||
|
using QuestionableCompanion.Data;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class MSQProgressionService
|
||||||
|
{
|
||||||
|
private readonly IDataManager dataManager;
|
||||||
|
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private readonly QuestDetectionService questDetectionService;
|
||||||
|
|
||||||
|
private readonly IClientState clientState;
|
||||||
|
|
||||||
|
private readonly IFramework framework;
|
||||||
|
|
||||||
|
private List<Quest>? mainScenarioQuests;
|
||||||
|
|
||||||
|
private Dictionary<uint, string> questNameCache = new Dictionary<uint, string>();
|
||||||
|
|
||||||
|
private Dictionary<string, List<Quest>> questsByExpansion = new Dictionary<string, List<Quest>>();
|
||||||
|
|
||||||
|
private static readonly uint[] MSQ_JOURNAL_GENRE_IDS = new uint[14]
|
||||||
|
{
|
||||||
|
1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 9u, 10u,
|
||||||
|
11u, 12u, 13u, 14u
|
||||||
|
};
|
||||||
|
|
||||||
|
private const uint LAST_ARR_QUEST_ID = 65964u;
|
||||||
|
|
||||||
|
private static readonly Dictionary<uint, MSQExpansionData.Expansion> JournalGenreToExpansion = new Dictionary<uint, MSQExpansionData.Expansion>
|
||||||
|
{
|
||||||
|
{
|
||||||
|
1u,
|
||||||
|
MSQExpansionData.Expansion.ARealmReborn
|
||||||
|
},
|
||||||
|
{
|
||||||
|
2u,
|
||||||
|
MSQExpansionData.Expansion.ARealmReborn
|
||||||
|
},
|
||||||
|
{
|
||||||
|
3u,
|
||||||
|
MSQExpansionData.Expansion.Heavensward
|
||||||
|
},
|
||||||
|
{
|
||||||
|
4u,
|
||||||
|
MSQExpansionData.Expansion.Heavensward
|
||||||
|
},
|
||||||
|
{
|
||||||
|
5u,
|
||||||
|
MSQExpansionData.Expansion.Heavensward
|
||||||
|
},
|
||||||
|
{
|
||||||
|
6u,
|
||||||
|
MSQExpansionData.Expansion.Stormblood
|
||||||
|
},
|
||||||
|
{
|
||||||
|
7u,
|
||||||
|
MSQExpansionData.Expansion.Stormblood
|
||||||
|
},
|
||||||
|
{
|
||||||
|
8u,
|
||||||
|
MSQExpansionData.Expansion.Shadowbringers
|
||||||
|
},
|
||||||
|
{
|
||||||
|
9u,
|
||||||
|
MSQExpansionData.Expansion.Shadowbringers
|
||||||
|
},
|
||||||
|
{
|
||||||
|
10u,
|
||||||
|
MSQExpansionData.Expansion.Shadowbringers
|
||||||
|
},
|
||||||
|
{
|
||||||
|
11u,
|
||||||
|
MSQExpansionData.Expansion.Endwalker
|
||||||
|
},
|
||||||
|
{
|
||||||
|
12u,
|
||||||
|
MSQExpansionData.Expansion.Endwalker
|
||||||
|
},
|
||||||
|
{
|
||||||
|
13u,
|
||||||
|
MSQExpansionData.Expansion.Dawntrail
|
||||||
|
},
|
||||||
|
{
|
||||||
|
14u,
|
||||||
|
MSQExpansionData.Expansion.Dawntrail
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public MSQProgressionService(IDataManager dataManager, IPluginLog log, QuestDetectionService questDetectionService, IClientState clientState, IFramework framework)
|
||||||
|
{
|
||||||
|
this.dataManager = dataManager;
|
||||||
|
this.log = log;
|
||||||
|
this.questDetectionService = questDetectionService;
|
||||||
|
this.clientState = clientState;
|
||||||
|
this.framework = framework;
|
||||||
|
InitializeMSQData();
|
||||||
|
framework.RunOnTick(delegate
|
||||||
|
{
|
||||||
|
DebugCurrentCharacterQuest();
|
||||||
|
}, default(TimeSpan), 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeMSQData()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information("[MSQProgression] === INITIALIZING MSQ DATA ===");
|
||||||
|
ExcelSheet<Quest> questSheet = dataManager.GetExcelSheet<Quest>();
|
||||||
|
if (questSheet == null)
|
||||||
|
{
|
||||||
|
log.Error("[MSQProgression] Failed to load Quest sheet from Lumina!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int totalQuests = questSheet.Count();
|
||||||
|
log.Information($"[MSQProgression] ✓ Lumina Quest Sheet loaded: {totalQuests} total quests");
|
||||||
|
int manualCount = 0;
|
||||||
|
foreach (Quest item in questSheet)
|
||||||
|
{
|
||||||
|
_ = item;
|
||||||
|
manualCount++;
|
||||||
|
}
|
||||||
|
log.Information($"[MSQProgression] Manual iteration count: {manualCount} quests");
|
||||||
|
List<Quest> highIdQuests = questSheet.Where((Quest q) => q.RowId > 66000).ToList();
|
||||||
|
log.Information($"[MSQProgression] Quests with RowId > 66000: {highIdQuests.Count}");
|
||||||
|
if (highIdQuests.Count > 0)
|
||||||
|
{
|
||||||
|
Quest firstHighId = highIdQuests.First();
|
||||||
|
log.Information($"[MSQProgression] First High ID Quest: {firstHighId.RowId}");
|
||||||
|
log.Information($"[MSQProgression] - Name: {firstHighId.Name}");
|
||||||
|
log.Information($"[MSQProgression] - Expansion.RowId: {firstHighId.Expansion.RowId}");
|
||||||
|
log.Information($"[MSQProgression] - JournalGenre.RowId: {firstHighId.JournalGenre.RowId}");
|
||||||
|
}
|
||||||
|
log.Information("[MSQProgression] Analyzing JournalGenre distribution...");
|
||||||
|
foreach (IGrouping<uint, Quest> group in (from q in questSheet
|
||||||
|
where q.RowId != 0
|
||||||
|
group q by q.JournalGenre.RowId into g
|
||||||
|
orderby g.Key
|
||||||
|
select g).Take(10))
|
||||||
|
{
|
||||||
|
log.Information($"[MSQProgression] Genre {group.Key}: {group.Count()} quests");
|
||||||
|
}
|
||||||
|
log.Information("[MSQProgression] Filtering MSQ quests by JournalGenre categories (1-14)...");
|
||||||
|
mainScenarioQuests = (from q in questSheet
|
||||||
|
where ((ReadOnlySpan<uint>)MSQ_JOURNAL_GENRE_IDS).Contains(q.JournalGenre.RowId)
|
||||||
|
orderby q.RowId
|
||||||
|
select q).ToList();
|
||||||
|
log.Information($"[MSQProgression] ✓ Found {mainScenarioQuests.Count} total MSQ quests across all expansions!");
|
||||||
|
if (mainScenarioQuests.Count == 0)
|
||||||
|
{
|
||||||
|
log.Error("[MSQProgression] No MSQ quests found! JournalGenre filter may be incorrect.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.Information("[MSQProgression] === DETAILED MSQ QUEST ANALYSIS (First 20) ===");
|
||||||
|
foreach (Quest quest in mainScenarioQuests.Take(20))
|
||||||
|
{
|
||||||
|
log.Information($"[MSQProgression] Quest {quest.RowId}:");
|
||||||
|
log.Information($"[MSQProgression] - Name: {quest.Name}");
|
||||||
|
log.Information($"[MSQProgression] - Expansion.RowId: {quest.Expansion.RowId}");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string expansionName = quest.Expansion.Value.Name.ToString();
|
||||||
|
log.Information("[MSQProgression] - Expansion.Name: " + expansionName);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Information("[MSQProgression] - Expansion.Name: ERROR - " + ex.Message);
|
||||||
|
}
|
||||||
|
log.Information($"[MSQProgression] - JournalGenre.RowId: {quest.JournalGenre.RowId}");
|
||||||
|
}
|
||||||
|
log.Information("[MSQProgression] === MSQ QUESTS BY JOURNALGENRE (EXPANSION) ===");
|
||||||
|
foreach (IGrouping<uint, Quest> group2 in from q in mainScenarioQuests
|
||||||
|
group q by q.JournalGenre.RowId into g
|
||||||
|
orderby g.Key
|
||||||
|
select g)
|
||||||
|
{
|
||||||
|
uint genreId = group2.Key;
|
||||||
|
MSQExpansionData.Expansion expansion = JournalGenreToExpansion.GetValueOrDefault(genreId, MSQExpansionData.Expansion.ARealmReborn);
|
||||||
|
string genreName = group2.First().JournalGenre.Value.Name.ToString();
|
||||||
|
string sampleQuests = string.Join(", ", from q in group2.Take(3)
|
||||||
|
select q.RowId);
|
||||||
|
log.Information($"[MSQProgression] JournalGenre {genreId} ({expansion}):");
|
||||||
|
log.Information("[MSQProgression] - Name: " + genreName);
|
||||||
|
log.Information($"[MSQProgression] - Count: {group2.Count()} quests");
|
||||||
|
log.Information("[MSQProgression] - Samples: " + sampleQuests + "...");
|
||||||
|
}
|
||||||
|
log.Information("[MSQProgression] === MSQ QUESTS BY EXPANSION (GROUPED) ===");
|
||||||
|
foreach (IGrouping<MSQExpansionData.Expansion, Quest> group3 in from q in mainScenarioQuests
|
||||||
|
group q by JournalGenreToExpansion.GetValueOrDefault(q.JournalGenre.RowId, MSQExpansionData.Expansion.ARealmReborn) into g
|
||||||
|
orderby g.Key
|
||||||
|
select g)
|
||||||
|
{
|
||||||
|
log.Information($"[MSQProgression] {group3.Key}: {group3.Count()} quests total");
|
||||||
|
}
|
||||||
|
MSQExpansionData.ClearQuests();
|
||||||
|
log.Information("[MSQProgression] Building expansion quest mappings...");
|
||||||
|
foreach (Quest quest2 in mainScenarioQuests)
|
||||||
|
{
|
||||||
|
string name = quest2.Name.ToString();
|
||||||
|
if (!string.IsNullOrEmpty(name))
|
||||||
|
{
|
||||||
|
questNameCache[quest2.RowId] = name;
|
||||||
|
}
|
||||||
|
MSQExpansionData.Expansion expansion2 = JournalGenreToExpansion.GetValueOrDefault(quest2.JournalGenre.RowId, MSQExpansionData.Expansion.ARealmReborn);
|
||||||
|
if (quest2.JournalGenre.RowId != 2 || quest2.RowId <= 65964)
|
||||||
|
{
|
||||||
|
MSQExpansionData.RegisterQuest(quest2.RowId, expansion2);
|
||||||
|
string shortName = MSQExpansionData.GetExpansionShortName(expansion2);
|
||||||
|
if (!questsByExpansion.ContainsKey(shortName))
|
||||||
|
{
|
||||||
|
questsByExpansion[shortName] = new List<Quest>();
|
||||||
|
}
|
||||||
|
questsByExpansion[shortName].Add(quest2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Information("[MSQProgression] === EXPANSION BREAKDOWN ===");
|
||||||
|
foreach (MSQExpansionData.Expansion exp in MSQExpansionData.GetAllExpansions())
|
||||||
|
{
|
||||||
|
string shortName2 = MSQExpansionData.GetExpansionShortName(exp);
|
||||||
|
List<Quest> quests = questsByExpansion.GetValueOrDefault(shortName2);
|
||||||
|
int count = quests?.Count ?? 0;
|
||||||
|
if (count > 0 && quests != null)
|
||||||
|
{
|
||||||
|
string sampleIds = string.Join(", ", from q in quests.Take(5)
|
||||||
|
select q.RowId);
|
||||||
|
log.Information($"[MSQProgression] ✓ {MSQExpansionData.GetExpansionName(exp)} ({shortName2}): {count} quests (IDs: {sampleIds}...)");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Warning($"[MSQProgression] ⚠ {MSQExpansionData.GetExpansionName(exp)} ({shortName2}): {count} quests (EMPTY!)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Information("[MSQProgression] === MSQ DATA INITIALIZATION COMPLETE ===");
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
log.Error("[MSQProgression] EXCEPTION during MSQ data initialization: " + ex2.Message);
|
||||||
|
log.Error("[MSQProgression] Stack trace: " + ex2.StackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public (uint questId, string questName) GetLastCompletedMSQ(string characterName)
|
||||||
|
{
|
||||||
|
if (mainScenarioQuests == null || mainScenarioQuests.Count == 0)
|
||||||
|
{
|
||||||
|
return (questId: 0u, questName: "—");
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<uint> completedQuests = questDetectionService.GetAllCompletedQuestIds();
|
||||||
|
Quest lastMSQ = (from q in mainScenarioQuests
|
||||||
|
where completedQuests.Contains(q.RowId)
|
||||||
|
orderby q.RowId descending
|
||||||
|
select q).FirstOrDefault();
|
||||||
|
if (lastMSQ.RowId != 0)
|
||||||
|
{
|
||||||
|
string questName = questNameCache.GetValueOrDefault(lastMSQ.RowId, "Unknown Quest");
|
||||||
|
return (questId: lastMSQ.RowId, questName: questName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[MSQProgression] Failed to get last completed MSQ: " + ex.Message);
|
||||||
|
}
|
||||||
|
return (questId: 0u, questName: "—");
|
||||||
|
}
|
||||||
|
|
||||||
|
public float GetMSQCompletionPercentage()
|
||||||
|
{
|
||||||
|
if (mainScenarioQuests == null || mainScenarioQuests.Count == 0)
|
||||||
|
{
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<uint> completedQuests = questDetectionService.GetAllCompletedQuestIds();
|
||||||
|
return (float)mainScenarioQuests.Count((Quest q) => completedQuests.Contains(q.RowId)) / (float)mainScenarioQuests.Count * 100f;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[MSQProgression] Failed to calculate MSQ completion: " + ex.Message);
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetTotalMSQCount()
|
||||||
|
{
|
||||||
|
return mainScenarioQuests?.Count ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetCompletedMSQCount()
|
||||||
|
{
|
||||||
|
if (mainScenarioQuests == null || mainScenarioQuests.Count == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<uint> completedQuests = questDetectionService.GetAllCompletedQuestIds();
|
||||||
|
return mainScenarioQuests.Count((Quest q) => completedQuests.Contains(q.RowId));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[MSQProgression] Failed to get completed MSQ count: " + ex.Message);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetQuestName(uint questId)
|
||||||
|
{
|
||||||
|
return questNameCache.GetValueOrDefault(questId, "Unknown Quest");
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsMSQ(uint questId)
|
||||||
|
{
|
||||||
|
return mainScenarioQuests?.Any((Quest q) => q.RowId == questId) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExpansionInfo? GetExpansionForQuest(uint questId)
|
||||||
|
{
|
||||||
|
MSQExpansionData.Expansion expansion = MSQExpansionData.GetExpansionForQuest(questId);
|
||||||
|
return new ExpansionInfo
|
||||||
|
{
|
||||||
|
Name = MSQExpansionData.GetExpansionName(expansion),
|
||||||
|
ShortName = MSQExpansionData.GetExpansionShortName(expansion),
|
||||||
|
MinQuestId = 0u,
|
||||||
|
MaxQuestId = 0u,
|
||||||
|
ExpectedQuestCount = MSQExpansionData.GetExpectedQuestCount(expansion)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ExpansionInfo> GetExpansions()
|
||||||
|
{
|
||||||
|
return (from exp in MSQExpansionData.GetAllExpansions()
|
||||||
|
select new ExpansionInfo
|
||||||
|
{
|
||||||
|
Name = MSQExpansionData.GetExpansionName(exp),
|
||||||
|
ShortName = MSQExpansionData.GetExpansionShortName(exp),
|
||||||
|
MinQuestId = 0u,
|
||||||
|
MaxQuestId = 0u,
|
||||||
|
ExpectedQuestCount = MSQExpansionData.GetExpectedQuestCount(exp)
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public (int completed, int total) GetExpansionProgress(string expansionShortName)
|
||||||
|
{
|
||||||
|
List<uint> completedQuests = questDetectionService.GetAllCompletedQuestIds();
|
||||||
|
List<Quest>? obj = questsByExpansion.GetValueOrDefault(expansionShortName) ?? new List<Quest>();
|
||||||
|
int completed = obj.Count((Quest q) => completedQuests.Contains(q.RowId));
|
||||||
|
int total = obj.Count;
|
||||||
|
return (completed: completed, total: total);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExpansionInfo? GetCurrentExpansion()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information("[MSQProgression] ========================================");
|
||||||
|
log.Information("[MSQProgression] === DETECTING CURRENT EXPANSION ===");
|
||||||
|
log.Information("[MSQProgression] ========================================");
|
||||||
|
List<uint> completedQuests = questDetectionService.GetAllCompletedQuestIds();
|
||||||
|
log.Information($"[MSQProgression] Total completed quests: {completedQuests.Count}");
|
||||||
|
log.Information("[MSQProgression] METHOD 1: Using AgentScenarioTree (Game Data)");
|
||||||
|
log.Information("[MSQProgression] ------------------------------------------------");
|
||||||
|
(MSQExpansionData.Expansion expansion, string debugInfo) currentExpansionFromGameWithDebug = MSQExpansionData.GetCurrentExpansionFromGameWithDebug();
|
||||||
|
MSQExpansionData.Expansion gameExpansion = currentExpansionFromGameWithDebug.expansion;
|
||||||
|
string[] array = currentExpansionFromGameWithDebug.debugInfo.Split('\n');
|
||||||
|
foreach (string line in array)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(line))
|
||||||
|
{
|
||||||
|
log.Information("[MSQProgression] " + line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Information("[MSQProgression] Game Data Result: " + MSQExpansionData.GetExpansionName(gameExpansion));
|
||||||
|
log.Information("[MSQProgression] METHOD 2: Using Completed Quests Analysis");
|
||||||
|
log.Information("[MSQProgression] ------------------------------------------------");
|
||||||
|
MSQExpansionData.Expansion analysisExpansion = MSQExpansionData.GetCurrentExpansion(completedQuests);
|
||||||
|
log.Information("[MSQProgression] Analysis Result: " + MSQExpansionData.GetExpansionName(analysisExpansion));
|
||||||
|
array = MSQExpansionData.GetExpansionDetectionDebugInfo(completedQuests).Split('\n');
|
||||||
|
foreach (string line2 in array)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(line2))
|
||||||
|
{
|
||||||
|
log.Debug("[MSQProgression] " + line2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Information("[MSQProgression] COMPARISON:");
|
||||||
|
log.Information("[MSQProgression] Game Data: " + MSQExpansionData.GetExpansionName(gameExpansion));
|
||||||
|
log.Information("[MSQProgression] Analysis: " + MSQExpansionData.GetExpansionName(analysisExpansion));
|
||||||
|
MSQExpansionData.Expansion finalExpansion = gameExpansion;
|
||||||
|
if (gameExpansion == MSQExpansionData.Expansion.ARealmReborn && analysisExpansion != MSQExpansionData.Expansion.ARealmReborn)
|
||||||
|
{
|
||||||
|
log.Warning("[MSQProgression] Game data returned ARR but analysis found higher expansion!");
|
||||||
|
log.Warning("[MSQProgression] Using analysis result: " + MSQExpansionData.GetExpansionName(analysisExpansion));
|
||||||
|
finalExpansion = analysisExpansion;
|
||||||
|
}
|
||||||
|
log.Information("[MSQProgression] ========================================");
|
||||||
|
log.Information("[MSQProgression] >>> FINAL EXPANSION: " + MSQExpansionData.GetExpansionName(finalExpansion) + " <<<");
|
||||||
|
log.Information("[MSQProgression] ========================================");
|
||||||
|
return new ExpansionInfo
|
||||||
|
{
|
||||||
|
Name = MSQExpansionData.GetExpansionName(finalExpansion),
|
||||||
|
ShortName = MSQExpansionData.GetExpansionShortName(finalExpansion),
|
||||||
|
MinQuestId = 0u,
|
||||||
|
MaxQuestId = 0u,
|
||||||
|
ExpectedQuestCount = MSQExpansionData.GetExpectedQuestCount(finalExpansion)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[MSQProgression] Error detecting expansion: " + ex.Message);
|
||||||
|
log.Error("[MSQProgression] Stack: " + ex.StackTrace);
|
||||||
|
return GetExpansions().FirstOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, (int completed, int total)> GetExpansionProgressForCharacter(List<uint> completedQuestIds)
|
||||||
|
{
|
||||||
|
Dictionary<string, (int, int)> result = new Dictionary<string, (int, int)>();
|
||||||
|
foreach (ExpansionInfo exp in GetExpansions())
|
||||||
|
{
|
||||||
|
List<Quest> expansionQuests = questsByExpansion.GetValueOrDefault(exp.ShortName) ?? new List<Quest>();
|
||||||
|
int completed = expansionQuests.Count((Quest q) => completedQuestIds.Contains(q.RowId));
|
||||||
|
result[exp.ShortName] = (completed, expansionQuests.Count);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Quest> GetAllMSQQuests()
|
||||||
|
{
|
||||||
|
return mainScenarioQuests ?? new List<Quest>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, ExpansionProgressInfo> GetExpansionProgress()
|
||||||
|
{
|
||||||
|
Dictionary<string, ExpansionProgressInfo> result = new Dictionary<string, ExpansionProgressInfo>();
|
||||||
|
List<uint> completedQuests = questDetectionService.GetAllCompletedQuestIds();
|
||||||
|
foreach (MSQExpansionData.Expansion expansion in MSQExpansionData.GetAllExpansions())
|
||||||
|
{
|
||||||
|
ExpansionProgress progress = MSQExpansionData.GetExpansionProgress(completedQuests, expansion);
|
||||||
|
result[progress.ExpansionName] = new ExpansionProgressInfo
|
||||||
|
{
|
||||||
|
ExpansionName = progress.ExpansionName,
|
||||||
|
ShortName = progress.ExpansionShortName,
|
||||||
|
TotalQuests = progress.ExpectedCount,
|
||||||
|
CompletedQuests = progress.CompletedCount,
|
||||||
|
Percentage = progress.Percentage
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, ExpansionProgressInfo> GetExpansionProgressForCharacter(List<string> completedQuestIds)
|
||||||
|
{
|
||||||
|
Dictionary<string, ExpansionProgressInfo> result = new Dictionary<string, ExpansionProgressInfo>();
|
||||||
|
uint result2;
|
||||||
|
List<uint> completedQuestIdsUint = (from id in completedQuestIds
|
||||||
|
select uint.TryParse(id, out result2) ? result2 : 0u into id
|
||||||
|
where id != 0
|
||||||
|
select id).ToList();
|
||||||
|
foreach (MSQExpansionData.Expansion expansion in MSQExpansionData.GetAllExpansions())
|
||||||
|
{
|
||||||
|
ExpansionProgress progress = MSQExpansionData.GetExpansionProgress(completedQuestIdsUint, expansion);
|
||||||
|
result[progress.ExpansionName] = new ExpansionProgressInfo
|
||||||
|
{
|
||||||
|
ExpansionName = progress.ExpansionName,
|
||||||
|
ShortName = progress.ExpansionShortName,
|
||||||
|
TotalQuests = progress.ExpectedCount,
|
||||||
|
CompletedQuests = progress.CompletedCount,
|
||||||
|
Percentage = progress.Percentage
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExpansionInfo? GetCurrentExpansion(uint lastCompletedQuestId)
|
||||||
|
{
|
||||||
|
MSQExpansionData.Expansion expansion = MSQExpansionData.GetExpansionForQuest(lastCompletedQuestId);
|
||||||
|
return new ExpansionInfo
|
||||||
|
{
|
||||||
|
Name = MSQExpansionData.GetExpansionName(expansion),
|
||||||
|
ShortName = MSQExpansionData.GetExpansionShortName(expansion),
|
||||||
|
MinQuestId = 0u,
|
||||||
|
MaxQuestId = 0u,
|
||||||
|
ExpectedQuestCount = MSQExpansionData.GetExpectedQuestCount(expansion)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private MSQExpansionData.Expansion ConvertLuminaExpansionToOurs(uint luminaExpansionId)
|
||||||
|
{
|
||||||
|
return luminaExpansionId switch
|
||||||
|
{
|
||||||
|
0u => MSQExpansionData.Expansion.ARealmReborn,
|
||||||
|
1u => MSQExpansionData.Expansion.Heavensward,
|
||||||
|
2u => MSQExpansionData.Expansion.Stormblood,
|
||||||
|
3u => MSQExpansionData.Expansion.Shadowbringers,
|
||||||
|
4u => MSQExpansionData.Expansion.Endwalker,
|
||||||
|
5u => MSQExpansionData.Expansion.Dawntrail,
|
||||||
|
_ => MSQExpansionData.Expansion.ARealmReborn,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DebugCurrentCharacterQuest()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information("[MSQProgression] === DEBUG CURRENT CHARACTER QUEST ===");
|
||||||
|
IPlayerCharacter player = clientState.LocalPlayer;
|
||||||
|
if (player == null)
|
||||||
|
{
|
||||||
|
log.Warning("[MSQProgression] LocalPlayer is null - not logged in yet?");
|
||||||
|
framework.RunOnTick(delegate
|
||||||
|
{
|
||||||
|
DebugCurrentCharacterQuest();
|
||||||
|
}, default(TimeSpan), 60);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
string characterName = player.Name.TextValue;
|
||||||
|
string worldName = player.HomeWorld.Value.Name.ToString();
|
||||||
|
log.Information("[MSQProgression] Character: " + characterName + " @ " + worldName);
|
||||||
|
ExcelSheet<Quest> questSheet = dataManager.GetExcelSheet<Quest>();
|
||||||
|
if (questSheet == null)
|
||||||
|
{
|
||||||
|
log.Error("[MSQProgression] Failed to load Quest sheet!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<Quest> completedMSQQuests = new List<Quest>();
|
||||||
|
log.Information("[MSQProgression] Checking MSQ quest completion...");
|
||||||
|
foreach (Quest quest in questSheet)
|
||||||
|
{
|
||||||
|
if (((ReadOnlySpan<uint>)MSQ_JOURNAL_GENRE_IDS).Contains(quest.JournalGenre.RowId) && QuestManager.IsQuestComplete((ushort)quest.RowId))
|
||||||
|
{
|
||||||
|
completedMSQQuests.Add(quest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Information($"[MSQProgression] Character has {completedMSQQuests.Count} completed MSQ quests");
|
||||||
|
if (completedMSQQuests.Count == 0)
|
||||||
|
{
|
||||||
|
log.Warning("[MSQProgression] No completed MSQ quests found!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Quest latestMSQQuest = completedMSQQuests.OrderByDescending((Quest quest2) => quest2.RowId).First();
|
||||||
|
log.Information($"[MSQProgression] Latest completed MSQ quest ID: {latestMSQQuest.RowId}");
|
||||||
|
Quest questData = latestMSQQuest;
|
||||||
|
log.Information("[MSQProgression] === LATEST MSQ QUEST DETAILS ===");
|
||||||
|
log.Information($"[MSQProgression] Quest ID: {questData.RowId}");
|
||||||
|
log.Information($"[MSQProgression] Quest Name: {questData.Name}");
|
||||||
|
log.Information($"[MSQProgression] JournalGenre.RowId: {questData.JournalGenre.RowId}");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string genreName = questData.JournalGenre.Value.Name.ToString();
|
||||||
|
log.Information("[MSQProgression] JournalGenre.Name: " + genreName);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
log.Information("[MSQProgression] JournalGenre.Name: ERROR");
|
||||||
|
}
|
||||||
|
log.Information($"[MSQProgression] Expansion.RowId: {questData.Expansion.RowId}");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string expansionName = questData.Expansion.Value.Name.ToString();
|
||||||
|
log.Information("[MSQProgression] Expansion.Name: " + expansionName);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
log.Information("[MSQProgression] Expansion.Name: ERROR");
|
||||||
|
}
|
||||||
|
log.Information("[MSQProgression] === CHARACTER IS IN THIS EXPANSION ===");
|
||||||
|
log.Information($"[MSQProgression] Character is at Quest {questData.RowId} which is in:");
|
||||||
|
log.Information($"[MSQProgression] - JournalGenre: {questData.JournalGenre.RowId}");
|
||||||
|
log.Information($"[MSQProgression] - Expansion: {questData.Expansion.RowId}");
|
||||||
|
log.Information("[MSQProgression] === RECENT COMPLETED MSQ QUESTS (Last 10) ===");
|
||||||
|
foreach (Quest q in completedMSQQuests.OrderByDescending((Quest quest2) => quest2.RowId).Take(10).ToList())
|
||||||
|
{
|
||||||
|
log.Information($"[MSQProgression] Quest {q.RowId}: {q.Name} (Genre: {q.JournalGenre.RowId}, Exp: {q.Expansion.RowId})");
|
||||||
|
}
|
||||||
|
log.Information("[MSQProgression] === COMPLETED MSQ QUESTS BY EXPANSION ===");
|
||||||
|
foreach (IGrouping<MSQExpansionData.Expansion, Quest> group in from quest2 in completedMSQQuests
|
||||||
|
group quest2 by JournalGenreToExpansion.GetValueOrDefault(quest2.JournalGenre.RowId, MSQExpansionData.Expansion.ARealmReborn) into g
|
||||||
|
orderby g.Key
|
||||||
|
select g)
|
||||||
|
{
|
||||||
|
log.Information($"[MSQProgression] {group.Key}: {group.Count()} quests completed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[MSQProgression] ERROR in DebugCurrentCharacterQuest: " + ex.Message);
|
||||||
|
log.Error("[MSQProgression] Stack trace: " + ex.StackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using Dalamud.Hooking;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class MemoryHelper : IDisposable
|
||||||
|
{
|
||||||
|
public unsafe delegate void RidePillionDelegate(BattleChara* target, int seatIndex);
|
||||||
|
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private Hook<RidePillionDelegate>? ridePillionHook;
|
||||||
|
|
||||||
|
private const string RidePillionSignature = "48 85 C9 0F 84 ?? ?? ?? ?? 48 89 6C 24 ?? 56 48 83 EC";
|
||||||
|
|
||||||
|
public RidePillionDelegate? RidePillion { get; private set; }
|
||||||
|
|
||||||
|
public unsafe MemoryHelper(IPluginLog log, IGameInteropProvider gameInterop)
|
||||||
|
{
|
||||||
|
this.log = log;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ridePillionHook = gameInterop.HookFromSignature<RidePillionDelegate>("48 85 C9 0F 84 ?? ?? ?? ?? 48 89 6C 24 ?? 56 48 83 EC", RidePillionDetour);
|
||||||
|
if (ridePillionHook != null && ridePillionHook.Address != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
log.Information($"[MemoryHelper] RidePillion function found at 0x{ridePillionHook.Address:X}");
|
||||||
|
RidePillion = ridePillionHook.Original;
|
||||||
|
log.Information("[MemoryHelper] RidePillion function initialized successfully");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Warning("[MemoryHelper] RidePillion function not found - will fall back to commands");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[MemoryHelper] Error initializing RidePillion: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void RidePillionDetour(BattleChara* target, int seatIndex)
|
||||||
|
{
|
||||||
|
ridePillionHook?.Original(target, seatIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe bool ExecuteRidePillion(BattleChara* target, int seatIndex = 10)
|
||||||
|
{
|
||||||
|
if (RidePillion == null)
|
||||||
|
{
|
||||||
|
log.Warning("[MemoryHelper] RidePillion function not available");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (target == null)
|
||||||
|
{
|
||||||
|
log.Error("[MemoryHelper] RidePillion target is null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information($"[MemoryHelper] Executing RidePillion on target (seat {seatIndex})");
|
||||||
|
RidePillion(target, seatIndex);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[MemoryHelper] RidePillion execution error: " + ex.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe bool SendChatMessage(string message)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UIModule* uiModule = UIModule.Instance();
|
||||||
|
if (uiModule == null)
|
||||||
|
{
|
||||||
|
log.Error("[MemoryHelper] UIModule is null!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(message);
|
||||||
|
nint mem1 = Marshal.AllocHGlobal(400);
|
||||||
|
nint mem2 = Marshal.AllocHGlobal(bytes.Length + 30);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Marshal.Copy(bytes, 0, mem2, bytes.Length);
|
||||||
|
Marshal.WriteByte(mem2 + bytes.Length, 0);
|
||||||
|
Marshal.WriteInt64(mem1, ((IntPtr)mem2).ToInt64());
|
||||||
|
Marshal.WriteInt64(mem1 + 8, 64L);
|
||||||
|
Marshal.WriteInt64(mem1 + 8 + 8, bytes.Length + 1);
|
||||||
|
Marshal.WriteInt64(mem1 + 8 + 8 + 8, 0L);
|
||||||
|
uiModule->ProcessChatBoxEntry((Utf8String*)mem1);
|
||||||
|
log.Information("[MemoryHelper] Chat message sent: " + message);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(mem1);
|
||||||
|
Marshal.FreeHGlobal(mem2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[MemoryHelper] SendChatMessage error: " + ex.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
ridePillionHook?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
using System;
|
||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class MovementMonitorService : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IClientState clientState;
|
||||||
|
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private readonly ICommandManager commandManager;
|
||||||
|
|
||||||
|
private readonly IFramework framework;
|
||||||
|
|
||||||
|
private readonly Configuration config;
|
||||||
|
|
||||||
|
private ChauffeurModeService? chauffeurMode;
|
||||||
|
|
||||||
|
private Vector3 lastPosition = Vector3.Zero;
|
||||||
|
|
||||||
|
private DateTime lastMovementTime = DateTime.Now;
|
||||||
|
|
||||||
|
private DateTime lastCheckTime = DateTime.MinValue;
|
||||||
|
|
||||||
|
private bool isMonitoring;
|
||||||
|
|
||||||
|
private const float MovementThreshold = 0.1f;
|
||||||
|
|
||||||
|
public bool IsMonitoring => isMonitoring;
|
||||||
|
|
||||||
|
public MovementMonitorService(IClientState clientState, IPluginLog log, ICommandManager commandManager, IFramework framework, Configuration config)
|
||||||
|
{
|
||||||
|
this.clientState = clientState;
|
||||||
|
this.log = log;
|
||||||
|
this.commandManager = commandManager;
|
||||||
|
this.framework = framework;
|
||||||
|
this.config = config;
|
||||||
|
log.Information("[MovementMonitor] Service initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetChauffeurMode(ChauffeurModeService service)
|
||||||
|
{
|
||||||
|
chauffeurMode = service;
|
||||||
|
log.Information("[MovementMonitor] ChauffeurMode service linked for failsafe");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartMonitoring()
|
||||||
|
{
|
||||||
|
if (!isMonitoring)
|
||||||
|
{
|
||||||
|
isMonitoring = true;
|
||||||
|
lastMovementTime = DateTime.Now;
|
||||||
|
lastCheckTime = DateTime.Now;
|
||||||
|
lastPosition = Vector3.Zero;
|
||||||
|
framework.Update += OnFrameworkUpdate;
|
||||||
|
log.Information("[MovementMonitor] Movement monitoring started");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopMonitoring()
|
||||||
|
{
|
||||||
|
if (isMonitoring)
|
||||||
|
{
|
||||||
|
isMonitoring = false;
|
||||||
|
framework.Update -= OnFrameworkUpdate;
|
||||||
|
log.Information("[MovementMonitor] Movement monitoring stopped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetMovementTimer()
|
||||||
|
{
|
||||||
|
lastMovementTime = DateTime.Now;
|
||||||
|
if (clientState.LocalPlayer != null)
|
||||||
|
{
|
||||||
|
lastPosition = clientState.LocalPlayer.Position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFrameworkUpdate(IFramework framework)
|
||||||
|
{
|
||||||
|
if (!isMonitoring)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DateTime now = DateTime.Now;
|
||||||
|
if ((now - lastCheckTime).TotalSeconds < (double)config.MovementCheckInterval)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastCheckTime = now;
|
||||||
|
if (clientState.LocalPlayer == null || !clientState.IsLoggedIn)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Vector3 currentPosition = clientState.LocalPlayer.Position;
|
||||||
|
if (lastPosition == Vector3.Zero)
|
||||||
|
{
|
||||||
|
lastPosition = currentPosition;
|
||||||
|
lastMovementTime = now;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Vector3.Distance(lastPosition, currentPosition) > 0.1f)
|
||||||
|
{
|
||||||
|
lastMovementTime = now;
|
||||||
|
lastPosition = currentPosition;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
double timeSinceMovement = (now - lastMovementTime).TotalSeconds;
|
||||||
|
if (!(timeSinceMovement >= (double)config.MovementStuckThreshold))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.Warning("[MovementMonitor] ========================================");
|
||||||
|
log.Warning("[MovementMonitor] === PLAYER STUCK DETECTED ===");
|
||||||
|
log.Warning("[MovementMonitor] ========================================");
|
||||||
|
log.Warning($"[MovementMonitor] No movement for {timeSinceMovement:F1} seconds");
|
||||||
|
log.Warning($"[MovementMonitor] Position: {currentPosition}");
|
||||||
|
if (chauffeurMode != null && (chauffeurMode.IsWaitingForHelper || chauffeurMode.IsTransportingQuester))
|
||||||
|
{
|
||||||
|
log.Warning("[MovementMonitor] FAILSAFE: Resetting Chauffeur Mode due to stuck detection!");
|
||||||
|
chauffeurMode.ResetChauffeurState();
|
||||||
|
}
|
||||||
|
log.Warning("[MovementMonitor] Sending /qst reload command...");
|
||||||
|
framework.RunOnTick(delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/qst reload");
|
||||||
|
log.Information("[MovementMonitor] /qst reload command sent");
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
log.Error("[MovementMonitor] Failed to send reload command: " + ex2.Message);
|
||||||
|
}
|
||||||
|
}, TimeSpan.FromMilliseconds(100L, 0L));
|
||||||
|
framework.RunOnTick(delegate
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/qst start");
|
||||||
|
log.Information("[MovementMonitor] /qst start command sent");
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
log.Error("[MovementMonitor] Failed to send start command: " + ex2.Message);
|
||||||
|
}
|
||||||
|
}, TimeSpan.FromSeconds(1L));
|
||||||
|
lastMovementTime = now;
|
||||||
|
lastPosition = currentPosition;
|
||||||
|
log.Information("[MovementMonitor] Movement timer reset - monitoring continues...");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[MovementMonitor] Error checking movement: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
StopMonitoring();
|
||||||
|
log.Information("[MovementMonitor] Service disposed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,230 @@
|
||||||
|
using System;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Dalamud.Plugin.Ipc;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class MultiClientIPC : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IDalamudPluginInterface pluginInterface;
|
||||||
|
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private readonly ICallGateProvider<string, ushort, object?> requestHelperProvider;
|
||||||
|
|
||||||
|
private readonly ICallGateProvider<object?> dismissHelperProvider;
|
||||||
|
|
||||||
|
private readonly ICallGateProvider<string, ushort, object?> helperAvailableProvider;
|
||||||
|
|
||||||
|
private readonly ICallGateProvider<string, object?> chatMessageProvider;
|
||||||
|
|
||||||
|
private readonly ICallGateProvider<string, ushort, object?> passengerMountedProvider;
|
||||||
|
|
||||||
|
private readonly ICallGateSubscriber<string, ushort, object?> requestHelperSubscriber;
|
||||||
|
|
||||||
|
private readonly ICallGateSubscriber<object?> dismissHelperSubscriber;
|
||||||
|
|
||||||
|
private readonly ICallGateSubscriber<string, ushort, object?> helperAvailableSubscriber;
|
||||||
|
|
||||||
|
private readonly ICallGateSubscriber<string, object?> chatMessageSubscriber;
|
||||||
|
|
||||||
|
private readonly ICallGateSubscriber<string, ushort, object?> passengerMountedSubscriber;
|
||||||
|
|
||||||
|
public event Action<string, ushort>? OnHelperRequested;
|
||||||
|
|
||||||
|
public event Action? OnHelperDismissed;
|
||||||
|
|
||||||
|
public event Action<string, ushort>? OnHelperAvailable;
|
||||||
|
|
||||||
|
public event Action<string>? OnChatMessageReceived;
|
||||||
|
|
||||||
|
public event Action<string, ushort>? OnPassengerMounted;
|
||||||
|
|
||||||
|
public MultiClientIPC(IDalamudPluginInterface pluginInterface, IPluginLog log)
|
||||||
|
{
|
||||||
|
this.pluginInterface = pluginInterface;
|
||||||
|
this.log = log;
|
||||||
|
requestHelperProvider = pluginInterface.GetIpcProvider<string, ushort, object>("QSTCompanion.RequestHelper");
|
||||||
|
dismissHelperProvider = pluginInterface.GetIpcProvider<object>("QSTCompanion.DismissHelper");
|
||||||
|
helperAvailableProvider = pluginInterface.GetIpcProvider<string, ushort, object>("QSTCompanion.HelperAvailable");
|
||||||
|
chatMessageProvider = pluginInterface.GetIpcProvider<string, object>("QSTCompanion.ChatMessage");
|
||||||
|
passengerMountedProvider = pluginInterface.GetIpcProvider<string, ushort, object>("QSTCompanion.PassengerMounted");
|
||||||
|
requestHelperSubscriber = pluginInterface.GetIpcSubscriber<string, ushort, object>("QSTCompanion.RequestHelper");
|
||||||
|
dismissHelperSubscriber = pluginInterface.GetIpcSubscriber<object>("QSTCompanion.DismissHelper");
|
||||||
|
helperAvailableSubscriber = pluginInterface.GetIpcSubscriber<string, ushort, object>("QSTCompanion.HelperAvailable");
|
||||||
|
chatMessageSubscriber = pluginInterface.GetIpcSubscriber<string, object>("QSTCompanion.ChatMessage");
|
||||||
|
passengerMountedSubscriber = pluginInterface.GetIpcSubscriber<string, ushort, object>("QSTCompanion.PassengerMounted");
|
||||||
|
requestHelperProvider.RegisterFunc(delegate(string name, ushort worldId)
|
||||||
|
{
|
||||||
|
OnRequestHelperReceived(name, worldId);
|
||||||
|
return (object?)null;
|
||||||
|
});
|
||||||
|
dismissHelperProvider.RegisterFunc(delegate
|
||||||
|
{
|
||||||
|
OnDismissHelperReceived();
|
||||||
|
return (object?)null;
|
||||||
|
});
|
||||||
|
helperAvailableProvider.RegisterFunc(delegate(string name, ushort worldId)
|
||||||
|
{
|
||||||
|
OnHelperAvailableReceived(name, worldId);
|
||||||
|
return (object?)null;
|
||||||
|
});
|
||||||
|
chatMessageProvider.RegisterFunc(delegate(string message)
|
||||||
|
{
|
||||||
|
OnChatMessageReceivedInternal(message);
|
||||||
|
return (object?)null;
|
||||||
|
});
|
||||||
|
passengerMountedProvider.RegisterFunc(delegate(string questerName, ushort questerWorld)
|
||||||
|
{
|
||||||
|
OnPassengerMountedReceived(questerName, questerWorld);
|
||||||
|
return (object?)null;
|
||||||
|
});
|
||||||
|
log.Information("[MultiClientIPC] ✅ IPC initialized successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RequestHelper(string characterName, ushort worldId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information($"[MultiClientIPC] Broadcasting helper request: {characterName}@{worldId}");
|
||||||
|
requestHelperSubscriber.InvokeFunc(characterName, worldId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[MultiClientIPC] Failed to send helper request: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DismissHelper()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information("[MultiClientIPC] Broadcasting helper dismiss");
|
||||||
|
dismissHelperSubscriber.InvokeFunc();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[MultiClientIPC] Failed to send helper dismiss: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRequestHelperReceived(string characterName, ushort worldId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information($"[MultiClientIPC] Received helper request: {characterName}@{worldId}");
|
||||||
|
this.OnHelperRequested?.Invoke(characterName, worldId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[MultiClientIPC] Error handling helper request: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDismissHelperReceived()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information("[MultiClientIPC] Received helper dismiss");
|
||||||
|
this.OnHelperDismissed?.Invoke();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[MultiClientIPC] Error handling helper dismiss: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AnnounceHelperAvailable(string characterName, ushort worldId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information($"[MultiClientIPC] Broadcasting helper availability: {characterName}@{worldId}");
|
||||||
|
helperAvailableSubscriber.InvokeFunc(characterName, worldId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[MultiClientIPC] Failed to announce helper: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnHelperAvailableReceived(string characterName, ushort worldId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information($"[MultiClientIPC] Received helper available: {characterName}@{worldId}");
|
||||||
|
this.OnHelperAvailable?.Invoke(characterName, worldId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[MultiClientIPC] Error handling helper available: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendChatMessage(string message)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information("[MultiClientIPC] Broadcasting chat message: " + message);
|
||||||
|
chatMessageSubscriber.InvokeFunc(message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[MultiClientIPC] Failed to send chat message: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnChatMessageReceivedInternal(string message)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information("[MultiClientIPC] Received chat message: " + message);
|
||||||
|
this.OnChatMessageReceived?.Invoke(message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[MultiClientIPC] Error handling chat message: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendPassengerMounted(string questerName, ushort questerWorld)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information($"[MultiClientIPC] Broadcasting passenger mounted: {questerName}@{questerWorld}");
|
||||||
|
passengerMountedSubscriber.InvokeFunc(questerName, questerWorld);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[MultiClientIPC] Failed to send passenger mounted: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPassengerMountedReceived(string questerName, ushort questerWorld)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information($"[MultiClientIPC] Received passenger mounted: {questerName}@{questerWorld}");
|
||||||
|
this.OnPassengerMounted?.Invoke(questerName, questerWorld);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[MultiClientIPC] Error handling passenger mounted: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
requestHelperProvider.UnregisterFunc();
|
||||||
|
dismissHelperProvider.UnregisterFunc();
|
||||||
|
helperAvailableProvider.UnregisterFunc();
|
||||||
|
chatMessageProvider.UnregisterFunc();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[MultiClientIPC] Error during dispose: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
using System;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class PartyInviteAutoAccept : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private readonly IFramework framework;
|
||||||
|
|
||||||
|
private readonly IGameGui gameGui;
|
||||||
|
|
||||||
|
private readonly IPartyList partyList;
|
||||||
|
|
||||||
|
private readonly Configuration configuration;
|
||||||
|
|
||||||
|
private bool shouldAutoAccept;
|
||||||
|
|
||||||
|
private DateTime autoAcceptUntil = DateTime.MinValue;
|
||||||
|
|
||||||
|
public PartyInviteAutoAccept(IPluginLog log, IFramework framework, IGameGui gameGui, IPartyList partyList, Configuration configuration)
|
||||||
|
{
|
||||||
|
this.log = log;
|
||||||
|
this.framework = framework;
|
||||||
|
this.gameGui = gameGui;
|
||||||
|
this.partyList = partyList;
|
||||||
|
this.configuration = configuration;
|
||||||
|
framework.Update += OnFrameworkUpdate;
|
||||||
|
log.Information("[PartyInviteAutoAccept] Initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EnableAutoAccept()
|
||||||
|
{
|
||||||
|
if (!configuration.IsHighLevelHelper && !configuration.IsQuester)
|
||||||
|
{
|
||||||
|
log.Debug("[PartyInviteAutoAccept] Not a helper or quester, ignoring auto-accept request");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
shouldAutoAccept = true;
|
||||||
|
autoAcceptUntil = DateTime.Now.AddSeconds(30.0);
|
||||||
|
string role = (configuration.IsHighLevelHelper ? "Helper" : "Quester");
|
||||||
|
log.Information("[PartyInviteAutoAccept] Auto-accept enabled for 30 seconds (" + role + ")");
|
||||||
|
log.Information($"[PartyInviteAutoAccept] Will accept until: {autoAcceptUntil:HH:mm:ss}");
|
||||||
|
log.Information("[PartyInviteAutoAccept] Will accept ALL party invites during this time!");
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void OnFrameworkUpdate(IFramework framework)
|
||||||
|
{
|
||||||
|
if (!shouldAutoAccept)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (DateTime.Now > autoAcceptUntil)
|
||||||
|
{
|
||||||
|
shouldAutoAccept = false;
|
||||||
|
log.Information("[PartyInviteAutoAccept] Auto-accept window expired");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string[] obj = new string[6] { "SelectYesno", "SelectYesNo", "_PartyInvite", "PartyInvite", "SelectString", "_Notification" };
|
||||||
|
nint addonPtr = IntPtr.Zero;
|
||||||
|
string[] array = obj;
|
||||||
|
foreach (string name in array)
|
||||||
|
{
|
||||||
|
addonPtr = (nint)gameGui.GetAddonByName(name);
|
||||||
|
if (addonPtr != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
log.Warning("[PartyInviteAutoAccept] Addon pointer is null!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!addon2->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
|
||||||
|
};
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
log.Error("[PartyInviteAutoAccept] Error: " + ex.Message);
|
||||||
|
log.Error("[PartyInviteAutoAccept] Stack: " + ex.StackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
framework.Update -= OnFrameworkUpdate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,193 @@
|
||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Group;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI.Shell;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class PartyInviteService
|
||||||
|
{
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private readonly IObjectTable objectTable;
|
||||||
|
|
||||||
|
private readonly IClientState clientState;
|
||||||
|
|
||||||
|
public PartyInviteService(IPluginLog log, IObjectTable objectTable, IClientState clientState)
|
||||||
|
{
|
||||||
|
this.log = log;
|
||||||
|
this.objectTable = objectTable;
|
||||||
|
this.clientState = clientState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe bool InviteToParty(string characterName, ushort worldId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(characterName))
|
||||||
|
{
|
||||||
|
log.Error("[PartyInvite] Character name is null or empty!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (worldId == 0)
|
||||||
|
{
|
||||||
|
log.Error("[PartyInvite] World ID is 0 (invalid)!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
characterName = characterName.Trim();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
InfoModule* infoModule = InfoModule.Instance();
|
||||||
|
if (infoModule == null)
|
||||||
|
{
|
||||||
|
log.Error("[PartyInvite] InfoModule is null!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
InfoProxyPartyInvite* partyInviteProxy = (InfoProxyPartyInvite*)infoModule->GetInfoProxyById(InfoProxyId.PartyInvite);
|
||||||
|
if (partyInviteProxy == null)
|
||||||
|
{
|
||||||
|
log.Error("[PartyInvite] InfoProxyPartyInvite is null!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ulong contentId = 0uL;
|
||||||
|
log.Information($"[PartyInvite] Using name-based invite (ContentId=0, Name={characterName}, World={worldId})");
|
||||||
|
log.Information($"[PartyInvite] Sending invite to {characterName}@{worldId} (ContentId: {contentId})");
|
||||||
|
fixed (byte* namePtr = Encoding.UTF8.GetBytes(characterName + "\0"))
|
||||||
|
{
|
||||||
|
bool num = partyInviteProxy->InviteToParty(contentId, namePtr, worldId);
|
||||||
|
if (num)
|
||||||
|
{
|
||||||
|
log.Information($"[PartyInvite] ✓ Successfully sent invite to {characterName}@{worldId}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Warning($"[PartyInvite] ✗ Failed to send invite to {characterName}@{worldId}");
|
||||||
|
}
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[PartyInvite] Exception: " + ex.Message);
|
||||||
|
log.Error("[PartyInvite] StackTrace: " + ex.StackTrace);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe bool InviteToPartyByContentId(ulong contentId, ushort worldId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
InfoModule* infoModule = InfoModule.Instance();
|
||||||
|
if (infoModule == null)
|
||||||
|
{
|
||||||
|
log.Error("[PartyInvite] InfoModule is null!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
InfoProxyPartyInvite* partyInviteProxy = (InfoProxyPartyInvite*)infoModule->GetInfoProxyById(InfoProxyId.PartyInvite);
|
||||||
|
if (partyInviteProxy == null)
|
||||||
|
{
|
||||||
|
log.Error("[PartyInvite] InfoProxyPartyInvite is null!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log.Information($"[PartyInvite] Sending invite to ContentID {contentId}@{worldId}");
|
||||||
|
bool num = partyInviteProxy->InviteToPartyContentId(contentId, worldId);
|
||||||
|
if (num)
|
||||||
|
{
|
||||||
|
log.Information($"[PartyInvite] Successfully sent invite to ContentID {contentId}@{worldId}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Warning($"[PartyInvite] Failed to send invite to ContentID {contentId}@{worldId}");
|
||||||
|
}
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[PartyInvite] Exception: " + ex.Message);
|
||||||
|
log.Error("[PartyInvite] StackTrace: " + ex.StackTrace);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe bool InviteToPartyInInstanceByContentId(ulong contentId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
InfoModule* infoModule = InfoModule.Instance();
|
||||||
|
if (infoModule == null)
|
||||||
|
{
|
||||||
|
log.Error("[PartyInvite] InfoModule is null!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
InfoProxyPartyInvite* partyInviteProxy = (InfoProxyPartyInvite*)infoModule->GetInfoProxyById(InfoProxyId.PartyInvite);
|
||||||
|
if (partyInviteProxy == null)
|
||||||
|
{
|
||||||
|
log.Error("[PartyInvite] InfoProxyPartyInvite is null!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log.Information($"[PartyInvite] Sending instance invite to ContentID {contentId}");
|
||||||
|
bool num = partyInviteProxy->InviteToPartyInInstanceByContentId(contentId);
|
||||||
|
if (num)
|
||||||
|
{
|
||||||
|
log.Information($"[PartyInvite] Successfully sent instance invite to ContentID {contentId}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Warning($"[PartyInvite] Failed to send instance invite to ContentID {contentId}");
|
||||||
|
}
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[PartyInvite] Exception: " + ex.Message);
|
||||||
|
log.Error("[PartyInvite] StackTrace: " + ex.StackTrace);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe bool LeaveParty()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
GroupManager* groupManager = GroupManager.Instance();
|
||||||
|
if (groupManager == null)
|
||||||
|
{
|
||||||
|
log.Error("[PartyInvite] GroupManager is null!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
GroupManager.Group* group = groupManager->GetGroup();
|
||||||
|
if (group == null || group->MemberCount == 0)
|
||||||
|
{
|
||||||
|
log.Debug("[PartyInvite] Not in a party");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
log.Information($"[PartyInvite] Leaving party (Members: {group->MemberCount})");
|
||||||
|
RaptureShellModule* shellModule = RaptureShellModule.Instance();
|
||||||
|
if (shellModule == null)
|
||||||
|
{
|
||||||
|
log.Error("[PartyInvite] RaptureShellModule is null!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
UIModule* uiModule = UIModule.Instance();
|
||||||
|
if (uiModule == null)
|
||||||
|
{
|
||||||
|
log.Error("[PartyInvite] UIModule is null!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Utf8String* leaveCommand = Utf8String.FromString("/leave");
|
||||||
|
shellModule->ExecuteCommandInner(leaveCommand, uiModule);
|
||||||
|
leaveCommand->Dtor();
|
||||||
|
log.Information("[PartyInvite] Leave command executed successfully");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[PartyInvite] Exception: " + ex.Message);
|
||||||
|
log.Error("[PartyInvite] StackTrace: " + ex.StackTrace);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
using System;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class PluginLogger
|
||||||
|
{
|
||||||
|
private readonly IPluginLog dalamudLog;
|
||||||
|
|
||||||
|
public PluginLogger(IPluginLog dalamudLog)
|
||||||
|
{
|
||||||
|
this.dalamudLog = dalamudLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Debug(string message, string component = "Plugin")
|
||||||
|
{
|
||||||
|
dalamudLog.Debug(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Information(string message, string component = "Plugin")
|
||||||
|
{
|
||||||
|
dalamudLog.Information(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Warning(string message, string component = "Plugin")
|
||||||
|
{
|
||||||
|
dalamudLog.Warning(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Error(string message, string component = "Plugin")
|
||||||
|
{
|
||||||
|
dalamudLog.Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Error(Exception ex, string message, string component = "Plugin")
|
||||||
|
{
|
||||||
|
dalamudLog.Error(ex, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,256 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class QuestDetectionService : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IFramework framework;
|
||||||
|
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private readonly IClientState clientState;
|
||||||
|
|
||||||
|
private readonly HashSet<uint> acceptedQuests = new HashSet<uint>();
|
||||||
|
|
||||||
|
private readonly HashSet<uint> completedQuests = new HashSet<uint>();
|
||||||
|
|
||||||
|
private HashSet<uint> completedQuestCache = new HashSet<uint>();
|
||||||
|
|
||||||
|
private DateTime lastCacheRefresh = DateTime.MinValue;
|
||||||
|
|
||||||
|
private const int CACHE_REFRESH_MINUTES = 5;
|
||||||
|
|
||||||
|
public event Action<uint, string>? QuestAccepted;
|
||||||
|
|
||||||
|
public event Action<uint, string>? QuestCompleted;
|
||||||
|
|
||||||
|
public QuestDetectionService(IFramework framework, IPluginLog log, IClientState clientState)
|
||||||
|
{
|
||||||
|
this.framework = framework;
|
||||||
|
this.log = log;
|
||||||
|
this.clientState = clientState;
|
||||||
|
framework.Update += OnFrameworkUpdate;
|
||||||
|
log.Information("[QuestDetection] Service initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFrameworkUpdate(IFramework framework)
|
||||||
|
{
|
||||||
|
if (!clientState.IsLoggedIn)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CheckQuestUpdates();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Debug("[QuestDetection] Error in framework update: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void CheckQuestUpdates()
|
||||||
|
{
|
||||||
|
QuestManager* questManager = QuestManager.Instance();
|
||||||
|
if (questManager == null)
|
||||||
|
{
|
||||||
|
log.Debug("[QuestDetection] QuestManager instance is null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Span<QuestWork> normalQuests = questManager->NormalQuests;
|
||||||
|
if (normalQuests.Length == 0)
|
||||||
|
{
|
||||||
|
log.Debug("[QuestDetection] NormalQuests array is empty");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int maxSlots = Math.Min(normalQuests.Length, 30);
|
||||||
|
for (int i = 0; i < maxSlots; i++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
QuestWork quest = normalQuests[i];
|
||||||
|
if (quest.QuestId == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
uint questId = quest.QuestId;
|
||||||
|
if (!acceptedQuests.Contains(questId))
|
||||||
|
{
|
||||||
|
if (!IsQuestComplete(questId))
|
||||||
|
{
|
||||||
|
acceptedQuests.Add(questId);
|
||||||
|
string questName = GetQuestName(questId);
|
||||||
|
log.Information($"[QuestDetection] Quest Accepted: {questId} - {questName}");
|
||||||
|
this.QuestAccepted?.Invoke(questId, questName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!completedQuests.Contains(questId) && IsQuestComplete(questId))
|
||||||
|
{
|
||||||
|
completedQuests.Add(questId);
|
||||||
|
string questName2 = GetQuestName(questId);
|
||||||
|
log.Information($"[QuestDetection] Quest Completed: {questId} - {questName2}");
|
||||||
|
this.QuestCompleted?.Invoke(questId, questName2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IndexOutOfRangeException)
|
||||||
|
{
|
||||||
|
log.Debug($"[QuestDetection] Index {i} out of range, stopping quest check");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
log.Debug($"[QuestDetection] Error checking quest slot {i}: {ex2.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex3)
|
||||||
|
{
|
||||||
|
log.Warning("[QuestDetection] Error accessing quest data: " + ex3.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsQuestComplete(uint questId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return QuestManager.IsQuestComplete(questId);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe bool IsQuestCompletedDirect(uint questId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (QuestManager.Instance() == null)
|
||||||
|
{
|
||||||
|
log.Warning("[QuestDetection] QuestManager instance not available");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool isComplete = QuestManager.IsQuestComplete(questId);
|
||||||
|
log.Debug($"[QuestDetection] Quest {questId} completion status: {isComplete}");
|
||||||
|
return isComplete;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error($"[QuestDetection] Failed to check quest {questId}: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe List<uint> GetAllCompletedQuestIds()
|
||||||
|
{
|
||||||
|
List<uint> completed = new List<uint>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (QuestManager.Instance() == null)
|
||||||
|
{
|
||||||
|
log.Warning("[QuestDetection] QuestManager instance not available");
|
||||||
|
return completed;
|
||||||
|
}
|
||||||
|
log.Information("[QuestDetection] Scanning for completed quests...");
|
||||||
|
foreach (var item in new List<(uint, uint)>
|
||||||
|
{
|
||||||
|
(1u, 3000u),
|
||||||
|
(65000u, 71000u)
|
||||||
|
})
|
||||||
|
{
|
||||||
|
uint start = item.Item1;
|
||||||
|
uint end = item.Item2;
|
||||||
|
for (uint i = start; i <= end; i++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (QuestManager.IsQuestComplete(i))
|
||||||
|
{
|
||||||
|
completed.Add(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Information($"[QuestDetection] Retrieved {completed.Count} completed quests");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[QuestDetection] Error while fetching completed quests: " + ex.Message);
|
||||||
|
}
|
||||||
|
return completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RefreshQuestCache()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information("[QuestDetection] Refreshing quest cache...");
|
||||||
|
List<uint> allCompleted = GetAllCompletedQuestIds();
|
||||||
|
completedQuestCache = new HashSet<uint>(allCompleted);
|
||||||
|
lastCacheRefresh = DateTime.Now;
|
||||||
|
log.Information($"[QuestDetection] Quest cache refreshed with {completedQuestCache.Count} completed quests");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[QuestDetection] Failed to refresh quest cache: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsQuestCompletedCached(uint questId)
|
||||||
|
{
|
||||||
|
if (completedQuestCache.Count == 0 || (DateTime.Now - lastCacheRefresh).TotalMinutes > 5.0)
|
||||||
|
{
|
||||||
|
RefreshQuestCache();
|
||||||
|
}
|
||||||
|
return completedQuestCache.Contains(questId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetQuestName(uint questId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return $"Quest {questId}";
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return $"Quest {questId}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetTracking()
|
||||||
|
{
|
||||||
|
acceptedQuests.Clear();
|
||||||
|
completedQuests.Clear();
|
||||||
|
completedQuestCache.Clear();
|
||||||
|
lastCacheRefresh = DateTime.MinValue;
|
||||||
|
log.Information("[QuestDetection] Tracking reset");
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsQuestAccepted(uint questId)
|
||||||
|
{
|
||||||
|
return acceptedQuests.Contains(questId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsQuestCompleted(uint questId)
|
||||||
|
{
|
||||||
|
return completedQuests.Contains(questId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
framework.Update -= OnFrameworkUpdate;
|
||||||
|
acceptedQuests.Clear();
|
||||||
|
completedQuests.Clear();
|
||||||
|
completedQuestCache.Clear();
|
||||||
|
log.Information("[QuestDetection] Service disposed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public static class QuestIdParser
|
||||||
|
{
|
||||||
|
private static readonly Regex EventQuestPattern = new Regex("^([A-Z])(\\d+)$", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
public static (string rawId, string eventQuestId) ParseQuestId(string questInput)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(questInput))
|
||||||
|
{
|
||||||
|
return (rawId: questInput, eventQuestId: questInput);
|
||||||
|
}
|
||||||
|
Match match = EventQuestPattern.Match(questInput);
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
_ = match.Groups[1].Value;
|
||||||
|
return (rawId: match.Groups[2].Value, eventQuestId: questInput);
|
||||||
|
}
|
||||||
|
return (rawId: questInput, eventQuestId: questInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool HasEventQuestPrefix(string questId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(questId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return EventQuestPattern.IsMatch(questId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? GetEventQuestPrefix(string questId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(questId))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Match match = EventQuestPattern.Match(questId);
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return match.Groups[1].Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetNumericPart(string questId)
|
||||||
|
{
|
||||||
|
return ParseQuestId(questId).rawId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static QuestIdType ClassifyQuestId(string questId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(questId))
|
||||||
|
{
|
||||||
|
return QuestIdType.Invalid;
|
||||||
|
}
|
||||||
|
if (HasEventQuestPrefix(questId))
|
||||||
|
{
|
||||||
|
return QuestIdType.EventQuest;
|
||||||
|
}
|
||||||
|
if (uint.TryParse(questId, out var _))
|
||||||
|
{
|
||||||
|
return QuestIdType.Standard;
|
||||||
|
}
|
||||||
|
return QuestIdType.Unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public enum QuestIdType
|
||||||
|
{
|
||||||
|
Standard,
|
||||||
|
EventQuest,
|
||||||
|
Unknown,
|
||||||
|
Invalid
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,301 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class QuestPreCheckService : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private readonly IClientState clientState;
|
||||||
|
|
||||||
|
private readonly Configuration config;
|
||||||
|
|
||||||
|
private readonly AutoRetainerIPC autoRetainerIPC;
|
||||||
|
|
||||||
|
private Dictionary<string, bool> preCheckResults = new Dictionary<string, bool>();
|
||||||
|
|
||||||
|
private Dictionary<string, Dictionary<uint, bool>> questDatabase = new Dictionary<string, Dictionary<uint, bool>>();
|
||||||
|
|
||||||
|
private Dictionary<string, DateTime> lastRefreshByCharacter = new Dictionary<string, DateTime>();
|
||||||
|
|
||||||
|
private readonly TimeSpan refreshInterval = TimeSpan.FromMinutes(30L);
|
||||||
|
|
||||||
|
private string QuestDatabasePath
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
global::_003C_003Ey__InlineArray5<string> buffer = default(global::_003C_003Ey__InlineArray5<string>);
|
||||||
|
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<global::_003C_003Ey__InlineArray5<string>, string>(ref buffer, 0) = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||||
|
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<global::_003C_003Ey__InlineArray5<string>, string>(ref buffer, 1) = "XIVLauncher";
|
||||||
|
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<global::_003C_003Ey__InlineArray5<string>, string>(ref buffer, 2) = "pluginConfigs";
|
||||||
|
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<global::_003C_003Ey__InlineArray5<string>, string>(ref buffer, 3) = "QuestionableCompanion";
|
||||||
|
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<global::_003C_003Ey__InlineArray5<string>, string>(ref buffer, 4) = "QuestDatabase.json";
|
||||||
|
return Path.Combine(global::_003CPrivateImplementationDetails_003E.InlineArrayAsReadOnlySpan<global::_003C_003Ey__InlineArray5<string>, string>(in buffer, 5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public QuestPreCheckService(IPluginLog log, IClientState clientState, Configuration config, AutoRetainerIPC autoRetainerIPC)
|
||||||
|
{
|
||||||
|
this.log = log;
|
||||||
|
this.clientState = clientState;
|
||||||
|
this.config = config;
|
||||||
|
this.autoRetainerIPC = autoRetainerIPC;
|
||||||
|
LoadQuestDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadQuestDatabase()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EnsureQuestDatabasePath();
|
||||||
|
if (!File.Exists(QuestDatabasePath))
|
||||||
|
{
|
||||||
|
log.Information("[QuestPreCheck] Creating new quest database...");
|
||||||
|
questDatabase = new Dictionary<string, Dictionary<uint, bool>>();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
string json = File.ReadAllText(QuestDatabasePath);
|
||||||
|
if (string.IsNullOrEmpty(json))
|
||||||
|
{
|
||||||
|
questDatabase = new Dictionary<string, Dictionary<uint, bool>>();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
questDatabase = JsonConvert.DeserializeObject<Dictionary<string, Dictionary<uint, bool>>>(json) ?? new Dictionary<string, Dictionary<uint, bool>>();
|
||||||
|
log.Information($"[QuestPreCheck] Loaded quest database for {questDatabase.Count} characters");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[QuestPreCheck] Error loading quest database: " + ex.Message);
|
||||||
|
questDatabase = new Dictionary<string, Dictionary<uint, bool>>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveQuestDatabase()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EnsureQuestDatabasePath();
|
||||||
|
string json = JsonConvert.SerializeObject(questDatabase, Formatting.Indented);
|
||||||
|
File.WriteAllText(QuestDatabasePath, json);
|
||||||
|
log.Information($"[QuestPreCheck] Quest database saved ({questDatabase.Count} characters)");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[QuestPreCheck] Error saving quest database: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureQuestDatabasePath()
|
||||||
|
{
|
||||||
|
string directory = Path.GetDirectoryName(QuestDatabasePath);
|
||||||
|
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe void ScanCurrentCharacterQuestStatus(bool verbose = false)
|
||||||
|
{
|
||||||
|
if (clientState.LocalPlayer == null)
|
||||||
|
{
|
||||||
|
log.Warning("[QuestPreCheck] No local player found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
string worldName = clientState.LocalPlayer.HomeWorld.Value.Name.ToString();
|
||||||
|
string charName = $"{clientState.LocalPlayer.Name}@{worldName}";
|
||||||
|
if (verbose)
|
||||||
|
{
|
||||||
|
log.Information("[QuestPreCheck] Scanning quest status for: " + charName);
|
||||||
|
}
|
||||||
|
if (!questDatabase.ContainsKey(charName))
|
||||||
|
{
|
||||||
|
questDatabase[charName] = new Dictionary<uint, bool>();
|
||||||
|
}
|
||||||
|
if (QuestManager.Instance() == null)
|
||||||
|
{
|
||||||
|
log.Error("[QuestPreCheck] QuestManager not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int questsScanned = 0;
|
||||||
|
int questsCompleted = 0;
|
||||||
|
int questsChanged = 0;
|
||||||
|
List<uint> newlyCompleted = new List<uint>();
|
||||||
|
List<uint> questsToScan = config.QuestPreCheckRange ?? new List<uint>();
|
||||||
|
if (questsToScan.Count == 0)
|
||||||
|
{
|
||||||
|
for (uint questId = 1u; questId <= 4500; questId++)
|
||||||
|
{
|
||||||
|
questsToScan.Add(questId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (uint questId2 in questsToScan)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool num = QuestManager.IsQuestComplete((ushort)(questId2 % 65536));
|
||||||
|
questsScanned++;
|
||||||
|
if (num)
|
||||||
|
{
|
||||||
|
questsCompleted++;
|
||||||
|
if (!questDatabase[charName].GetValueOrDefault(questId2, defaultValue: false))
|
||||||
|
{
|
||||||
|
questDatabase[charName][questId2] = true;
|
||||||
|
questsChanged++;
|
||||||
|
newlyCompleted.Add(questId2);
|
||||||
|
if (verbose)
|
||||||
|
{
|
||||||
|
log.Debug($"[QuestPreCheck] {charName} - Quest {questId2}: ✓ NEWLY COMPLETED");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
questDatabase[charName][questId2] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (verbose && questId2 % 500 == 0)
|
||||||
|
{
|
||||||
|
log.Debug($"[QuestPreCheck] Progress: {questId2}/{questsToScan.Count} quests scanned...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error($"[QuestPreCheck] Error checking quest {questId2}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (verbose)
|
||||||
|
{
|
||||||
|
log.Information($"[QuestPreCheck] Scan complete: {questsScanned} checked, {questsCompleted} completed, {questsChanged} changed");
|
||||||
|
if (newlyCompleted.Count > 0)
|
||||||
|
{
|
||||||
|
log.Information("[QuestPreCheck] NEWLY COMPLETED: " + string.Join(", ", newlyCompleted));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastRefreshByCharacter[charName] = DateTime.Now;
|
||||||
|
SaveQuestDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RefreshQuestDatabasePeriodic()
|
||||||
|
{
|
||||||
|
if (clientState.LocalPlayer != null && clientState.IsLoggedIn)
|
||||||
|
{
|
||||||
|
string worldName = clientState.LocalPlayer.HomeWorld.Value.Name.ToString();
|
||||||
|
string charName = $"{clientState.LocalPlayer.Name}@{worldName}";
|
||||||
|
if (!lastRefreshByCharacter.TryGetValue(charName, out var lastRefresh) || DateTime.Now - lastRefresh >= refreshInterval)
|
||||||
|
{
|
||||||
|
log.Information("[QuestDB] === 30-MINUTE REFRESH TRIGGERED ===");
|
||||||
|
log.Information("[QuestDB] Updating quest status for: " + charName);
|
||||||
|
ScanCurrentCharacterQuestStatus(verbose: true);
|
||||||
|
log.Information("[QuestDB] === 30-MINUTE REFRESH COMPLETE ===");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogCompletedQuestsBeforeLogout()
|
||||||
|
{
|
||||||
|
if (clientState.LocalPlayer != null)
|
||||||
|
{
|
||||||
|
string worldName = clientState.LocalPlayer.HomeWorld.Value.Name.ToString();
|
||||||
|
string charName = $"{clientState.LocalPlayer.Name}@{worldName}";
|
||||||
|
log.Information("[QuestDB] Logging final quest status before logout: " + charName);
|
||||||
|
ScanCurrentCharacterQuestStatus();
|
||||||
|
log.Information("[QuestDB] Final quest state saved for: " + charName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, bool> PerformPreRotationCheck(uint stopQuestId, List<string> characters)
|
||||||
|
{
|
||||||
|
log.Information("[QuestPreCheck] === STARTING PRE-ROTATION QUEST VERIFICATION ===");
|
||||||
|
log.Information($"[QuestPreCheck] Checking {characters.Count} characters for quest {stopQuestId}...");
|
||||||
|
preCheckResults.Clear();
|
||||||
|
foreach (string character in characters)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (questDatabase.ContainsKey(character) && questDatabase[character].ContainsKey(stopQuestId))
|
||||||
|
{
|
||||||
|
bool isCompleted = questDatabase[character][stopQuestId];
|
||||||
|
preCheckResults[character] = isCompleted;
|
||||||
|
string status = (isCompleted ? "✓ COMPLETED" : "○ PENDING");
|
||||||
|
log.Information($"[QuestPreCheck] {character}: {status} (from database)");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Debug("[QuestPreCheck] " + character + ": Not in database, will check during rotation");
|
||||||
|
preCheckResults[character] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[QuestPreCheck] Error checking " + character + ": " + ex.Message);
|
||||||
|
preCheckResults[character] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Information("[QuestPreCheck] === PRE-ROTATION CHECK COMPLETE ===");
|
||||||
|
return preCheckResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShouldSkipCharacter(string characterName, uint questId)
|
||||||
|
{
|
||||||
|
if (preCheckResults.TryGetValue(characterName, out var isCompleted) && isCompleted)
|
||||||
|
{
|
||||||
|
log.Information($"[QuestPreCheck] Character {characterName} already completed quest {questId} - SKIPPING");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool completed = default(bool);
|
||||||
|
if (questDatabase.TryGetValue(characterName, out Dictionary<uint, bool> quests) && quests.TryGetValue(questId, out completed) && completed)
|
||||||
|
{
|
||||||
|
log.Information($"[QuestPreCheck] Character {characterName} already completed quest {questId} (from DB) - SKIPPING");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool? GetQuestStatus(string characterName, uint questId)
|
||||||
|
{
|
||||||
|
if (questDatabase.TryGetValue(characterName, out Dictionary<uint, bool> quests) && quests.TryGetValue(questId, out var isCompleted))
|
||||||
|
{
|
||||||
|
return isCompleted;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<uint> GetCompletedQuests(string characterName)
|
||||||
|
{
|
||||||
|
if (!questDatabase.TryGetValue(characterName, out Dictionary<uint, bool> quests))
|
||||||
|
{
|
||||||
|
return new List<uint>();
|
||||||
|
}
|
||||||
|
return (from kvp in quests
|
||||||
|
where kvp.Value
|
||||||
|
select kvp.Key).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MarkQuestCompleted(string characterName, uint questId)
|
||||||
|
{
|
||||||
|
if (!questDatabase.ContainsKey(characterName))
|
||||||
|
{
|
||||||
|
questDatabase[characterName] = new Dictionary<uint, bool>();
|
||||||
|
}
|
||||||
|
questDatabase[characterName][questId] = true;
|
||||||
|
SaveQuestDatabase();
|
||||||
|
log.Information($"[QuestPreCheck] Marked quest {questId} as completed for {characterName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearPreCheckResults()
|
||||||
|
{
|
||||||
|
preCheckResults.Clear();
|
||||||
|
log.Information("[QuestPreCheck] Pre-check results cleared");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
SaveQuestDatabase();
|
||||||
|
log.Information("[QuestPreCheck] Service disposed");
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,87 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class QuestTrackingService : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private readonly Dictionary<string, HashSet<uint>> characterQuestCache = new Dictionary<string, HashSet<uint>>();
|
||||||
|
|
||||||
|
public QuestTrackingService(IPluginLog log)
|
||||||
|
{
|
||||||
|
this.log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HashSet<uint> GetCharacterCompletedQuests(string characterName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(characterName))
|
||||||
|
{
|
||||||
|
return new HashSet<uint>();
|
||||||
|
}
|
||||||
|
if (characterQuestCache.TryGetValue(characterName, out HashSet<uint> quests))
|
||||||
|
{
|
||||||
|
return quests;
|
||||||
|
}
|
||||||
|
return new HashSet<uint>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateCurrentCharacterQuests(string characterName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(characterName))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HashSet<uint> completedQuests = new HashSet<uint>();
|
||||||
|
for (uint questId = 66000u; questId < 72000; questId++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (QuestManager.IsQuestComplete(questId))
|
||||||
|
{
|
||||||
|
completedQuests.Add(questId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
characterQuestCache[characterName] = completedQuests;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[QuestTracking] Failed to update quests for " + characterName + ": " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsQuestCompleted(string characterName, uint questId)
|
||||||
|
{
|
||||||
|
return GetCharacterCompletedQuests(characterName).Contains(questId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearCharacterCache(string characterName)
|
||||||
|
{
|
||||||
|
if (characterQuestCache.ContainsKey(characterName))
|
||||||
|
{
|
||||||
|
characterQuestCache.Remove(characterName);
|
||||||
|
log.Debug("[QuestTracking] Cleared cache for " + characterName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearAllCache()
|
||||||
|
{
|
||||||
|
characterQuestCache.Clear();
|
||||||
|
log.Information("[QuestTracking] Cleared all cached quest data");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
characterQuestCache.Clear();
|
||||||
|
log.Information("[QuestTracking] Service disposed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,959 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Dalamud.Plugin.Ipc;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class QuestionableIPC : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IDalamudPluginInterface pluginInterface;
|
||||||
|
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<string, bool>? importQuestPrioritySubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<string?>? getCurrentQuestIdSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<StepData?>? getCurrentStepDataSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<bool>? isRunningSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<object?>? getCurrentTaskSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<string, bool>? isQuestCompleteSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<string, bool>? isReadyToAcceptQuestSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<string, bool>? addQuestPrioritySubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<bool>? clearQuestPrioritySubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<List<string>>? getCurrentlyActiveEventQuestsSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<int>? getAlliedSocietyRemainingAllowancesSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<byte, List<string>>? getAlliedSocietyAvailableQuestIdsSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<Dictionary<byte, int>>? getAlliedSocietyAllAvailableQuestCountsSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<byte, bool>? getAlliedSocietyIsMaxRankSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<byte, int>? getAlliedSocietyCurrentRankSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<List<byte>>? getAlliedSocietiesWithAvailableQuestsSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<byte, int>? addAlliedSocietyOptimalQuestsSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<byte, List<string>>? getAlliedSocietyOptimalQuestsSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<long>? getAlliedSocietyTimeUntilResetSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<bool>? getStopConditionsEnabledSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<List<string>>? getStopQuestListSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<StopConditionData>? getLevelStopConditionSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<StopConditionData>? getSequenceStopConditionSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<string, uint, int, object?>? getQuestSequenceStopConditionSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<string, bool>? removeQuestSequenceStopConditionSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<Dictionary<string, object>>? getAllQuestSequenceStopConditionsSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<int>? getDefaultDutyModeSubscriber;
|
||||||
|
|
||||||
|
private ICallGateSubscriber<int, bool>? setDefaultDutyModeSubscriber;
|
||||||
|
|
||||||
|
private bool subscribersInitialized;
|
||||||
|
|
||||||
|
private DateTime lastAvailabilityCheck = DateTime.MinValue;
|
||||||
|
|
||||||
|
private const int AvailabilityCheckCooldownSeconds = 5;
|
||||||
|
|
||||||
|
public bool IsAvailable { get; private set; }
|
||||||
|
|
||||||
|
public QuestionableIPC(IDalamudPluginInterface pluginInterface, IPluginLog log)
|
||||||
|
{
|
||||||
|
this.pluginInterface = pluginInterface;
|
||||||
|
this.log = log;
|
||||||
|
InitializeIPC();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeIPC()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
getCurrentQuestIdSubscriber = pluginInterface.GetIpcSubscriber<string>("Questionable.GetCurrentQuestId");
|
||||||
|
getCurrentStepDataSubscriber = pluginInterface.GetIpcSubscriber<StepData>("Questionable.GetCurrentStepData");
|
||||||
|
isRunningSubscriber = pluginInterface.GetIpcSubscriber<bool>("Questionable.IsRunning");
|
||||||
|
importQuestPrioritySubscriber = pluginInterface.GetIpcSubscriber<string, bool>("Questionable.ImportQuestPriority");
|
||||||
|
getCurrentTaskSubscriber = pluginInterface.GetIpcSubscriber<object>("Questionable.GetCurrentTask");
|
||||||
|
isQuestCompleteSubscriber = pluginInterface.GetIpcSubscriber<string, bool>("Questionable.IsQuestComplete");
|
||||||
|
isReadyToAcceptQuestSubscriber = pluginInterface.GetIpcSubscriber<string, bool>("Questionable.IsReadyToAcceptQuest");
|
||||||
|
addQuestPrioritySubscriber = pluginInterface.GetIpcSubscriber<string, bool>("Questionable.AddQuestPriority");
|
||||||
|
clearQuestPrioritySubscriber = pluginInterface.GetIpcSubscriber<bool>("Questionable.ClearQuestPriority");
|
||||||
|
getCurrentlyActiveEventQuestsSubscriber = pluginInterface.GetIpcSubscriber<List<string>>("Questionable.GetCurrentlyActiveEventQuests");
|
||||||
|
getAlliedSocietyRemainingAllowancesSubscriber = pluginInterface.GetIpcSubscriber<int>("Questionable.AlliedSociety.GetRemainingAllowances");
|
||||||
|
getAlliedSocietyAvailableQuestIdsSubscriber = pluginInterface.GetIpcSubscriber<byte, List<string>>("Questionable.AlliedSociety.GetAvailableQuestIds");
|
||||||
|
getAlliedSocietyAllAvailableQuestCountsSubscriber = pluginInterface.GetIpcSubscriber<Dictionary<byte, int>>("Questionable.AlliedSociety.GetAllAvailableQuestCounts");
|
||||||
|
getAlliedSocietyIsMaxRankSubscriber = pluginInterface.GetIpcSubscriber<byte, bool>("Questionable.AlliedSociety.IsMaxRank");
|
||||||
|
getAlliedSocietyCurrentRankSubscriber = pluginInterface.GetIpcSubscriber<byte, int>("Questionable.AlliedSociety.GetCurrentRank");
|
||||||
|
getAlliedSocietiesWithAvailableQuestsSubscriber = pluginInterface.GetIpcSubscriber<List<byte>>("Questionable.AlliedSociety.GetSocietiesWithAvailableQuests");
|
||||||
|
addAlliedSocietyOptimalQuestsSubscriber = pluginInterface.GetIpcSubscriber<byte, int>("Questionable.AlliedSociety.AddOptimalQuests");
|
||||||
|
getAlliedSocietyOptimalQuestsSubscriber = pluginInterface.GetIpcSubscriber<byte, List<string>>("Questionable.AlliedSociety.GetOptimalQuests");
|
||||||
|
getAlliedSocietyTimeUntilResetSubscriber = pluginInterface.GetIpcSubscriber<long>("Questionable.AlliedSociety.GetTimeUntilReset");
|
||||||
|
getStopConditionsEnabledSubscriber = pluginInterface.GetIpcSubscriber<bool>("Questionable.GetStopConditionsEnabled");
|
||||||
|
getStopQuestListSubscriber = pluginInterface.GetIpcSubscriber<List<string>>("Questionable.GetStopQuestList");
|
||||||
|
getLevelStopConditionSubscriber = pluginInterface.GetIpcSubscriber<StopConditionData>("Questionable.GetLevelStopCondition");
|
||||||
|
getSequenceStopConditionSubscriber = pluginInterface.GetIpcSubscriber<StopConditionData>("Questionable.GetSequenceStopCondition");
|
||||||
|
getQuestSequenceStopConditionSubscriber = pluginInterface.GetIpcSubscriber<string, uint, int, object>("Questionable.GetQuestSequenceStopCondition");
|
||||||
|
removeQuestSequenceStopConditionSubscriber = pluginInterface.GetIpcSubscriber<string, bool>("Questionable.RemoveQuestSequenceStopCondition");
|
||||||
|
getAllQuestSequenceStopConditionsSubscriber = pluginInterface.GetIpcSubscriber<Dictionary<string, object>>("Questionable.GetAllQuestSequenceStopConditions");
|
||||||
|
getDefaultDutyModeSubscriber = pluginInterface.GetIpcSubscriber<int>("Questionable.GetDefaultDutyMode");
|
||||||
|
setDefaultDutyModeSubscriber = pluginInterface.GetIpcSubscriber<int, bool>("Questionable.SetDefaultDutyMode");
|
||||||
|
subscribersInitialized = true;
|
||||||
|
log.Debug("[QuestionableIPC] IPC subscribers initialized (lazy-loading enabled)");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
IsAvailable = false;
|
||||||
|
subscribersInitialized = false;
|
||||||
|
log.Error("[QuestionableIPC] Failed to initialize subscribers: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryEnsureAvailable()
|
||||||
|
{
|
||||||
|
if (IsAvailable)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!subscribersInitialized)
|
||||||
|
{
|
||||||
|
log.Warning("[QuestionableIPC] Subscribers not initialized!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
DateTime now = DateTime.Now;
|
||||||
|
if ((now - lastAvailabilityCheck).TotalSeconds < 5.0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
lastAvailabilityCheck = now;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (isRunningSubscriber == null)
|
||||||
|
{
|
||||||
|
log.Error("[QuestionableIPC] isRunningSubscriber is NULL!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
isRunningSubscriber.InvokeFunc();
|
||||||
|
if (!IsAvailable)
|
||||||
|
{
|
||||||
|
IsAvailable = true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[QuestionableIPC] Failed to connect to Questionable:");
|
||||||
|
log.Error("[QuestionableIPC] Exception Type: " + ex.GetType().Name);
|
||||||
|
log.Error("[QuestionableIPC] Message: " + ex.Message);
|
||||||
|
log.Error("[QuestionableIPC] Stack Trace: " + ex.StackTrace);
|
||||||
|
IsAvailable = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ForceCheckAvailability()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!subscribersInitialized)
|
||||||
|
{
|
||||||
|
log.Error("[QuestionableIPC] Subscribers not initialized!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isRunningSubscriber == null)
|
||||||
|
{
|
||||||
|
log.Error("[QuestionableIPC] isRunningSubscriber is NULL!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log.Information("[QuestionableIPC] Force checking Questionable availability...");
|
||||||
|
log.Information("[QuestionableIPC] Attempting to call Questionable.IsRunning...");
|
||||||
|
bool testRunning = isRunningSubscriber.InvokeFunc();
|
||||||
|
log.Information($"[QuestionableIPC] SUCCESS! Questionable.IsRunning returned: {testRunning}");
|
||||||
|
IsAvailable = true;
|
||||||
|
lastAvailabilityCheck = DateTime.Now;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[QuestionableIPC] Failed to connect to Questionable:");
|
||||||
|
log.Error("[QuestionableIPC] Exception Type: " + ex.GetType().Name);
|
||||||
|
log.Error("[QuestionableIPC] Message: " + ex.Message);
|
||||||
|
log.Error("[QuestionableIPC] Stack Trace: " + ex.StackTrace);
|
||||||
|
IsAvailable = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryEnsureAvailableSilent()
|
||||||
|
{
|
||||||
|
if (IsAvailable)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!subscribersInitialized)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
lastAvailabilityCheck = DateTime.MinValue;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (isRunningSubscriber == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
isRunningSubscriber.InvokeFunc();
|
||||||
|
if (!IsAvailable)
|
||||||
|
{
|
||||||
|
IsAvailable = true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
IsAvailable = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? GetCurrentQuestId()
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || getCurrentQuestIdSubscriber == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return getCurrentQuestIdSubscriber.InvokeFunc();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Debug("[QuestionableIPC] GetCurrentQuestId failed: " + ex.Message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public StepData? GetCurrentStepData()
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || getCurrentStepDataSubscriber == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return getCurrentStepDataSubscriber.InvokeFunc();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Debug("[QuestionableIPC] GetCurrentStepData failed: " + ex.Message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte? GetCurrentSequence()
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || getCurrentStepDataSubscriber == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return getCurrentStepDataSubscriber.InvokeFunc()?.Sequence;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Debug("[QuestionableIPC] GetCurrentSequence failed: " + ex.Message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsRunning()
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (isRunningSubscriber == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool result = isRunningSubscriber.InvokeFunc();
|
||||||
|
if (!IsAvailable)
|
||||||
|
{
|
||||||
|
IsAvailable = true;
|
||||||
|
log.Information("[QuestionableIPC] Questionable is now available");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (IsAvailable)
|
||||||
|
{
|
||||||
|
IsAvailable = false;
|
||||||
|
log.Warning("[QuestionableIPC] Questionable is no longer available: " + ex.Message);
|
||||||
|
}
|
||||||
|
log.Debug("[QuestionableIPC] IsRunning failed: " + ex.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? GetCurrentTask()
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (getCurrentTaskSubscriber == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
object? result = getCurrentTaskSubscriber.InvokeFunc();
|
||||||
|
if (!IsAvailable)
|
||||||
|
{
|
||||||
|
IsAvailable = true;
|
||||||
|
log.Information("[QuestionableIPC] Questionable is now available");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (IsAvailable)
|
||||||
|
{
|
||||||
|
IsAvailable = false;
|
||||||
|
log.Warning("[QuestionableIPC] Questionable is no longer available: " + ex.Message);
|
||||||
|
}
|
||||||
|
log.Debug("[QuestionableIPC] GetCurrentTask failed: " + ex.Message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Start()
|
||||||
|
{
|
||||||
|
log.Warning("[QuestionableIPC] Start() called - NOT AVAILABLE VIA IPC!");
|
||||||
|
log.Warning("[QuestionableIPC] Use /qst start command instead");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Stop()
|
||||||
|
{
|
||||||
|
log.Warning("[QuestionableIPC] Stop() called - NOT AVAILABLE VIA IPC!");
|
||||||
|
log.Warning("[QuestionableIPC] Use /qst stop command instead");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ImportQuestPriority(string base64QuestData)
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || importQuestPrioritySubscriber == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool result = importQuestPrioritySubscriber.InvokeFunc(base64QuestData);
|
||||||
|
log.Information($"[QuestionableIPC] Imported priority quest: {result}");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[QuestionableIPC] ImportQuestPriority failed: " + ex.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AddQuestPriority(string questId)
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || addQuestPrioritySubscriber == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool result = addQuestPrioritySubscriber.InvokeFunc(questId);
|
||||||
|
log.Debug($"[QuestionableIPC] Added quest {questId} to priority: {result}");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[QuestionableIPC] AddQuestPriority failed: " + ex.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ClearQuestPriority()
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || clearQuestPrioritySubscriber == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool result = clearQuestPrioritySubscriber.InvokeFunc();
|
||||||
|
log.Debug($"[QuestionableIPC] Cleared quest priority: {result}");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[QuestionableIPC] ClearQuestPriority failed: " + ex.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsQuestComplete(string questId)
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || isQuestCompleteSubscriber == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool result = isQuestCompleteSubscriber.InvokeFunc(questId);
|
||||||
|
log.Debug($"[QuestionableIPC] Quest {questId} complete: {result}");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[QuestionableIPC] IsQuestComplete failed: " + ex.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsReadyToAcceptQuest(string questId)
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || isReadyToAcceptQuestSubscriber == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool result = isReadyToAcceptQuestSubscriber.InvokeFunc(questId);
|
||||||
|
log.Debug($"[QuestionableIPC] Quest {questId} ready to accept: {result}");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[QuestionableIPC] IsReadyToAcceptQuest failed: " + ex.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AddQuestsToQueue(List<string> questIds)
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable)
|
||||||
|
{
|
||||||
|
log.Warning("[QuestionableIPC] Cannot add quests to queue - Questionable not available");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (questIds == null || questIds.Count == 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information($"[QuestionableIPC] Adding {questIds.Count} quests to priority queue");
|
||||||
|
foreach (string questId in questIds)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(questId))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool? result = addQuestPrioritySubscriber?.InvokeFunc(questId);
|
||||||
|
log.Debug($"[QuestionableIPC] Added quest {questId} to queue: {result}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Warning("[QuestionableIPC] Failed to add quest " + questId + " to queue: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Information("[QuestionableIPC] All quests added to priority queue");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
log.Error("[QuestionableIPC] Error adding quests to queue: " + ex2.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> GetCurrentlyActiveEventQuests()
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || getCurrentlyActiveEventQuestsSubscriber == null)
|
||||||
|
{
|
||||||
|
log.Warning("[QuestionableIPC] Cannot get active event quests - Questionable not available");
|
||||||
|
return new List<string>();
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<string> eventQuests = getCurrentlyActiveEventQuestsSubscriber.InvokeFunc();
|
||||||
|
log.Debug($"[QuestionableIPC] Retrieved {eventQuests?.Count ?? 0} active event quests");
|
||||||
|
return eventQuests ?? new List<string>();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[QuestionableIPC] Error getting active event quests: " + ex.Message);
|
||||||
|
return new List<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetAlliedSocietyRemainingAllowances()
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || getAlliedSocietyRemainingAllowancesSubscriber == null)
|
||||||
|
{
|
||||||
|
log.Debug("[AlliedSociety] Cannot get remaining allowances - Questionable not available");
|
||||||
|
return 12;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int result = getAlliedSocietyRemainingAllowancesSubscriber.InvokeFunc();
|
||||||
|
log.Debug($"[AlliedSociety] Remaining allowances: {result}");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[AlliedSociety] Error getting remaining allowances: " + ex.Message);
|
||||||
|
return 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> GetAlliedSocietyAvailableQuestIds(byte societyId)
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || getAlliedSocietyAvailableQuestIdsSubscriber == null)
|
||||||
|
{
|
||||||
|
log.Debug($"[AlliedSociety] Cannot get quest IDs for society {societyId} - Questionable not available");
|
||||||
|
return new List<string>();
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<string> result = getAlliedSocietyAvailableQuestIdsSubscriber.InvokeFunc(societyId);
|
||||||
|
log.Debug($"[AlliedSociety] Society {societyId} has {result?.Count ?? 0} available quests");
|
||||||
|
return result ?? new List<string>();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error($"[AlliedSociety] Error getting quest IDs for society {societyId}: {ex.Message}");
|
||||||
|
return new List<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<byte, int> GetAlliedSocietyAllAvailableQuestCounts()
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || getAlliedSocietyAllAvailableQuestCountsSubscriber == null)
|
||||||
|
{
|
||||||
|
log.Debug("[AlliedSociety] Cannot get quest counts - Questionable not available");
|
||||||
|
return new Dictionary<byte, int>();
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Dictionary<byte, int> result = getAlliedSocietyAllAvailableQuestCountsSubscriber.InvokeFunc();
|
||||||
|
log.Debug($"[AlliedSociety] Found {result?.Count ?? 0} societies with available quests");
|
||||||
|
return result ?? new Dictionary<byte, int>();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[AlliedSociety] Error getting quest counts: " + ex.Message);
|
||||||
|
return new Dictionary<byte, int>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool GetAlliedSocietyIsMaxRank(byte societyId)
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || getAlliedSocietyIsMaxRankSubscriber == null)
|
||||||
|
{
|
||||||
|
log.Debug($"[AlliedSociety] Cannot check max rank for society {societyId} - Questionable not available");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool result = getAlliedSocietyIsMaxRankSubscriber.InvokeFunc(societyId);
|
||||||
|
log.Debug($"[AlliedSociety] Society {societyId} max rank: {result}");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error($"[AlliedSociety] Error checking max rank for society {societyId}: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetAlliedSocietyCurrentRank(byte societyId)
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || getAlliedSocietyCurrentRankSubscriber == null)
|
||||||
|
{
|
||||||
|
log.Debug($"[AlliedSociety] Cannot get rank for society {societyId} - Questionable not available");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int result = getAlliedSocietyCurrentRankSubscriber.InvokeFunc(societyId);
|
||||||
|
log.Debug($"[AlliedSociety] Society {societyId} current rank: {result}");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error($"[AlliedSociety] Error getting rank for society {societyId}: {ex.Message}");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<byte> GetAlliedSocietiesWithAvailableQuests()
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || getAlliedSocietiesWithAvailableQuestsSubscriber == null)
|
||||||
|
{
|
||||||
|
log.Debug("[AlliedSociety] Cannot get societies with quests - Questionable not available");
|
||||||
|
return new List<byte>();
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<byte> result = getAlliedSocietiesWithAvailableQuestsSubscriber.InvokeFunc();
|
||||||
|
log.Debug($"[AlliedSociety] Found {result?.Count ?? 0} societies with available quests");
|
||||||
|
return result ?? new List<byte>();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[AlliedSociety] Error getting societies with quests: " + ex.Message);
|
||||||
|
return new List<byte>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int AddAlliedSocietyOptimalQuests(byte societyId)
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || addAlliedSocietyOptimalQuestsSubscriber == null)
|
||||||
|
{
|
||||||
|
log.Debug($"[AlliedSociety] Cannot add optimal quests for society {societyId} - Questionable not available");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int result = addAlliedSocietyOptimalQuestsSubscriber.InvokeFunc(societyId);
|
||||||
|
log.Information($"[AlliedSociety] Added {result} optimal quests for society {societyId}");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error($"[AlliedSociety] Error adding optimal quests for society {societyId}: {ex.Message}");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> GetAlliedSocietyOptimalQuests(byte societyId)
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || getAlliedSocietyOptimalQuestsSubscriber == null)
|
||||||
|
{
|
||||||
|
log.Debug($"[AlliedSociety] Cannot get optimal quests for society {societyId} - Questionable not available");
|
||||||
|
return new List<string>();
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<string> result = getAlliedSocietyOptimalQuestsSubscriber.InvokeFunc(societyId);
|
||||||
|
log.Debug($"[AlliedSociety] Found {result?.Count ?? 0} optimal quests for society {societyId}");
|
||||||
|
return result ?? new List<string>();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error($"[AlliedSociety] Error getting optimal quests for society {societyId}: {ex.Message}");
|
||||||
|
return new List<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeSpan GetAlliedSocietyTimeUntilReset()
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || getAlliedSocietyTimeUntilResetSubscriber == null)
|
||||||
|
{
|
||||||
|
log.Debug("[AlliedSociety] Cannot get time until reset - Questionable not available");
|
||||||
|
return TimeSpan.Zero;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
TimeSpan timeSpan = TimeSpan.FromTicks(getAlliedSocietyTimeUntilResetSubscriber.InvokeFunc());
|
||||||
|
log.Debug($"[AlliedSociety] Time until reset: {timeSpan}");
|
||||||
|
return timeSpan;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[AlliedSociety] Error getting time until reset: " + ex.Message);
|
||||||
|
return TimeSpan.Zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool GetStopConditionsEnabled()
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || getStopConditionsEnabledSubscriber == null)
|
||||||
|
{
|
||||||
|
log.Debug("[StopCondition] Cannot get stop conditions enabled - Questionable not available");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool result = getStopConditionsEnabledSubscriber.InvokeFunc();
|
||||||
|
log.Debug($"[StopCondition] Stop conditions enabled: {result}");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[StopCondition] Error getting stop conditions enabled: " + ex.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> GetStopQuestList()
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || getStopQuestListSubscriber == null)
|
||||||
|
{
|
||||||
|
return new List<string>();
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<string> result = getStopQuestListSubscriber.InvokeFunc();
|
||||||
|
log.Debug($"[StopCondition] Found {result?.Count ?? 0} stop quests");
|
||||||
|
return result ?? new List<string>();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[StopCondition] Error getting stop quest list: " + ex.Message);
|
||||||
|
return new List<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public StopConditionData? GetLevelStopCondition()
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || getLevelStopConditionSubscriber == null)
|
||||||
|
{
|
||||||
|
log.Debug("[StopCondition] Cannot get level stop condition - Questionable not available");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return getLevelStopConditionSubscriber.InvokeFunc();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[StopCondition] Error getting level stop condition: " + ex.Message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public StopConditionData? GetSequenceStopCondition()
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || getSequenceStopConditionSubscriber == null)
|
||||||
|
{
|
||||||
|
log.Debug("[StopCondition] Cannot get sequence stop condition - Questionable not available");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
StopConditionData result = getSequenceStopConditionSubscriber.InvokeFunc();
|
||||||
|
log.Debug($"[StopCondition] Sequence stop condition - Enabled: {result?.Enabled}, Target: {result?.TargetValue}");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[StopCondition] Error getting sequence stop condition: " + ex.Message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? GetQuestSequenceStopCondition(string questId, uint sequence, int step)
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || getQuestSequenceStopConditionSubscriber == null)
|
||||||
|
{
|
||||||
|
log.Warning("[StopCondition] Cannot get quest sequence stop condition - Questionable not available");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
object result = getQuestSequenceStopConditionSubscriber.InvokeFunc(questId, sequence, step);
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
log.Information($"[StopCondition] No quest sequence stop condition found for {questId} at {sequence}-{step}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Information($"[StopCondition] Quest sequence stop condition for {questId} at {sequence}-{step}: {result}");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[StopCondition] Error getting quest sequence stop condition: " + ex.Message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RemoveQuestSequenceStopCondition(string questId)
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || removeQuestSequenceStopConditionSubscriber == null)
|
||||||
|
{
|
||||||
|
log.Warning("[StopCondition] Cannot remove quest sequence stop condition - Questionable not available");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool num = removeQuestSequenceStopConditionSubscriber.InvokeFunc(questId);
|
||||||
|
if (num)
|
||||||
|
{
|
||||||
|
log.Information("[StopCondition] ✓ Removed quest sequence stop condition for " + questId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
log.Warning("[StopCondition] ✗ No quest sequence stop condition to remove for " + questId + " (or removal failed)");
|
||||||
|
}
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[StopCondition] Error removing quest sequence stop condition: " + ex.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, object> GetAllQuestSequenceStopConditions()
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || getAllQuestSequenceStopConditionsSubscriber == null)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, object>();
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Dictionary<string, object> result = getAllQuestSequenceStopConditionsSubscriber.InvokeFunc();
|
||||||
|
if (result == null || result.Count == 0)
|
||||||
|
{
|
||||||
|
log.Information("[StopCondition] No quest sequence stop conditions configured (empty or null result)");
|
||||||
|
return new Dictionary<string, object>();
|
||||||
|
}
|
||||||
|
log.Information($"[StopCondition] Found {result.Count} quest sequence stop condition(s)");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[StopCondition] Error getting all quest sequence stop conditions: " + ex.Message);
|
||||||
|
return new Dictionary<string, object>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetDefaultDutyMode()
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || getDefaultDutyModeSubscriber == null)
|
||||||
|
{
|
||||||
|
log.Debug("[QuestionableIPC] Cannot get default duty mode - Questionable not available");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int result = getDefaultDutyModeSubscriber.InvokeFunc();
|
||||||
|
log.Debug($"[QuestionableIPC] Default Duty Mode: {result}");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[QuestionableIPC] GetDefaultDutyMode failed: " + ex.Message);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SetDefaultDutyMode(int dutyMode)
|
||||||
|
{
|
||||||
|
TryEnsureAvailable();
|
||||||
|
if (!IsAvailable || setDefaultDutyModeSubscriber == null)
|
||||||
|
{
|
||||||
|
log.Debug("[QuestionableIPC] Cannot set default duty mode - Questionable not available");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool result = setDefaultDutyModeSubscriber.InvokeFunc(dutyMode);
|
||||||
|
log.Information($"[QuestionableIPC] Set Default Duty Mode to {dutyMode}: {result}");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[QuestionableIPC] SetDefaultDutyMode failed: " + ex.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ValidateFeatureCompatibility()
|
||||||
|
{
|
||||||
|
if (!IsAvailable)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (getAllQuestSequenceStopConditionsSubscriber == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (getLevelStopConditionSubscriber == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (getAlliedSocietyOptimalQuestsSubscriber == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (getDefaultDutyModeSubscriber == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
getAllQuestSequenceStopConditionsSubscriber.InvokeFunc();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
IsAvailable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class StepData
|
||||||
|
{
|
||||||
|
public required string QuestId { get; init; }
|
||||||
|
|
||||||
|
public required byte Sequence { get; init; }
|
||||||
|
|
||||||
|
public required byte Step { get; init; }
|
||||||
|
|
||||||
|
public required string InteractionType { get; init; }
|
||||||
|
|
||||||
|
public Vector3? Position { get; init; }
|
||||||
|
|
||||||
|
public ushort TerritoryId { get; init; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using Dalamud.Game.ClientState.Conditions;
|
||||||
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class StepsOfFaithHandler : IDisposable
|
||||||
|
{
|
||||||
|
private readonly ICondition condition;
|
||||||
|
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private readonly IClientState clientState;
|
||||||
|
|
||||||
|
private readonly ICommandManager commandManager;
|
||||||
|
|
||||||
|
private readonly IFramework framework;
|
||||||
|
|
||||||
|
private readonly Configuration config;
|
||||||
|
|
||||||
|
private bool isActive;
|
||||||
|
|
||||||
|
private readonly Dictionary<string, bool> characterHandledStatus = new Dictionary<string, bool>();
|
||||||
|
|
||||||
|
private const uint StepsOfFaithQuestId = 4591u;
|
||||||
|
|
||||||
|
public bool IsActive => isActive;
|
||||||
|
|
||||||
|
public bool IsStepsOfFaithQuest(uint questId)
|
||||||
|
{
|
||||||
|
return questId == 4591;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StepsOfFaithHandler(ICondition condition, IPluginLog log, IClientState clientState, ICommandManager commandManager, IFramework framework, Configuration config)
|
||||||
|
{
|
||||||
|
this.condition = condition;
|
||||||
|
this.log = log;
|
||||||
|
this.clientState = clientState;
|
||||||
|
this.commandManager = commandManager;
|
||||||
|
this.framework = framework;
|
||||||
|
this.config = config;
|
||||||
|
log.Information("[StepsOfFaith] Handler initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShouldActivate(uint questId, bool isInSoloDuty)
|
||||||
|
{
|
||||||
|
if (!config.EnableAutoDutyUnsynced)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isActive)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (questId != 4591)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!isInSoloDuty)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
string characterName = GetCurrentCharacterName();
|
||||||
|
if (string.IsNullOrEmpty(characterName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (characterHandledStatus.GetValueOrDefault(characterName, defaultValue: false))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute(string characterName)
|
||||||
|
{
|
||||||
|
isActive = true;
|
||||||
|
if (!string.IsNullOrEmpty(characterName))
|
||||||
|
{
|
||||||
|
characterHandledStatus[characterName] = true;
|
||||||
|
log.Information("[StepsOfFaith] Marked " + characterName + " as handled");
|
||||||
|
}
|
||||||
|
log.Information("[StepsOfFaith] ========================================");
|
||||||
|
log.Information("[StepsOfFaith] === STEPS OF FAITH HANDLER ACTIVATED ===");
|
||||||
|
log.Information("[StepsOfFaith] ========================================");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
log.Information("[StepsOfFaith] Waiting for conditions to clear...");
|
||||||
|
DateTime startTime = DateTime.Now;
|
||||||
|
TimeSpan maxWaitTime = TimeSpan.FromSeconds(6000L);
|
||||||
|
while (DateTime.Now - startTime < maxWaitTime)
|
||||||
|
{
|
||||||
|
bool hasCondition29 = condition[ConditionFlag.Occupied];
|
||||||
|
bool hasCondition63 = condition[ConditionFlag.SufferingStatusAffliction63];
|
||||||
|
if (!hasCondition29 && !hasCondition63)
|
||||||
|
{
|
||||||
|
log.Information("[StepsOfFaith] Conditions cleared!");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ((DateTime.Now - startTime).TotalSeconds % 5.0 < 0.1)
|
||||||
|
{
|
||||||
|
log.Information($"[StepsOfFaith] Waiting... (29: {hasCondition29}, 63: {hasCondition63})");
|
||||||
|
}
|
||||||
|
Thread.Sleep(200);
|
||||||
|
}
|
||||||
|
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
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/vnav moveto 2.8788917064667 0.0 293.36273193359");
|
||||||
|
});
|
||||||
|
log.Information("[StepsOfFaith] Enabling combat commands...");
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/rsr auto");
|
||||||
|
Thread.Sleep(100);
|
||||||
|
commandManager.ProcessCommand("/vbmai on");
|
||||||
|
Thread.Sleep(100);
|
||||||
|
commandManager.ProcessCommand("/bmrai on");
|
||||||
|
});
|
||||||
|
log.Information("[StepsOfFaith] === HANDLER COMPLETE ===");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[StepsOfFaith] Error: " + ex.Message);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
isActive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
isActive = false;
|
||||||
|
log.Information("[StepsOfFaith] Active state reset (character completion status preserved)");
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetCurrentCharacterName()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IPlayerCharacter player = clientState.LocalPlayer;
|
||||||
|
if (player != null)
|
||||||
|
{
|
||||||
|
return $"{player.Name}@{player.HomeWorld.Value.Name}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[StepsOfFaith] Failed to get character name: " + ex.Message);
|
||||||
|
}
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class StopConditionData
|
||||||
|
{
|
||||||
|
public required bool Enabled { get; init; }
|
||||||
|
|
||||||
|
public required int TargetValue { get; init; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,388 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class SubmarineManager : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IPluginLog log;
|
||||||
|
|
||||||
|
private readonly AutoRetainerIPC autoRetainerIPC;
|
||||||
|
|
||||||
|
private readonly Configuration config;
|
||||||
|
|
||||||
|
private readonly ICommandManager? commandManager;
|
||||||
|
|
||||||
|
private readonly IFramework? framework;
|
||||||
|
|
||||||
|
private DateTime lastSubmarineCheck = DateTime.MinValue;
|
||||||
|
|
||||||
|
private DateTime submarineReloginCooldownEnd = DateTime.MinValue;
|
||||||
|
|
||||||
|
private DateTime submarineNoAvailableWaitEnd = DateTime.MinValue;
|
||||||
|
|
||||||
|
private bool submarinesPaused;
|
||||||
|
|
||||||
|
private bool submarinesWaitingForSeq0;
|
||||||
|
|
||||||
|
private bool submarineReloginInProgress;
|
||||||
|
|
||||||
|
private bool submarineJustCompleted;
|
||||||
|
|
||||||
|
private string? originalCharacterForSubmarines;
|
||||||
|
|
||||||
|
public bool IsSubmarinePaused => submarinesPaused;
|
||||||
|
|
||||||
|
public bool IsWaitingForSequence0 => submarinesWaitingForSeq0;
|
||||||
|
|
||||||
|
public bool IsReloginInProgress => submarineReloginInProgress;
|
||||||
|
|
||||||
|
public bool IsSubmarineJustCompleted => submarineJustCompleted;
|
||||||
|
|
||||||
|
public SubmarineManager(IPluginLog log, AutoRetainerIPC autoRetainerIPC, Configuration config, ICommandManager? commandManager = null, IFramework? framework = null)
|
||||||
|
{
|
||||||
|
this.log = log;
|
||||||
|
this.autoRetainerIPC = autoRetainerIPC;
|
||||||
|
this.config = config;
|
||||||
|
this.commandManager = commandManager;
|
||||||
|
this.framework = framework;
|
||||||
|
log.Information("[SubmarineManager] Service initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? GetConfigPath()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string userProfile = Environment.GetEnvironmentVariable("USERPROFILE");
|
||||||
|
if (string.IsNullOrEmpty(userProfile))
|
||||||
|
{
|
||||||
|
string username = Environment.GetEnvironmentVariable("USERNAME");
|
||||||
|
if (string.IsNullOrEmpty(username))
|
||||||
|
{
|
||||||
|
log.Warning("[SubmarineManager] Could not resolve USERPROFILE or USERNAME");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
userProfile = "C:\\Users\\" + username;
|
||||||
|
}
|
||||||
|
_003C_003Ey__InlineArray7<string> buffer = default(_003C_003Ey__InlineArray7<string>);
|
||||||
|
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<_003C_003Ey__InlineArray7<string>, string>(ref buffer, 0) = userProfile;
|
||||||
|
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<_003C_003Ey__InlineArray7<string>, string>(ref buffer, 1) = "AppData";
|
||||||
|
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<_003C_003Ey__InlineArray7<string>, string>(ref buffer, 2) = "Roaming";
|
||||||
|
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<_003C_003Ey__InlineArray7<string>, string>(ref buffer, 3) = "XIVLauncher";
|
||||||
|
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<_003C_003Ey__InlineArray7<string>, string>(ref buffer, 4) = "pluginConfigs";
|
||||||
|
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<_003C_003Ey__InlineArray7<string>, string>(ref buffer, 5) = "AutoRetainer";
|
||||||
|
global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef<_003C_003Ey__InlineArray7<string>, string>(ref buffer, 6) = "DefaultConfig.json";
|
||||||
|
return Path.Combine(global::_003CPrivateImplementationDetails_003E.InlineArrayAsReadOnlySpan<_003C_003Ey__InlineArray7<string>, string>(in buffer, 7));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[SubmarineManager] Error resolving config path: " + ex.Message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CheckSubmarines()
|
||||||
|
{
|
||||||
|
if (!config.EnableSubmarineCheck)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
string configPath = GetConfigPath();
|
||||||
|
if (string.IsNullOrEmpty(configPath))
|
||||||
|
{
|
||||||
|
log.Warning("[SubmarineManager] Could not resolve config path");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!File.Exists(configPath))
|
||||||
|
{
|
||||||
|
log.Debug("[SubmarineManager] Config file not found: " + configPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string content = File.ReadAllText(configPath);
|
||||||
|
if (string.IsNullOrEmpty(content))
|
||||||
|
{
|
||||||
|
log.Warning("[SubmarineManager] Config file is empty");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
List<long> returnTimes = ParseReturnTimes(content);
|
||||||
|
if (returnTimes.Count == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
int available = 0;
|
||||||
|
long? minDelta = null;
|
||||||
|
foreach (long item in returnTimes)
|
||||||
|
{
|
||||||
|
long delta = item - now;
|
||||||
|
if (delta <= 0)
|
||||||
|
{
|
||||||
|
available++;
|
||||||
|
}
|
||||||
|
else if (!minDelta.HasValue || delta < minDelta.Value)
|
||||||
|
{
|
||||||
|
minDelta = delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (available > 0)
|
||||||
|
{
|
||||||
|
string plural = ((available == 1) ? "Sub" : "Subs");
|
||||||
|
log.Information($"[SubmarineManager] {available} {plural} available - pausing quest rotation!");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (minDelta.HasValue && minDelta.Value > 0)
|
||||||
|
{
|
||||||
|
int minutes = Math.Max(0, (int)Math.Ceiling((double)minDelta.Value / 60.0));
|
||||||
|
string plural2 = ((minutes == 1) ? "minute" : "minutes");
|
||||||
|
log.Debug($"[SubmarineManager] Next submarine in {minutes} {plural2}");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[SubmarineManager] Error checking submarines: " + ex.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CheckSubmarinesSoon()
|
||||||
|
{
|
||||||
|
if (!config.EnableSubmarineCheck)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
string configPath = GetConfigPath();
|
||||||
|
if (string.IsNullOrEmpty(configPath) || !File.Exists(configPath))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string content = File.ReadAllText(configPath);
|
||||||
|
if (string.IsNullOrEmpty(content))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
List<long> returnTimes = ParseReturnTimes(content);
|
||||||
|
if (returnTimes.Count == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
long? minDelta = null;
|
||||||
|
int availableNow = 0;
|
||||||
|
foreach (long item in returnTimes)
|
||||||
|
{
|
||||||
|
long delta = item - now;
|
||||||
|
if (delta <= 0)
|
||||||
|
{
|
||||||
|
availableNow++;
|
||||||
|
}
|
||||||
|
else if (delta <= 120 && (!minDelta.HasValue || delta < minDelta.Value))
|
||||||
|
{
|
||||||
|
minDelta = delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (availableNow > 0)
|
||||||
|
{
|
||||||
|
log.Debug($"[SubmarineManager] {availableNow} submarines ready NOW - continue Multi-Mode");
|
||||||
|
return 999;
|
||||||
|
}
|
||||||
|
if (minDelta.HasValue)
|
||||||
|
{
|
||||||
|
int minutes = (int)Math.Ceiling((double)minDelta.Value / 60.0);
|
||||||
|
log.Debug($"[SubmarineManager] Submarine will be ready in {minDelta.Value} seconds ({minutes} min) - waiting before character switch");
|
||||||
|
return (int)minDelta.Value;
|
||||||
|
}
|
||||||
|
if (submarineNoAvailableWaitEnd == DateTime.MinValue)
|
||||||
|
{
|
||||||
|
submarineNoAvailableWaitEnd = DateTime.Now.AddSeconds(60.0);
|
||||||
|
log.Information("[SubmarineManager] No submarines available - waiting 60 seconds before relog");
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
|
if (DateTime.Now < submarineNoAvailableWaitEnd)
|
||||||
|
{
|
||||||
|
int remaining = (int)(submarineNoAvailableWaitEnd - DateTime.Now).TotalSeconds;
|
||||||
|
log.Debug($"[SubmarineManager] Waiting {remaining}s before relog...");
|
||||||
|
return remaining;
|
||||||
|
}
|
||||||
|
submarineNoAvailableWaitEnd = DateTime.MinValue;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[SubmarineManager] Error checking submarines soon: " + ex.Message);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<long> ParseReturnTimes(string jsonContent)
|
||||||
|
{
|
||||||
|
List<long> returnTimes = new List<long>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
JObject json = JObject.Parse(jsonContent);
|
||||||
|
FindReturnTimes(json, returnTimes);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
string pattern = "\"ReturnTime\"\\s*:\\s*(\\d+)";
|
||||||
|
foreach (Match match in Regex.Matches(jsonContent, pattern))
|
||||||
|
{
|
||||||
|
if (match.Groups.Count > 1 && long.TryParse(match.Groups[1].Value, out var timestamp))
|
||||||
|
{
|
||||||
|
returnTimes.Add(timestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return returnTimes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FindReturnTimes(JToken token, List<long> returnTimes)
|
||||||
|
{
|
||||||
|
if (token is JObject obj)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
foreach (JProperty property in obj.Properties())
|
||||||
|
{
|
||||||
|
if (property.Name == "ReturnTime" && property.Value.Type == JTokenType.Integer)
|
||||||
|
{
|
||||||
|
returnTimes.Add(property.Value.Value<long>());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FindReturnTimes(property.Value, returnTimes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!(token is JArray array))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach (JToken item in array)
|
||||||
|
{
|
||||||
|
FindReturnTimes(item, returnTimes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartSubmarineWait(string currentCharacter)
|
||||||
|
{
|
||||||
|
submarinesWaitingForSeq0 = true;
|
||||||
|
originalCharacterForSubmarines = currentCharacter;
|
||||||
|
log.Information("[SubmarineManager] Waiting for Sequence 0 completion before enabling Multi-Mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EnableMultiMode()
|
||||||
|
{
|
||||||
|
if (!autoRetainerIPC.IsAvailable)
|
||||||
|
{
|
||||||
|
log.Warning("[SubmarineManager] AutoRetainer not available - cannot enable Multi-Mode");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (autoRetainerIPC.GetMultiModeEnabled())
|
||||||
|
{
|
||||||
|
log.Information("[SubmarineManager] Multi-Mode is already enabled - skipping activation");
|
||||||
|
submarinesPaused = true;
|
||||||
|
submarinesWaitingForSeq0 = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (commandManager != null && framework != null)
|
||||||
|
{
|
||||||
|
log.Information("[SubmarineManager] Sending /ays multi e command...");
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/ays multi e");
|
||||||
|
}).Wait();
|
||||||
|
log.Information("[SubmarineManager] ✓ /ays multi e command sent");
|
||||||
|
}
|
||||||
|
autoRetainerIPC.SetMultiModeEnabled(enabled: true);
|
||||||
|
submarinesPaused = true;
|
||||||
|
submarinesWaitingForSeq0 = false;
|
||||||
|
log.Information("[SubmarineManager] Multi-Mode enabled - quest automation paused");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[SubmarineManager] Failed to enable Multi-Mode: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DisableMultiModeAndReturn()
|
||||||
|
{
|
||||||
|
if (!autoRetainerIPC.IsAvailable)
|
||||||
|
{
|
||||||
|
log.Warning("[SubmarineManager] AutoRetainer not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (commandManager != null && framework != null)
|
||||||
|
{
|
||||||
|
log.Information("[SubmarineManager] Sending /ays multi d command...");
|
||||||
|
framework.RunOnFrameworkThread(delegate
|
||||||
|
{
|
||||||
|
commandManager.ProcessCommand("/ays multi d");
|
||||||
|
}).Wait();
|
||||||
|
log.Information("[SubmarineManager] ✓ /ays multi d command sent");
|
||||||
|
}
|
||||||
|
autoRetainerIPC.SetMultiModeEnabled(enabled: false);
|
||||||
|
log.Information("[SubmarineManager] Multi-Mode disabled - starting return to original character");
|
||||||
|
submarineNoAvailableWaitEnd = DateTime.MinValue;
|
||||||
|
if (!string.IsNullOrEmpty(originalCharacterForSubmarines))
|
||||||
|
{
|
||||||
|
submarineReloginInProgress = true;
|
||||||
|
log.Information("[SubmarineManager] Returning to original character: " + originalCharacterForSubmarines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error("[SubmarineManager] Failed to disable Multi-Mode: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CompleteSubmarineRelog()
|
||||||
|
{
|
||||||
|
submarineReloginInProgress = false;
|
||||||
|
submarinesPaused = false;
|
||||||
|
submarineJustCompleted = true;
|
||||||
|
submarineReloginCooldownEnd = DateTime.Now.AddSeconds(config.SubmarineReloginCooldown);
|
||||||
|
log.Information($"[SubmarineManager] Submarine rotation complete - cooldown active for {config.SubmarineReloginCooldown} seconds");
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSubmarineCooldownActive()
|
||||||
|
{
|
||||||
|
return DateTime.Now < submarineReloginCooldownEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearSubmarineJustCompleted()
|
||||||
|
{
|
||||||
|
submarineJustCompleted = false;
|
||||||
|
log.Information("[SubmarineManager] Cooldown expired - submarine checks re-enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
submarinesPaused = false;
|
||||||
|
submarinesWaitingForSeq0 = false;
|
||||||
|
submarineReloginInProgress = false;
|
||||||
|
submarineJustCompleted = false;
|
||||||
|
originalCharacterForSubmarines = null;
|
||||||
|
submarineReloginCooldownEnd = DateTime.MinValue;
|
||||||
|
log.Information("[SubmarineManager] State reset");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Reset();
|
||||||
|
log.Information("[SubmarineManager] Service disposed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
using System;
|
||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Dalamud.Plugin.Ipc;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
public class VNavmeshIPC : IDisposable
|
||||||
|
{
|
||||||
|
private readonly ICallGateSubscriber<Vector3, bool, float, Vector3?> pointOnFloorSubscriber;
|
||||||
|
|
||||||
|
private readonly ICallGateSubscriber<Vector3, float, float, Vector3?> nearestPointSubscriber;
|
||||||
|
|
||||||
|
private readonly ICallGateSubscriber<bool> isReadySubscriber;
|
||||||
|
|
||||||
|
public VNavmeshIPC(IDalamudPluginInterface pluginInterface)
|
||||||
|
{
|
||||||
|
pointOnFloorSubscriber = pluginInterface.GetIpcSubscriber<Vector3, bool, float, Vector3?>("vnavmesh.Query.Mesh.PointOnFloor");
|
||||||
|
nearestPointSubscriber = pluginInterface.GetIpcSubscriber<Vector3, float, float, Vector3?>("vnavmesh.Query.Mesh.NearestPoint");
|
||||||
|
isReadySubscriber = pluginInterface.GetIpcSubscriber<bool>("vnavmesh.Nav.IsReady");
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsReady()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return isReadySubscriber.InvokeFunc();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector3? FindPointOnFloor(Vector3 position, bool allowUnlandable = false, float searchRadius = 10f)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return pointOnFloorSubscriber.InvokeFunc(position, allowUnlandable, searchRadius);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector3? FindNearestPoint(Vector3 position, float horizontalRadius = 10f, float verticalRadius = 5f)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return nearestPointSubscriber.InvokeFunc(position, horizontalRadius, verticalRadius);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,171 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.Windowing;
|
||||||
|
using QuestionableCompanion.Helpers;
|
||||||
|
using QuestionableCompanion.Models;
|
||||||
|
using QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Windows;
|
||||||
|
|
||||||
|
public class AlliedSocietyPriorityWindow : Window, IDisposable
|
||||||
|
{
|
||||||
|
private readonly Configuration configuration;
|
||||||
|
|
||||||
|
private readonly AlliedSocietyDatabase database;
|
||||||
|
|
||||||
|
private List<AlliedSocietyPriority> editingPriorities = new List<AlliedSocietyPriority>();
|
||||||
|
|
||||||
|
private int? draggedIndex;
|
||||||
|
|
||||||
|
private const string DragDropId = "ALLIED_SOCIETY_PRIORITY";
|
||||||
|
|
||||||
|
private readonly Dictionary<byte, string> societyNames = new Dictionary<byte, string>
|
||||||
|
{
|
||||||
|
{ 1, "Amalj'aa" },
|
||||||
|
{ 2, "Sylphs" },
|
||||||
|
{ 3, "Kobolds" },
|
||||||
|
{ 4, "Sahagin" },
|
||||||
|
{ 5, "Ixal" },
|
||||||
|
{ 6, "Vanu Vanu" },
|
||||||
|
{ 7, "Vath" },
|
||||||
|
{ 8, "Moogles" },
|
||||||
|
{ 9, "Kojin" },
|
||||||
|
{ 10, "Ananta" },
|
||||||
|
{ 11, "Namazu" },
|
||||||
|
{ 12, "Pixies" },
|
||||||
|
{ 13, "Qitari" },
|
||||||
|
{ 14, "Dwarves" },
|
||||||
|
{ 15, "Arkasodara" },
|
||||||
|
{ 16, "Omicrons" },
|
||||||
|
{ 17, "Loporrits" },
|
||||||
|
{ 18, "Pelupelu" },
|
||||||
|
{ 19, "Mamool Ja" },
|
||||||
|
{ 20, "Yok Huy" }
|
||||||
|
};
|
||||||
|
|
||||||
|
public AlliedSocietyPriorityWindow(Configuration configuration, AlliedSocietyDatabase database)
|
||||||
|
: base("Allied Society Priority Configuration", ImGuiWindowFlags.NoCollapse)
|
||||||
|
{
|
||||||
|
this.configuration = configuration;
|
||||||
|
this.database = database;
|
||||||
|
base.Size = new Vector2(400f, 600f);
|
||||||
|
base.SizeCondition = ImGuiCond.FirstUseEver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnOpen()
|
||||||
|
{
|
||||||
|
if (configuration.AlliedSociety.RotationConfig.Priorities.Count == 0)
|
||||||
|
{
|
||||||
|
configuration.AlliedSociety.RotationConfig.InitializeDefaults();
|
||||||
|
database.SaveToConfig();
|
||||||
|
}
|
||||||
|
editingPriorities = (from p in configuration.AlliedSociety.RotationConfig.Priorities
|
||||||
|
orderby p.Order
|
||||||
|
select new AlliedSocietyPriority
|
||||||
|
{
|
||||||
|
SocietyId = p.SocietyId,
|
||||||
|
Enabled = p.Enabled,
|
||||||
|
Order = p.Order
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Draw()
|
||||||
|
{
|
||||||
|
ImGui.TextWrapped("Drag societies to reorder priorities. Uncheck to disable specific societies.");
|
||||||
|
ImGui.Separator();
|
||||||
|
float availableHeight = ImGui.GetWindowSize().Y - 120f;
|
||||||
|
ImGui.BeginChild("PriorityList", new Vector2(0f, availableHeight), border: true);
|
||||||
|
for (int i = 0; i < editingPriorities.Count; i++)
|
||||||
|
{
|
||||||
|
AlliedSocietyPriority priority = editingPriorities[i];
|
||||||
|
string name = (societyNames.ContainsKey(priority.SocietyId) ? societyNames[priority.SocietyId] : $"Unknown ({priority.SocietyId})");
|
||||||
|
ImGui.PushID(i);
|
||||||
|
if (draggedIndex == i)
|
||||||
|
{
|
||||||
|
uint highlightColor = ImGui.ColorConvertFloat4ToU32(new Vector4(0f, 0.7f, 0f, 0.3f));
|
||||||
|
Vector2 cursorPos = ImGui.GetCursorScreenPos();
|
||||||
|
Vector2 itemSize = new Vector2(ImGui.GetContentRegionAvail().X, ImGui.GetFrameHeight());
|
||||||
|
ImGui.GetWindowDrawList().AddRectFilled(cursorPos, cursorPos + itemSize, highlightColor);
|
||||||
|
}
|
||||||
|
ImGui.PushFont(UiBuilder.IconFont);
|
||||||
|
ImU8String label = new ImU8String(6, 1);
|
||||||
|
label.AppendFormatted(FontAwesomeIcon.ArrowsUpDownLeftRight.ToIconString());
|
||||||
|
label.AppendLiteral("##drag");
|
||||||
|
ImGui.Button(label);
|
||||||
|
ImGui.PopFont();
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetMouseCursor(ImGuiMouseCursor.ResizeAll);
|
||||||
|
}
|
||||||
|
if (ImGui.BeginDragDropSource())
|
||||||
|
{
|
||||||
|
draggedIndex = i;
|
||||||
|
ImGuiDragDrop.SetDragDropPayload("ALLIED_SOCIETY_PRIORITY", i);
|
||||||
|
ImGui.Text(name);
|
||||||
|
ImGui.EndDragDropSource();
|
||||||
|
}
|
||||||
|
else if (draggedIndex == i && !ImGui.IsMouseDown(ImGuiMouseButton.Left))
|
||||||
|
{
|
||||||
|
draggedIndex = null;
|
||||||
|
}
|
||||||
|
if (ImGui.BeginDragDropTarget())
|
||||||
|
{
|
||||||
|
if (ImGuiDragDrop.AcceptDragDropPayload<int>("ALLIED_SOCIETY_PRIORITY", out var sourceIndex) && sourceIndex != i)
|
||||||
|
{
|
||||||
|
AlliedSocietyPriority item = editingPriorities[sourceIndex];
|
||||||
|
editingPriorities.RemoveAt(sourceIndex);
|
||||||
|
editingPriorities.Insert(i, item);
|
||||||
|
UpdateOrders();
|
||||||
|
draggedIndex = i;
|
||||||
|
}
|
||||||
|
ImGui.EndDragDropTarget();
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
bool enabled = priority.Enabled;
|
||||||
|
ImU8String label2 = new ImU8String(9, 0);
|
||||||
|
label2.AppendLiteral("##enabled");
|
||||||
|
if (ImGui.Checkbox(label2, ref enabled))
|
||||||
|
{
|
||||||
|
priority.Enabled = enabled;
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.Text(name);
|
||||||
|
ImGui.PopID();
|
||||||
|
}
|
||||||
|
ImGui.EndChild();
|
||||||
|
ImGui.Separator();
|
||||||
|
if (ImGui.Button("Save"))
|
||||||
|
{
|
||||||
|
Save();
|
||||||
|
base.IsOpen = false;
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.Button("Cancel"))
|
||||||
|
{
|
||||||
|
base.IsOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateOrders()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < editingPriorities.Count; i++)
|
||||||
|
{
|
||||||
|
editingPriorities[i].Order = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Save()
|
||||||
|
{
|
||||||
|
UpdateOrders();
|
||||||
|
configuration.AlliedSociety.RotationConfig.Priorities = editingPriorities;
|
||||||
|
database.SaveToConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,564 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Interface.Windowing;
|
||||||
|
using QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Windows;
|
||||||
|
|
||||||
|
public class ConfigWindow : Window, IDisposable
|
||||||
|
{
|
||||||
|
private readonly Configuration configuration;
|
||||||
|
|
||||||
|
private readonly Plugin plugin;
|
||||||
|
|
||||||
|
public ConfigWindow(Plugin plugin)
|
||||||
|
: base("Questionable Companion Settings###QCSettings")
|
||||||
|
{
|
||||||
|
base.Size = new Vector2(600f, 400f);
|
||||||
|
base.SizeCondition = ImGuiCond.FirstUseEver;
|
||||||
|
this.plugin = plugin;
|
||||||
|
configuration = plugin.Configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void PreDraw()
|
||||||
|
{
|
||||||
|
if (configuration.IsConfigWindowMovable)
|
||||||
|
{
|
||||||
|
base.Flags &= ~ImGuiWindowFlags.NoMove;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
base.Flags |= ImGuiWindowFlags.NoMove;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Draw()
|
||||||
|
{
|
||||||
|
ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(1f, 0.8f, 0.2f, 1f));
|
||||||
|
ImGui.TextWrapped("Configuration Moved!");
|
||||||
|
ImGui.PopStyleColor();
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextWrapped("The configuration interface has been moved to the new Main Window for a better user experience.");
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextWrapped("All settings are now available in the Main Window with improved organization and features:");
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.BulletText("Quest Rotation Management");
|
||||||
|
ImGui.BulletText("Event Quest Automation");
|
||||||
|
ImGui.BulletText("MSQ Progress Tracking");
|
||||||
|
ImGui.BulletText("DC Travel Configuration");
|
||||||
|
ImGui.BulletText("Advanced Settings");
|
||||||
|
ImGui.BulletText("And much more!");
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
if (ImGui.Button(size: new Vector2(ImGui.GetContentRegionAvail().X, 50f), label: "Open Main Window (Settings Tab)"))
|
||||||
|
{
|
||||||
|
plugin.ToggleMainUi();
|
||||||
|
base.IsOpen = false;
|
||||||
|
}
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0.7f, 0.7f, 0.7f, 1f));
|
||||||
|
ImGui.TextWrapped("This legacy configuration window will be removed in a future update.");
|
||||||
|
ImGui.TextWrapped("Please use the Main Window for all configuration needs.");
|
||||||
|
ImGui.PopStyleColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawGeneralTab()
|
||||||
|
{
|
||||||
|
ImGui.Text("General Settings");
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
bool autoStart = configuration.AutoStartOnLogin;
|
||||||
|
if (ImGui.Checkbox("Auto-start on login", ref autoStart))
|
||||||
|
{
|
||||||
|
configuration.AutoStartOnLogin = autoStart;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
bool dryRun = configuration.EnableDryRun;
|
||||||
|
if (ImGui.Checkbox("Enable Dry Run Mode (simulate without executing)", ref dryRun))
|
||||||
|
{
|
||||||
|
configuration.EnableDryRun = dryRun;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
bool restoreState = configuration.RestoreStateOnLoad;
|
||||||
|
if (ImGui.Checkbox("Restore state on plugin load", ref restoreState))
|
||||||
|
{
|
||||||
|
configuration.RestoreStateOnLoad = restoreState;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Text("Execution Settings");
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
int maxRetries = configuration.MaxRetryAttempts;
|
||||||
|
if (ImGui.SliderInt("Max retry attempts", ref maxRetries, 1, 10))
|
||||||
|
{
|
||||||
|
configuration.MaxRetryAttempts = maxRetries;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
int switchDelay = configuration.CharacterSwitchDelay;
|
||||||
|
if (ImGui.SliderInt("Character switch delay (seconds)", ref switchDelay, 3, 15))
|
||||||
|
{
|
||||||
|
configuration.CharacterSwitchDelay = switchDelay;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Text("Logging Settings");
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
int maxLogs = configuration.MaxLogEntries;
|
||||||
|
if (ImGui.SliderInt("Max log entries", ref maxLogs, 50, 500))
|
||||||
|
{
|
||||||
|
configuration.MaxLogEntries = maxLogs;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
bool showDebug = configuration.ShowDebugLogs;
|
||||||
|
if (ImGui.Checkbox("Show debug logs", ref showDebug))
|
||||||
|
{
|
||||||
|
configuration.ShowDebugLogs = showDebug;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
bool logToFile = configuration.LogToFile;
|
||||||
|
if (ImGui.Checkbox("Log to file", ref logToFile))
|
||||||
|
{
|
||||||
|
configuration.LogToFile = logToFile;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Text("UI Settings");
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
bool movable = configuration.IsConfigWindowMovable;
|
||||||
|
if (ImGui.Checkbox("Movable config window", ref movable))
|
||||||
|
{
|
||||||
|
configuration.IsConfigWindowMovable = movable;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawAdvancedFeaturesTab()
|
||||||
|
{
|
||||||
|
ImGui.TextColored(new Vector4(0.3f, 0.8f, 1f, 1f), "Submarine Management");
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
bool enableSubmarineCheck = configuration.EnableSubmarineCheck;
|
||||||
|
if (ImGui.Checkbox("Enable Submarine Monitoring", ref enableSubmarineCheck))
|
||||||
|
{
|
||||||
|
configuration.EnableSubmarineCheck = enableSubmarineCheck;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("Automatically monitor submarines and pause quest rotation when submarines are ready");
|
||||||
|
}
|
||||||
|
if (configuration.EnableSubmarineCheck)
|
||||||
|
{
|
||||||
|
ImGui.Indent();
|
||||||
|
int submarineCheckInterval = configuration.SubmarineCheckInterval;
|
||||||
|
if (ImGui.SliderInt("Check Interval (seconds)", ref submarineCheckInterval, 30, 300))
|
||||||
|
{
|
||||||
|
configuration.SubmarineCheckInterval = submarineCheckInterval;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
int submarineReloginCooldown = configuration.SubmarineReloginCooldown;
|
||||||
|
if (ImGui.SliderInt("Cooldown after Relog (seconds)", ref submarineReloginCooldown, 60, 300))
|
||||||
|
{
|
||||||
|
configuration.SubmarineReloginCooldown = submarineReloginCooldown;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
int submarineWaitTime = configuration.SubmarineWaitTime;
|
||||||
|
if (ImGui.SliderInt("Wait time before submarine (seconds)", ref submarineWaitTime, 10, 120))
|
||||||
|
{
|
||||||
|
configuration.SubmarineWaitTime = submarineWaitTime;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
ImGui.Unindent();
|
||||||
|
}
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextColored(new Vector4(0.3f, 0.8f, 1f, 1f), "Movement Monitor");
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
bool enableMovementMonitor = configuration.EnableMovementMonitor;
|
||||||
|
if (ImGui.Checkbox("Enable Movement Monitor", ref enableMovementMonitor))
|
||||||
|
{
|
||||||
|
configuration.EnableMovementMonitor = enableMovementMonitor;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("Automatically detect if player is stuck and send /qst reload");
|
||||||
|
}
|
||||||
|
if (configuration.EnableMovementMonitor)
|
||||||
|
{
|
||||||
|
ImGui.Indent();
|
||||||
|
int movementCheckInterval = configuration.MovementCheckInterval;
|
||||||
|
if (ImGui.SliderInt("Check Interval (seconds)##movement", ref movementCheckInterval, 3, 30))
|
||||||
|
{
|
||||||
|
configuration.MovementCheckInterval = movementCheckInterval;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
int movementStuckThreshold = configuration.MovementStuckThreshold;
|
||||||
|
if (ImGui.SliderInt("Stuck Threshold (seconds)", ref movementStuckThreshold, 15, 120))
|
||||||
|
{
|
||||||
|
configuration.MovementStuckThreshold = movementStuckThreshold;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("Time without movement before sending /qst reload");
|
||||||
|
}
|
||||||
|
ImGui.Unindent();
|
||||||
|
}
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextColored(new Vector4(1f, 0.5f, 0.3f, 1f), "Combat Handling");
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
bool enableCombatHandling = configuration.EnableCombatHandling;
|
||||||
|
if (ImGui.Checkbox("Enable Combat Handling", ref enableCombatHandling))
|
||||||
|
{
|
||||||
|
configuration.EnableCombatHandling = enableCombatHandling;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("Automatically enable RSR/VBMAI/BMRAI when HP drops below threshold during combat");
|
||||||
|
}
|
||||||
|
if (configuration.EnableCombatHandling)
|
||||||
|
{
|
||||||
|
ImGui.Indent();
|
||||||
|
int combatHPThreshold = configuration.CombatHPThreshold;
|
||||||
|
if (ImGui.SliderInt("HP Threshold (%)", ref combatHPThreshold, 1, 99))
|
||||||
|
{
|
||||||
|
configuration.CombatHPThreshold = combatHPThreshold;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("Enable combat automation when HP drops below this percentage\nCommands: /rsr manual, /vbmai on, /bmrai on");
|
||||||
|
}
|
||||||
|
ImGui.TextWrapped("When HP drops below threshold:");
|
||||||
|
ImGui.BulletText("/rsr manual");
|
||||||
|
ImGui.BulletText("/vbmai on");
|
||||||
|
ImGui.BulletText("/bmrai on");
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextWrapped("When combat ends:");
|
||||||
|
ImGui.BulletText("/rsr off");
|
||||||
|
ImGui.BulletText("/vbmai off");
|
||||||
|
ImGui.BulletText("/bmrai off");
|
||||||
|
ImGui.Unindent();
|
||||||
|
}
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextColored(new Vector4(1f, 0.3f, 0.3f, 1f), "Death Handling");
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
bool enableDeathHandling = configuration.EnableDeathHandling;
|
||||||
|
if (ImGui.Checkbox("Enable Death Handling", ref enableDeathHandling))
|
||||||
|
{
|
||||||
|
configuration.EnableDeathHandling = enableDeathHandling;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("Automatically respawn when player dies");
|
||||||
|
}
|
||||||
|
if (configuration.EnableDeathHandling)
|
||||||
|
{
|
||||||
|
ImGui.Indent();
|
||||||
|
int deathRespawnDelay = configuration.DeathRespawnDelay;
|
||||||
|
if (ImGui.SliderInt("Teleport Delay (seconds)", ref deathRespawnDelay, 1, 30))
|
||||||
|
{
|
||||||
|
configuration.DeathRespawnDelay = deathRespawnDelay;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("Time to wait after respawn before teleporting back to death location");
|
||||||
|
}
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextWrapped("On Death:");
|
||||||
|
ImGui.BulletText("Detect 0% HP");
|
||||||
|
ImGui.BulletText("Save position & territory");
|
||||||
|
ImGui.BulletText("Auto-click SelectYesNo (respawn)");
|
||||||
|
ImU8String text = new ImU8String(13, 1);
|
||||||
|
text.AppendLiteral("Wait ");
|
||||||
|
text.AppendFormatted(configuration.DeathRespawnDelay);
|
||||||
|
text.AppendLiteral(" seconds");
|
||||||
|
ImGui.BulletText(text);
|
||||||
|
ImGui.BulletText("Teleport back to death location");
|
||||||
|
ImGui.Unindent();
|
||||||
|
}
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextColored(new Vector4(0.3f, 0.8f, 1f, 1f), "Logging Settings");
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
bool logToDalamud = configuration.LogToDalamud;
|
||||||
|
if (ImGui.Checkbox("Log to Dalamud Log", ref logToDalamud))
|
||||||
|
{
|
||||||
|
configuration.LogToDalamud = logToDalamud;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("Enable to also log to Dalamud log (can cause spam)");
|
||||||
|
}
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextColored(new Vector4(0.3f, 0.8f, 1f, 1f), "Dungeon Automation");
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
bool enableAutoDutyUnsynced = configuration.EnableAutoDutyUnsynced;
|
||||||
|
if (ImGui.Checkbox("Enable AutoDuty Unsynced", ref enableAutoDutyUnsynced))
|
||||||
|
{
|
||||||
|
configuration.EnableAutoDutyUnsynced = enableAutoDutyUnsynced;
|
||||||
|
configuration.Save();
|
||||||
|
plugin.GetDungeonAutomation()?.SetDutyModeBasedOnConfig();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("Run dungeons unsynced for faster completion");
|
||||||
|
}
|
||||||
|
if (configuration.EnableAutoDutyUnsynced)
|
||||||
|
{
|
||||||
|
ImGui.Indent();
|
||||||
|
int autoDutyPartySize = configuration.AutoDutyPartySize;
|
||||||
|
if (ImGui.SliderInt("Party Size Check (members)", ref autoDutyPartySize, 1, 8))
|
||||||
|
{
|
||||||
|
configuration.AutoDutyPartySize = autoDutyPartySize;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("Minimum party size required before starting dungeon");
|
||||||
|
}
|
||||||
|
int autoDutyMaxWaitForParty = configuration.AutoDutyMaxWaitForParty;
|
||||||
|
if (ImGui.SliderInt("Max Wait for Party (seconds)", ref autoDutyMaxWaitForParty, 10, 120))
|
||||||
|
{
|
||||||
|
configuration.AutoDutyMaxWaitForParty = autoDutyMaxWaitForParty;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
int autoDutyReInviteInterval = configuration.AutoDutyReInviteInterval;
|
||||||
|
if (ImGui.SliderInt("Re-invite Interval (seconds)", ref autoDutyReInviteInterval, 5, 60))
|
||||||
|
{
|
||||||
|
configuration.AutoDutyReInviteInterval = autoDutyReInviteInterval;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
ImGui.Unindent();
|
||||||
|
}
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextColored(new Vector4(0.3f, 0.8f, 1f, 1f), "Quest Automation");
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
bool enableQSTReloadTracking = configuration.EnableQSTReloadTracking;
|
||||||
|
if (ImGui.Checkbox("Enable QST Reload Tracking", ref enableQSTReloadTracking))
|
||||||
|
{
|
||||||
|
configuration.EnableQSTReloadTracking = enableQSTReloadTracking;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("Track Questionable reloads and switch character if too many reloads occur");
|
||||||
|
}
|
||||||
|
if (configuration.EnableQSTReloadTracking)
|
||||||
|
{
|
||||||
|
ImGui.Indent();
|
||||||
|
int maxQSTReloadsBeforeSwitch = configuration.MaxQSTReloadsBeforeSwitch;
|
||||||
|
if (ImGui.SliderInt("Max Reloads before switch", ref maxQSTReloadsBeforeSwitch, 1, 20))
|
||||||
|
{
|
||||||
|
configuration.MaxQSTReloadsBeforeSwitch = maxQSTReloadsBeforeSwitch;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
ImGui.Unindent();
|
||||||
|
}
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextColored(new Vector4(0.3f, 0.8f, 1f, 1f), "Character Management");
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
bool enableMultiModeAfterRotation = configuration.EnableMultiModeAfterRotation;
|
||||||
|
if (ImGui.Checkbox("Enable Multi-Mode after Rotation", ref enableMultiModeAfterRotation))
|
||||||
|
{
|
||||||
|
configuration.EnableMultiModeAfterRotation = enableMultiModeAfterRotation;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("Enable AutoRetainer Multi-Mode after completing character rotation");
|
||||||
|
}
|
||||||
|
bool returnToHomeworldOnStopQuest = configuration.ReturnToHomeworldOnStopQuest;
|
||||||
|
if (ImGui.Checkbox("Return to Homeworld on Stop Quest", ref returnToHomeworldOnStopQuest))
|
||||||
|
{
|
||||||
|
configuration.ReturnToHomeworldOnStopQuest = returnToHomeworldOnStopQuest;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("Automatically return to homeworld when stop quest is completed");
|
||||||
|
}
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextColored(new Vector4(0.3f, 0.8f, 1f, 1f), "Safe Wait Settings");
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
bool enableSafeWaitBefore = configuration.EnableSafeWaitBeforeCharacterSwitch;
|
||||||
|
if (ImGui.Checkbox("Enable Safe Wait Before Character Switch", ref enableSafeWaitBefore))
|
||||||
|
{
|
||||||
|
configuration.EnableSafeWaitBeforeCharacterSwitch = enableSafeWaitBefore;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("Wait for character to stabilize (movement, actions) before switching");
|
||||||
|
}
|
||||||
|
bool enableSafeWaitAfter = configuration.EnableSafeWaitAfterCharacterSwitch;
|
||||||
|
if (ImGui.Checkbox("Enable Safe Wait After Character Switch", ref enableSafeWaitAfter))
|
||||||
|
{
|
||||||
|
configuration.EnableSafeWaitAfterCharacterSwitch = enableSafeWaitAfter;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("Wait for character to fully load after switching");
|
||||||
|
}
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextColored(new Vector4(0.3f, 0.8f, 1f, 1f), "Quest Pre-Check");
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
bool enableQuestPreCheck = configuration.EnableQuestPreCheck;
|
||||||
|
if (ImGui.Checkbox("Enable Quest Pre-Check", ref enableQuestPreCheck))
|
||||||
|
{
|
||||||
|
configuration.EnableQuestPreCheck = enableQuestPreCheck;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("Check quest completion status before starting rotation to skip completed characters");
|
||||||
|
}
|
||||||
|
ImGui.TextWrapped("Quest Pre-Check scans all characters for completed quests before rotation starts, preventing unnecessary character switches.");
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextColored(new Vector4(0.3f, 0.8f, 1f, 1f), "DC Travel World Selector");
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextWrapped("Configure world travel for Data Center travel quests. Requires Lifestream plugin.");
|
||||||
|
ImGui.Spacing();
|
||||||
|
string[] datacenters = configuration.WorldsByDatacenter.Keys.ToArray();
|
||||||
|
int currentDCIndex = Array.IndexOf(datacenters, configuration.SelectedDatacenter);
|
||||||
|
if (currentDCIndex < 0)
|
||||||
|
{
|
||||||
|
currentDCIndex = 0;
|
||||||
|
}
|
||||||
|
ImGui.Text("Select Datacenter:");
|
||||||
|
if (ImGui.Combo((ImU8String)"##DCSelector", ref currentDCIndex, (ReadOnlySpan<string>)datacenters, datacenters.Length))
|
||||||
|
{
|
||||||
|
configuration.SelectedDatacenter = datacenters[currentDCIndex];
|
||||||
|
if (configuration.WorldsByDatacenter.TryGetValue(configuration.SelectedDatacenter, out List<string> newWorlds) && newWorlds.Count > 0)
|
||||||
|
{
|
||||||
|
configuration.DCTravelWorld = newWorlds[0];
|
||||||
|
}
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
ImGui.Spacing();
|
||||||
|
if (configuration.WorldsByDatacenter.TryGetValue(configuration.SelectedDatacenter, out List<string> worlds))
|
||||||
|
{
|
||||||
|
string[] worldArray = worlds.ToArray();
|
||||||
|
int currentWorldIndex = Array.IndexOf(worldArray, configuration.DCTravelWorld);
|
||||||
|
if (currentWorldIndex < 0)
|
||||||
|
{
|
||||||
|
currentWorldIndex = 0;
|
||||||
|
}
|
||||||
|
ImGui.Text("Select Target World:");
|
||||||
|
if (ImGui.Combo((ImU8String)"##WorldSelector", ref currentWorldIndex, (ReadOnlySpan<string>)worldArray, worldArray.Length))
|
||||||
|
{
|
||||||
|
configuration.DCTravelWorld = worldArray[currentWorldIndex];
|
||||||
|
configuration.EnableDCTravel = !string.IsNullOrEmpty(configuration.DCTravelWorld);
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
LifestreamIPC lifestreamIPC = Plugin.Instance?.LifestreamIPC;
|
||||||
|
if (lifestreamIPC != null && !lifestreamIPC.IsAvailable)
|
||||||
|
{
|
||||||
|
lifestreamIPC.ForceCheckAvailability();
|
||||||
|
}
|
||||||
|
bool lifestreamAvailable = lifestreamIPC?.IsAvailable ?? false;
|
||||||
|
if (!lifestreamAvailable)
|
||||||
|
{
|
||||||
|
ImGui.BeginDisabled();
|
||||||
|
}
|
||||||
|
bool enableDCTravel = configuration.EnableDCTravel;
|
||||||
|
if (ImGui.Checkbox("Enable DC Travel", ref enableDCTravel))
|
||||||
|
{
|
||||||
|
configuration.EnableDCTravel = enableDCTravel;
|
||||||
|
configuration.Save();
|
||||||
|
}
|
||||||
|
if (!lifestreamAvailable)
|
||||||
|
{
|
||||||
|
ImGui.EndDisabled();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
if (!lifestreamAvailable)
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("Lifestream plugin is required for DC Travel!\nPlease install and enable Lifestream to use this feature.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("Enable automatic DC travel when DC travel quests are detected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!lifestreamAvailable)
|
||||||
|
{
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextColored(new Vector4(1f, 0.5f, 0f, 1f), "⚠\ufe0f Lifestream plugin not available!");
|
||||||
|
ImGui.TextWrapped("DC Travel requires Lifestream to be installed and enabled.");
|
||||||
|
}
|
||||||
|
ImGui.Spacing();
|
||||||
|
if (configuration.EnableDCTravel && !string.IsNullOrEmpty(configuration.DCTravelWorld))
|
||||||
|
{
|
||||||
|
Vector4 col = new Vector4(0.2f, 1f, 0.2f, 1f);
|
||||||
|
ImU8String text2 = new ImU8String(21, 1);
|
||||||
|
text2.AppendLiteral("✓ DC Travel ACTIVE → ");
|
||||||
|
text2.AppendFormatted(configuration.DCTravelWorld);
|
||||||
|
ImGui.TextColored(in col, text2);
|
||||||
|
ImU8String text3 = new ImU8String(100, 1);
|
||||||
|
text3.AppendLiteral("Character will travel to ");
|
||||||
|
text3.AppendFormatted(configuration.DCTravelWorld);
|
||||||
|
text3.AppendLiteral(" immediately after login, then return to homeworld before character switch.");
|
||||||
|
ImGui.TextWrapped(text3);
|
||||||
|
}
|
||||||
|
else if (!configuration.EnableDCTravel)
|
||||||
|
{
|
||||||
|
ImGui.TextColored(new Vector4(0.7f, 0.7f, 0.7f, 1f), "○ DC Travel disabled");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextColored(new Vector4(1f, 0.5f, 0f, 1f), "⚠ DC Travel enabled but no world selected!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextColored(new Vector4(1f, 0.5f, 0f, 1f), "No worlds available for selected datacenter");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
using System;
|
||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Interface.Windowing;
|
||||||
|
using QuestionableCompanion.Services;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion.Windows;
|
||||||
|
|
||||||
|
public class DebugWindow : Window, IDisposable
|
||||||
|
{
|
||||||
|
private readonly Plugin plugin;
|
||||||
|
|
||||||
|
private readonly CombatDutyDetectionService? combatDutyDetection;
|
||||||
|
|
||||||
|
private readonly DeathHandlerService? deathHandler;
|
||||||
|
|
||||||
|
private readonly DungeonAutomationService? dungeonAutomation;
|
||||||
|
|
||||||
|
public DebugWindow(Plugin plugin, CombatDutyDetectionService? combatDutyDetection, DeathHandlerService? deathHandler, DungeonAutomationService? dungeonAutomation)
|
||||||
|
: base("QST Companion Debug###QSTDebug", ImGuiWindowFlags.NoCollapse)
|
||||||
|
{
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.combatDutyDetection = combatDutyDetection;
|
||||||
|
this.deathHandler = deathHandler;
|
||||||
|
this.dungeonAutomation = dungeonAutomation;
|
||||||
|
base.Size = new Vector2(500f, 400f);
|
||||||
|
base.SizeCondition = ImGuiCond.FirstUseEver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Draw()
|
||||||
|
{
|
||||||
|
ImGui.TextColored(new Vector4(1f, 0.5f, 0f, 1f), " DEBUG MENU - FOR TESTING ONLY ");
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextColored(new Vector4(1f, 0.3f, 0.3f, 1f), "Combat Handling");
|
||||||
|
ImGui.Separator();
|
||||||
|
if (combatDutyDetection != null)
|
||||||
|
{
|
||||||
|
ImU8String text = new ImU8String(11, 1);
|
||||||
|
text.AppendLiteral("In Combat: ");
|
||||||
|
text.AppendFormatted(combatDutyDetection.IsInCombat);
|
||||||
|
ImGui.Text(text);
|
||||||
|
ImU8String text2 = new ImU8String(9, 1);
|
||||||
|
text2.AppendLiteral("In Duty: ");
|
||||||
|
text2.AppendFormatted(combatDutyDetection.IsInDuty);
|
||||||
|
ImGui.Text(text2);
|
||||||
|
ImU8String text3 = new ImU8String(15, 1);
|
||||||
|
text3.AppendLiteral("In Duty Queue: ");
|
||||||
|
text3.AppendFormatted(combatDutyDetection.IsInDutyQueue);
|
||||||
|
ImGui.Text(text3);
|
||||||
|
ImU8String text4 = new ImU8String(14, 1);
|
||||||
|
text4.AppendLiteral("Should Pause: ");
|
||||||
|
text4.AppendFormatted(combatDutyDetection.ShouldPauseAutomation);
|
||||||
|
ImGui.Text(text4);
|
||||||
|
ImGui.Spacing();
|
||||||
|
if (ImGui.Button("Test Combat Detection"))
|
||||||
|
{
|
||||||
|
combatDutyDetection.Update();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("Manually trigger combat detection update");
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.Button("Reset Combat State"))
|
||||||
|
{
|
||||||
|
combatDutyDetection.Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextColored(new Vector4(1f, 0f, 0f, 1f), "Combat Detection Service not available");
|
||||||
|
}
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextColored(new Vector4(1f, 0.3f, 0.3f, 1f), "Death Handling");
|
||||||
|
ImGui.Separator();
|
||||||
|
if (deathHandler != null)
|
||||||
|
{
|
||||||
|
ImU8String text5 = new ImU8String(9, 1);
|
||||||
|
text5.AppendLiteral("Is Dead: ");
|
||||||
|
text5.AppendFormatted(deathHandler.IsDead);
|
||||||
|
ImGui.Text(text5);
|
||||||
|
ImU8String text6 = new ImU8String(19, 1);
|
||||||
|
text6.AppendLiteral("Time Since Death: ");
|
||||||
|
text6.AppendFormatted(deathHandler.TimeSinceDeath.TotalSeconds, "F1");
|
||||||
|
text6.AppendLiteral("s");
|
||||||
|
ImGui.Text(text6);
|
||||||
|
ImGui.Spacing();
|
||||||
|
if (ImGui.Button("Test Death Detection"))
|
||||||
|
{
|
||||||
|
deathHandler.Update();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("Manually trigger death detection update");
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.Button("Reset Death State"))
|
||||||
|
{
|
||||||
|
deathHandler.Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextColored(new Vector4(1f, 0f, 0f, 1f), "Death Handler Service not available");
|
||||||
|
}
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.TextColored(new Vector4(1f, 0.3f, 0.3f, 1f), "Dungeon Automation");
|
||||||
|
ImGui.Separator();
|
||||||
|
if (dungeonAutomation != null)
|
||||||
|
{
|
||||||
|
ImU8String text7 = new ImU8String(19, 1);
|
||||||
|
text7.AppendLiteral("Waiting for Party: ");
|
||||||
|
text7.AppendFormatted(dungeonAutomation.IsWaitingForParty);
|
||||||
|
ImGui.Text(text7);
|
||||||
|
ImU8String text8 = new ImU8String(20, 1);
|
||||||
|
text8.AppendLiteral("Current Party Size: ");
|
||||||
|
text8.AppendFormatted(dungeonAutomation.CurrentPartySize);
|
||||||
|
ImGui.Text(text8);
|
||||||
|
ImGui.Spacing();
|
||||||
|
if (ImGui.Button("Test Party Invite"))
|
||||||
|
{
|
||||||
|
dungeonAutomation.StartDungeonAutomation();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("Test BTB enable + invite + party wait");
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.Button("Test Party Disband"))
|
||||||
|
{
|
||||||
|
dungeonAutomation.DisbandParty();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("Test party disband with /pcmd breakup");
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.Button("Reset Dungeon State"))
|
||||||
|
{
|
||||||
|
dungeonAutomation.Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextColored(new Vector4(1f, 0f, 0f, 1f), "Dungeon Automation Service not available");
|
||||||
|
}
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Spacing();
|
||||||
|
ImGui.Separator();
|
||||||
|
ImGui.TextColored(new Vector4(0.5f, 0.8f, 1f, 1f), "Info");
|
||||||
|
ImGui.Text("This debug menu allows testing of individual features.");
|
||||||
|
ImGui.Text("Use /qstcomp dbg to toggle this window.");
|
||||||
|
}
|
||||||
|
}
|
||||||
3368
QuestionableCompanion/QuestionableCompanion.Windows/NewMainWindow.cs
Normal file
3368
QuestionableCompanion/QuestionableCompanion.Windows/NewMainWindow.cs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,17 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class AlliedSocietySettings
|
||||||
|
{
|
||||||
|
public AlliedSocietyConfiguration RotationConfig { get; set; } = new AlliedSocietyConfiguration();
|
||||||
|
|
||||||
|
public Dictionary<string, AlliedSocietyCharacterStatus> CharacterStatuses { get; set; } = new Dictionary<string, AlliedSocietyCharacterStatus>();
|
||||||
|
|
||||||
|
public Dictionary<string, List<AlliedSocietyProgress>> CharacterProgress { get; set; } = new Dictionary<string, List<AlliedSocietyProgress>>();
|
||||||
|
|
||||||
|
public DateTime LastResetDate { get; set; } = DateTime.MinValue;
|
||||||
|
}
|
||||||
211
QuestionableCompanion/QuestionableCompanion/Configuration.cs
Normal file
211
QuestionableCompanion/QuestionableCompanion/Configuration.cs
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Dalamud.Configuration;
|
||||||
|
using QuestionableCompanion.Models;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class Configuration : IPluginConfiguration
|
||||||
|
{
|
||||||
|
public int Version { get; set; } = 1;
|
||||||
|
|
||||||
|
public bool IsConfigWindowMovable { get; set; } = true;
|
||||||
|
|
||||||
|
public bool ShowDebugLogs { get; set; }
|
||||||
|
|
||||||
|
public List<QuestProfile> Profiles { get; set; } = new List<QuestProfile>();
|
||||||
|
|
||||||
|
public string ActiveProfileName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public AlliedSocietySettings AlliedSociety { get; set; } = new AlliedSocietySettings();
|
||||||
|
|
||||||
|
public bool AutoStartOnLogin { get; set; }
|
||||||
|
|
||||||
|
public bool EnableDryRun { get; set; }
|
||||||
|
|
||||||
|
public int MaxRetryAttempts { get; set; } = 3;
|
||||||
|
|
||||||
|
public int CharacterSwitchDelay { get; set; } = 5;
|
||||||
|
|
||||||
|
public int MaxLogEntries { get; set; } = 100;
|
||||||
|
|
||||||
|
public bool LogToFile { get; set; }
|
||||||
|
|
||||||
|
public ExecutionState LastExecutionState { get; set; } = new ExecutionState();
|
||||||
|
|
||||||
|
public bool RestoreStateOnLoad { get; set; }
|
||||||
|
|
||||||
|
public List<StopPoint> StopPoints { get; set; } = new List<StopPoint>();
|
||||||
|
|
||||||
|
public RotationState LastRotationState { get; set; } = new RotationState();
|
||||||
|
|
||||||
|
public List<string> SelectedCharactersForRotation { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
public Dictionary<uint, List<string>> QuestCompletionByCharacter { get; set; } = new Dictionary<uint, List<string>>();
|
||||||
|
|
||||||
|
public Dictionary<string, List<string>> EventQuestCompletionByCharacter { get; set; } = new Dictionary<string, List<string>>();
|
||||||
|
|
||||||
|
public string CurrentEventQuestId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public List<string> SelectedCharactersForEventQuest { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
public bool RunEventQuestsOnARPostProcess { get; set; }
|
||||||
|
|
||||||
|
public List<string> EventQuestsToRunOnPostProcess { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
public int EventQuestPostProcessTimeoutMinutes { get; set; } = 30;
|
||||||
|
|
||||||
|
public bool EnableSubmarineCheck { get; set; }
|
||||||
|
|
||||||
|
public int SubmarineCheckInterval { get; set; } = 90;
|
||||||
|
|
||||||
|
public int SubmarineReloginCooldown { get; set; } = 120;
|
||||||
|
|
||||||
|
public int SubmarineWaitTime { get; set; } = 30;
|
||||||
|
|
||||||
|
public bool EnableAutoDutyUnsynced { get; set; }
|
||||||
|
|
||||||
|
public int AutoDutyPartySize { get; set; } = 2;
|
||||||
|
|
||||||
|
public int AutoDutyMaxWaitForParty { get; set; } = 30;
|
||||||
|
|
||||||
|
public int AutoDutyReInviteInterval { get; set; } = 10;
|
||||||
|
|
||||||
|
public bool EnableQSTReloadTracking { get; set; }
|
||||||
|
|
||||||
|
public int MaxQSTReloadsBeforeSwitch { get; set; } = 5;
|
||||||
|
|
||||||
|
public bool EnableDCTravel { get; set; }
|
||||||
|
|
||||||
|
public string DCTravelWorld { get; set; } = "";
|
||||||
|
|
||||||
|
public bool EnableMovementMonitor { get; set; }
|
||||||
|
|
||||||
|
public int MovementCheckInterval { get; set; } = 5;
|
||||||
|
|
||||||
|
public int MovementStuckThreshold { get; set; } = 30;
|
||||||
|
|
||||||
|
public bool EnableCombatHandling { get; set; }
|
||||||
|
|
||||||
|
public int CombatHPThreshold { get; set; } = 50;
|
||||||
|
|
||||||
|
public bool EnableDeathHandling { get; set; }
|
||||||
|
|
||||||
|
public int DeathRespawnDelay { get; set; } = 5;
|
||||||
|
|
||||||
|
public bool LogToDalamud { get; set; }
|
||||||
|
|
||||||
|
public MSQDisplayMode MSQDisplayMode { get; set; } = MSQDisplayMode.Overall;
|
||||||
|
|
||||||
|
public bool ShowPatchVersion { get; set; }
|
||||||
|
|
||||||
|
public string DCTravelDataCenter { get; set; } = "";
|
||||||
|
|
||||||
|
public string DCTravelTargetWorld { get; set; } = "";
|
||||||
|
|
||||||
|
public bool EnableDCTravelFeature { get; set; }
|
||||||
|
|
||||||
|
public bool EnableMultiModeAfterRotation { get; set; }
|
||||||
|
|
||||||
|
public bool ReturnToHomeworldOnStopQuest { get; set; }
|
||||||
|
|
||||||
|
public bool IsHighLevelHelper { get; set; }
|
||||||
|
|
||||||
|
public bool IsQuester { get; set; }
|
||||||
|
|
||||||
|
public List<HighLevelHelperConfig> HighLevelHelpers { get; set; } = new List<HighLevelHelperConfig>();
|
||||||
|
|
||||||
|
public bool ChauffeurModeEnabled { get; set; }
|
||||||
|
|
||||||
|
public float ChauffeurDistanceThreshold { get; set; } = 100f;
|
||||||
|
|
||||||
|
public float ChauffeurStopDistance { get; set; } = 5f;
|
||||||
|
|
||||||
|
public uint ChauffeurMountId { get; set; }
|
||||||
|
|
||||||
|
public string PreferredHelper { get; set; } = "";
|
||||||
|
|
||||||
|
public string AssignedQuester { get; set; } = "";
|
||||||
|
|
||||||
|
public HelperStatus CurrentHelperStatus { get; set; }
|
||||||
|
|
||||||
|
public bool EnableHelperFollowing { get; set; }
|
||||||
|
|
||||||
|
public float HelperFollowDistance { get; set; } = 100f;
|
||||||
|
|
||||||
|
public int HelperFollowCheckInterval { get; set; } = 5;
|
||||||
|
|
||||||
|
public string AssignedQuesterForFollowing { get; set; } = "";
|
||||||
|
|
||||||
|
public string AssignedHelperForFollowing { get; set; } = "";
|
||||||
|
|
||||||
|
public bool EnableSafeWaitBeforeCharacterSwitch { get; set; }
|
||||||
|
|
||||||
|
public bool EnableSafeWaitAfterCharacterSwitch { get; set; }
|
||||||
|
|
||||||
|
public bool EnableQuestPreCheck { get; set; }
|
||||||
|
|
||||||
|
public List<uint>? QuestPreCheckRange { get; set; }
|
||||||
|
|
||||||
|
public string SelectedDatacenter { get; set; } = "NA";
|
||||||
|
|
||||||
|
public Dictionary<string, List<string>> WorldsByDatacenter { get; set; } = new Dictionary<string, List<string>>
|
||||||
|
{
|
||||||
|
{
|
||||||
|
"NA",
|
||||||
|
new List<string>
|
||||||
|
{
|
||||||
|
"Adamantoise", "Cactuar", "Faerie", "Gilgamesh", "Jenova", "Midgardsormr", "Sargatanas", "Siren", "Behemoth", "Excalibur",
|
||||||
|
"Exodus", "Famfrit", "Hyperion", "Lamia", "Leviathan", "Ultros", "Balmung", "Brynhildr", "Coeurl", "Diabolos",
|
||||||
|
"Goblin", "Malboro", "Mateus", "Zalera", "Halicarnassus", "Maduin", "Marilith", "Seraph"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"EU",
|
||||||
|
new List<string>
|
||||||
|
{
|
||||||
|
"Cerberus", "Louisoix", "Moogle", "Omega", "Phantom", "Ragnarok", "Sagittarius", "Spriggan", "Alpha", "Lich",
|
||||||
|
"Odin", "Phoenix", "Raiden", "Shiva", "Twintania", "Zodiark"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"JP",
|
||||||
|
new List<string>
|
||||||
|
{
|
||||||
|
"Aegis", "Atomos", "Carbuncle", "Garuda", "Gungnir", "Kujata", "Tonberry", "Typhon", "Alexander", "Bahamut",
|
||||||
|
"Durandal", "Fenrir", "Ifrit", "Ridill", "Tiamat", "Ultima", "Anima", "Asura", "Chocobo", "Hades",
|
||||||
|
"Ixion", "Masamune", "Pandaemonium", "Titan", "Gaia", "Belias", "Mandragora", "Ramuh", "Shinryu", "Unicorn",
|
||||||
|
"Valefor", "Yojimbo", "Zeromus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"OCE",
|
||||||
|
new List<string> { "Bismarck", "Ravana", "Sephirot", "Sophia", "Zurvan" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public void Save()
|
||||||
|
{
|
||||||
|
Plugin.PluginInterface.SavePluginConfig(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public QuestProfile? GetActiveProfile()
|
||||||
|
{
|
||||||
|
return Profiles.Find((QuestProfile p) => p.Name == ActiveProfileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EnsureDefaultProfile()
|
||||||
|
{
|
||||||
|
if (Profiles.Count == 0)
|
||||||
|
{
|
||||||
|
QuestProfile defaultProfile = new QuestProfile
|
||||||
|
{
|
||||||
|
Name = "Default Profile",
|
||||||
|
IsActive = true
|
||||||
|
};
|
||||||
|
Profiles.Add(defaultProfile);
|
||||||
|
ActiveProfileName = defaultProfile.Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace QuestionableCompanion;
|
||||||
|
|
||||||
|
public enum HelperStatus
|
||||||
|
{
|
||||||
|
Available,
|
||||||
|
Transporting,
|
||||||
|
InDungeon
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace QuestionableCompanion;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class HighLevelHelperConfig
|
||||||
|
{
|
||||||
|
public string CharacterName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public ushort WorldId { get; set; }
|
||||||
|
|
||||||
|
public string WorldName { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace QuestionableCompanion;
|
||||||
|
|
||||||
|
public enum MSQDisplayMode
|
||||||
|
{
|
||||||
|
CurrentExpansion,
|
||||||
|
Overall,
|
||||||
|
ExpansionBreakdown
|
||||||
|
}
|
||||||
1020
QuestionableCompanion/QuestionableCompanion/Plugin.cs
Normal file
1020
QuestionableCompanion/QuestionableCompanion/Plugin.cs
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue