Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
- Feature: Priority preset to unlock Level Cap Dungeons roulette -alydev
- 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
2 changes: 1 addition & 1 deletion Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup Condition="$(MSBuildProjectName) != 'GatheringPathRenderer'">
<Version>15.306.3.19</Version>
<Version>15.306.3.20</Version>
</PropertyGroup>

<PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
},
"TerritoryId": 137,
"InteractionType": "SinglePlayerDuty",
"SinglePlayerDutyOptions": {"Enabled": true}
"SinglePlayerDutyOptions": {"Enabled": true, "Target": 16983}
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions QuestPaths/quest-v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -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+$"
Expand Down
1 change: 1 addition & 0 deletions Questionable.Model/Questing/SinglePlayerDutyOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ public sealed class SinglePlayerDutyOptions
public bool Enabled { get; set; }
public List<string> Notes { get; set; } = [];
public byte Index { get; set; }
public int? Target { get; set; }
}
4 changes: 2 additions & 2 deletions Questionable/Controller/MovementController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ internal sealed class MovementController
AetheryteData aetheryteData,
ICommandManager commandManager,
IChatGui chatGui,
IServiceProvider serviceProvider,
Lazy<QuestController> questController,
ILogger<MovementController> logger) : IDisposable
{
public const float DefaultVerticalInteractionDistance = 1.95f;
Expand Down Expand Up @@ -168,7 +168,7 @@ public void Update()
}
}

if (serviceProvider.GetRequiredService<QuestController>().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.");
Expand Down
38 changes: 38 additions & 0 deletions Questionable/Controller/Steps/Interactions/SinglePlayerDuty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -253,6 +254,40 @@ public IEnumerable<ITask> 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)
//{
Expand Down Expand Up @@ -288,6 +323,9 @@ public IEnumerable<ITask> 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);
Expand Down
8 changes: 8 additions & 0 deletions Questionable/DalamudInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
23 changes: 23 additions & 0 deletions Questionable/QuestionablePlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<DalamudInitializer>()?.Dispose();
}
catch (ObjectDisposedException)
{
// Container already torn down elsewhere — nothing to unhook.
}

if (serviceProvider is IAsyncDisposable asyncDisposable)
await asyncDisposable.DisposeAsync().ConfigureAwait(false);
else
Expand Down Expand Up @@ -137,6 +155,11 @@ private ServiceProvider BuildAndInitialize()
serviceCollection.AddSingleton<IAetheryteTerritoryProvider>(sp => sp.GetRequiredService<AetheryteData>());
serviceCollection.AddSingleton<IQuestValidator>(sp => sp.GetRequiredService<JsonSchemaValidator>());

// 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<QuestController>(sp.GetRequiredService<QuestController>));

var serviceProvider = serviceCollection.BuildServiceProvider();
Initialize(serviceProvider);
return serviceProvider;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading