diff --git a/CHANGELOG.md b/CHANGELOG.md index 57b18ea9e..3f4844c67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,2 @@ -- Feature: Priority preset to unlock Level Cap Dungeons roulette -alydev \ No newline at end of file +- Bug fix: unload error resolved. You may need to restart your game to properly unload Questionable to load this new update, apologies. -alydev +- Feature: Added override so quest paths can specify a target for the start of solo duties -alydev \ No newline at end of file diff --git a/Directory.Build.targets b/Directory.Build.targets index ffe6da0ea..3debfebb4 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,6 +1,6 @@ - 15.306.3.19 + 15.306.3.20 diff --git a/QuestPaths/4.x - Stormblood/Allied Societies/Kojin/Story/2977_A Test of Courage.json b/QuestPaths/4.x - Stormblood/Allied Societies/Kojin/Story/2977_A Test of Courage.json index 8b753d483..8d7f13fed 100644 --- a/QuestPaths/4.x - Stormblood/Allied Societies/Kojin/Story/2977_A Test of Courage.json +++ b/QuestPaths/4.x - Stormblood/Allied Societies/Kojin/Story/2977_A Test of Courage.json @@ -76,7 +76,7 @@ "TerritoryId": 613, "InteractionType": "SinglePlayerDuty", "Comment": "Please report in discord whether this solo duty works fine.\nIt has not been tested yet and may fail. Thank you.", - "SinglePlayerDutyOptions": {"Enabled": true}, + "SinglePlayerDutyOptions": {"Enabled": false}, "Fly": true } ] diff --git a/QuestPaths/7.x - Dawntrail/Role Quests/Healer/4829_An Antidote for Anarchy.json b/QuestPaths/7.x - Dawntrail/Role Quests/Healer/4829_An Antidote for Anarchy.json index d3e23f743..17fd74819 100644 --- a/QuestPaths/7.x - Dawntrail/Role Quests/Healer/4829_An Antidote for Anarchy.json +++ b/QuestPaths/7.x - Dawntrail/Role Quests/Healer/4829_An Antidote for Anarchy.json @@ -47,7 +47,7 @@ }, "TerritoryId": 137, "InteractionType": "SinglePlayerDuty", - "SinglePlayerDutyOptions": {"Enabled": true} + "SinglePlayerDutyOptions": {"Enabled": true, "Target": 16983} } ] }, diff --git a/QuestPaths/7.x - Dawntrail/Role Quests/Magical Ranged/4847_Heroes and Pretenders.json b/QuestPaths/7.x - Dawntrail/Role Quests/Magical Ranged/4847_Heroes and Pretenders.json index 3b965e540..406cc1438 100644 --- a/QuestPaths/7.x - Dawntrail/Role Quests/Magical Ranged/4847_Heroes and Pretenders.json +++ b/QuestPaths/7.x - Dawntrail/Role Quests/Magical Ranged/4847_Heroes and Pretenders.json @@ -31,7 +31,7 @@ }, "TerritoryId": 957, "InteractionType": "SinglePlayerDuty", - "SinglePlayerDutyOptions": {"Enabled": true}, + "SinglePlayerDutyOptions": {"Enabled": true, "Target": 17034}, "AetheryteShortcut": "Radz-at-Han", "AethernetShortcut": [ "[Radz-at-Han] Aetheryte Plaza", diff --git a/QuestPaths/quest-v1.json b/QuestPaths/quest-v1.json index 3a3a4f17e..afcb9b91e 100644 --- a/QuestPaths/quest-v1.json +++ b/QuestPaths/quest-v1.json @@ -1739,6 +1739,10 @@ "maximum": 1, "description": "If a quest has multiple solo instances (which affects 5 quests total), indicates which one this is" }, + "Target": { + "type": "integer", + "description": "The DataId to target at the start of the instance" + }, "TestedBossModVersion": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+\\.\\d+$" diff --git a/Questionable.Model/Questing/SinglePlayerDutyOptions.cs b/Questionable.Model/Questing/SinglePlayerDutyOptions.cs index 8efc474cc..ef604022d 100644 --- a/Questionable.Model/Questing/SinglePlayerDutyOptions.cs +++ b/Questionable.Model/Questing/SinglePlayerDutyOptions.cs @@ -6,4 +6,5 @@ public sealed class SinglePlayerDutyOptions public bool Enabled { get; set; } public List Notes { get; set; } = []; public byte Index { get; set; } + public int? Target { get; set; } } diff --git a/Questionable/Controller/MovementController.cs b/Questionable/Controller/MovementController.cs index 8a0f98310..8678e4df3 100644 --- a/Questionable/Controller/MovementController.cs +++ b/Questionable/Controller/MovementController.cs @@ -26,7 +26,7 @@ internal sealed class MovementController AetheryteData aetheryteData, ICommandManager commandManager, IChatGui chatGui, - IServiceProvider serviceProvider, + Lazy questController, ILogger logger) : IDisposable { public const float DefaultVerticalInteractionDistance = 1.95f; @@ -168,7 +168,7 @@ public void Update() } } - if (serviceProvider.GetRequiredService().IsQuestingStopped) + if (questController.Value.IsQuestingStopped) { // if (EzThrottler.Throttle("qstwouldhavejumpedin", 5000)) // logger.LogDebug("Questionable would have jumped in here to do something, but decided against it."); diff --git a/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs b/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs index 6e7d12692..03be951b8 100644 --- a/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs +++ b/Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs @@ -46,6 +46,7 @@ internal static class SpecialTerritories public const ushort EgistentialCrisis = 701; public const ushort Nightkin = 676; public const ushort WarmthOfFamily = 1244; + public const ushort BarThePassage = 1246; } internal sealed class Factory @@ -253,6 +254,40 @@ public IEnumerable CreateAllTasks(Quest quest, QuestSequence sequence, Qu "Wait(leg exposed)"); yield return new SetTarget(17992); } + else if (tId == SpecialTerritories.BarThePassage) + { + yield return new EnableAi(); + yield return new WaitCondition.Task( + () => + { + if (clientState.TerritoryType != SpecialTerritories.BarThePassage) + return true; + return !condition[ConditionFlag.SufferingStatusAffliction63]; + }, + "Wait(in event)"); + Vector3[] points = [ + new(0f, 0f, -300f), + new(0f, 0f, -300f), + new(0f, 0f, -270f), + new(0f, 0f, 78f), + new(0f, 0f, 103f), + new(0f, 0f, 361f), + ]; + foreach (Vector3 point in points) + { + yield return new WaitAtEnd.WaitDelay(TimeSpan.FromSeconds(2)); + yield return new WaitCondition.Task( + () => + { + if (clientState.TerritoryType != SpecialTerritories.BarThePassage) + return true; + return !condition[ConditionFlag.InCombat]; + }, + "Wait(in combat)"); + yield return new MoveTask(SpecialTerritories.BarThePassage, point); + } + yield return new SetTarget(18032); + } //else if (tId == SpecialTerritories.ViperTutorial) //{ @@ -288,6 +323,9 @@ public IEnumerable CreateAllTasks(Quest quest, QuestSequence sequence, Qu else yield return new EnableAi(tId == SpecialTerritories.Naadam); + if (step.SinglePlayerDutyOptions?.Target != null) + yield return new SetTarget((uint)step.SinglePlayerDutyOptions.Target); + yield return new WaitSinglePlayerDuty(cfcId); yield return new DisableAi(); yield return new WaitForSinglePlayerDutyOutcome(cfcId, quest.Id, sequence.Sequence); diff --git a/Questionable/DalamudInitializer.cs b/Questionable/DalamudInitializer.cs index 894723ed3..8a5de5d1a 100644 --- a/Questionable/DalamudInitializer.cs +++ b/Questionable/DalamudInitializer.cs @@ -20,6 +20,7 @@ internal sealed class DalamudInitializer : IDisposable private readonly IChatGui _chatGui; private readonly IToastGui _toastGui; private readonly WindowSystem _windowSystem; + private bool _disposed; public DalamudInitializer( IDalamudPluginInterface pluginInterface, @@ -87,6 +88,13 @@ public DalamudInitializer( public void Dispose() { + // Idempotent: QuestionablePlugin.DisposeAsync calls this up-front to unhook Dalamud event + // sources before the DI container is disposed, and MS.DI then calls it again during the + // container's own disposal walk. Second call must be a no-op. + if (_disposed) + return; + _disposed = true; + _toastGui.QuestToast -= OnQuestToast; _toastGui.ErrorToast -= OnErrorToast; _toastGui.Toast -= OnToast; diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index f30cd38fd..3b1144a9a 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -52,6 +52,24 @@ public async Task LoadAsync(CancellationToken cancellationToken) public async ValueTask DisposeAsync() { var serviceProvider = Interlocked.Exchange(ref _serviceProvider, value: null); + + // Unhook Dalamud event sources (Framework.Update, UiBuilder callbacks, toast hooks, ...) + // before disposing the container. MS.DI marks the root scope as disposed at the *start* of + // Dispose/DisposeAsync, so any GetService call after that point throws ObjectDisposedException. + // Under IAsyncDalamudPlugin.DisposeAsync the framework thread can tick between "scope flagged + // disposed" and DalamudInitializer being reached in the disposal walk, and other singletons' + // per-frame container access would blow up. Disposing DalamudInitializer up-front removes the + // event subscriptions before that window opens; its Dispose is idempotent so MS.DI's later + // disposal pass is a no-op. + try + { + serviceProvider?.GetService()?.Dispose(); + } + catch (ObjectDisposedException) + { + // Container already torn down elsewhere — nothing to unhook. + } + if (serviceProvider is IAsyncDisposable asyncDisposable) await asyncDisposable.DisposeAsync().ConfigureAwait(false); else @@ -137,6 +155,11 @@ private ServiceProvider BuildAndInitialize() serviceCollection.AddSingleton(sp => sp.GetRequiredService()); serviceCollection.AddSingleton(sp => sp.GetRequiredService()); + // Breaks the QuestController <-> MovementController ctor cycle without handing MovementController + // the whole IServiceProvider. Once .Value is evaluated the container isn't touched again, so a + // framework tick during shutdown can't hit a disposed scope through this path. + serviceCollection.AddSingleton(sp => new Lazy(sp.GetRequiredService)); + var serviceProvider = serviceCollection.BuildServiceProvider(); Initialize(serviceProvider); return serviceProvider; diff --git a/Questionable/Windows/QuestComponents/CreationUtilsComponent.cs b/Questionable/Windows/QuestComponents/CreationUtilsComponent.cs index 1fb9717fc..9dad7ea86 100644 --- a/Questionable/Windows/QuestComponents/CreationUtilsComponent.cs +++ b/Questionable/Windows/QuestComponents/CreationUtilsComponent.cs @@ -264,7 +264,8 @@ private unsafe void DrawSavedDetails() { if (objectTable[0] != null) { - ImGui.Text(_LF("Distance: {0:F2} ({1}y)", + ImGui.Text($"<{_savedPos.Value.X:F3},{_savedPos.Value.Y:F3},{_savedPos.Value.Z:F3}>" + + _LF("Distance: {0:F2} ({1}y)", (_savedPos.Value - objectTable[0]!.Position).Length(), Math.Floor(_savedPos.Value.DistanceTo_XZ(objectTable[0]!.Position)) - 1)); ImGui.SameLine();