1
0
forked from liza/Questionable

Draft for auto-completing quest battles (HW MSQ)

This commit is contained in:
Liza 2025-02-20 01:34:59 +01:00
parent b35ee13704
commit 92873554cc
Signed by: liza
GPG Key ID: 2C41B84815CF6445
24 changed files with 359 additions and 46 deletions

View File

@ -123,6 +123,9 @@ internal static class QuestStepExtensions
Assignment(nameof(QuestStep.AutoDutyEnabled),
step.AutoDutyEnabled, emptyStep.AutoDutyEnabled)
.AsSyntaxNodeOrToken(),
Assignment(nameof(QuestStep.BossModEnabled),
step.BossModEnabled, emptyStep.BossModEnabled)
.AsSyntaxNodeOrToken(),
Assignment(nameof(QuestStep.SkipConditions), step.SkipConditions,
emptyStep.SkipConditions)
.AsSyntaxNodeOrToken(),

View File

@ -95,7 +95,9 @@
},
"TerritoryId": 138,
"InteractionType": "SinglePlayerDuty",
"Fly": true
"Fly": true,
"ContentFinderConditionId": 393,
"BossModEnabled": true
}
]
},

View File

@ -58,7 +58,9 @@
"Z": 349.96558
},
"TerritoryId": 401,
"InteractionType": "SinglePlayerDuty"
"InteractionType": "SinglePlayerDuty",
"ContentFinderConditionId": 395,
"BossModEnabled": true
}
]
},

View File

@ -78,7 +78,9 @@
"AethernetShortcut": [
"[Ishgard] The Forgotten Knight",
"[Ishgard] The Tribunal"
]
],
"ContentFinderConditionId": 396,
"BossModEnabled": true
}
]
},

View File

@ -28,7 +28,9 @@
"Z": 388.63196
},
"TerritoryId": 145,
"InteractionType": "SinglePlayerDuty"
"InteractionType": "SinglePlayerDuty",
"ContentFinderConditionId": 400,
"BossModEnabled": true
}
]
},

View File

@ -21,6 +21,16 @@
{
"Sequence": 1,
"Steps": [
{
"Position": {
"X": 474.62885,
"Y": 200.2377,
"Z": 657.9519
},
"TerritoryId": 397,
"InteractionType": "WalkTo",
"AetheryteShortcut": "Coerthas Western Highlands - Falcon's Nest"
},
{
"Position": {
"X": 486.38373,
@ -28,8 +38,7 @@
"Z": 239.54294
},
"TerritoryId": 397,
"InteractionType": "WalkTo",
"AetheryteShortcut": "Coerthas Western Highlands - Falcon's Nest"
"InteractionType": "WalkTo"
},
{
"Position": {
@ -69,7 +78,9 @@
},
"TerritoryId": 397,
"InteractionType": "SinglePlayerDuty",
"DisableNavmesh": true
"DisableNavmesh": true,
"ContentFinderConditionId": 397,
"BossModEnabled": true
}
]
},

View File

@ -59,7 +59,14 @@
"KillEnemyDataIds": [
4015
],
"$": "0 0 0 0 0 0 -> "
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
128
]
},
{
"Position": {
@ -72,6 +79,14 @@
"EnemySpawnType": "AutoOnEnterArea",
"KillEnemyDataIds": [
4015
],
"CompletionQuestVariablesFlags": [
null,
null,
null,
null,
null,
64
]
}
]

View File

@ -89,6 +89,16 @@
"InteractionType": "WalkTo",
"Mount": true
},
{
"Position": {
"X": -335.0186,
"Y": 13.983504,
"Z": -100.87753
},
"TerritoryId": 140,
"InteractionType": "WalkTo",
"Fly": true
},
{
"DataId": 1004019,
"Position": {
@ -98,7 +108,6 @@
},
"TerritoryId": 140,
"InteractionType": "Interact",
"Fly": true,
"TargetTerritoryId": 140
},
{

View File

@ -74,7 +74,9 @@
"Z": 37.247192
},
"TerritoryId": 418,
"InteractionType": "SinglePlayerDuty"
"InteractionType": "SinglePlayerDuty",
"ContentFinderConditionId": 398,
"BossModEnabled": true
}
]
},

View File

@ -56,7 +56,9 @@
"TerritoryId": 401,
"InteractionType": "SinglePlayerDuty",
"Emote": "lookout",
"StopDistance": 0.25
"StopDistance": 0.25,
"ContentFinderConditionId": 401,
"BossModEnabled": true
}
]
},

View File

@ -47,7 +47,9 @@
"AethernetShortcut": [
"[Idyllshire] Aetheryte Plaza",
"[Idyllshire] Epilogue Gate (Eastern Hinterlands)"
]
],
"ContentFinderConditionId": 422,
"BossModEnabled": true
}
]
},

View File

@ -68,7 +68,9 @@
"Z": 553.97876
},
"TerritoryId": 402,
"InteractionType": "SinglePlayerDuty"
"InteractionType": "SinglePlayerDuty",
"ContentFinderConditionId": 399,
"BossModEnabled": true
}
]
},

View File

@ -1257,6 +1257,27 @@
]
}
},
{
"if": {
"properties": {
"InteractionType": {
"const": "SinglePlayerDuty"
}
}
},
"then": {
"properties": {
"ContentFinderConditionId": {
"type": "integer",
"exclusiveMinimum": 0,
"exclusiveMaximum": 3000
},
"BossModEnabled": {
"type": "boolean"
}
}
}
},
{
"if": {
"properties": {

View File

@ -75,6 +75,7 @@ public sealed class QuestStep
public JumpDestination? JumpDestination { get; set; }
public uint? ContentFinderConditionId { get; set; }
public bool AutoDutyEnabled { get; set; }
public bool BossModEnabled { get; set; }
public SkipConditions? SkipConditions { get; set; }
public List<List<QuestWorkValue>?> RequiredQuestVariables { get; set; } = new();

View File

@ -9,33 +9,26 @@ using Questionable.Model;
using System;
using System.IO;
using System.Numerics;
using Questionable.External;
namespace Questionable.Controller.CombatModules;
internal sealed class BossModModule : ICombatModule, IDisposable
{
private const string Name = "BossMod";
private readonly ILogger<BossModModule> _logger;
private readonly BossModIpc _bossModIpc;
private readonly Configuration _configuration;
private readonly ICallGateSubscriber<string, string?> _getPreset;
private readonly ICallGateSubscriber<string, bool, bool> _createPreset;
private readonly ICallGateSubscriber<string, bool> _setPreset;
private readonly ICallGateSubscriber<bool> _clearPreset;
private static Stream Preset => typeof(BossModModule).Assembly.GetManifestResourceStream("Questionable.Controller.CombatModules.BossModPreset")!;
public BossModModule(
ILogger<BossModModule> logger,
IDalamudPluginInterface pluginInterface,
BossModIpc bossModIpc,
Configuration configuration)
{
_logger = logger;
_bossModIpc = bossModIpc;
_configuration = configuration;
_getPreset = pluginInterface.GetIpcSubscriber<string, string?>($"{Name}.Presets.Get");
_createPreset = pluginInterface.GetIpcSubscriber<string, bool, bool>($"{Name}.Presets.Create");
_setPreset = pluginInterface.GetIpcSubscriber<string, bool>($"{Name}.Presets.SetActive");
_clearPreset = pluginInterface.GetIpcSubscriber<bool>($"{Name}.Presets.ClearActive");
}
public bool CanHandleFight(CombatController.CombatData combatData)
@ -43,26 +36,19 @@ internal sealed class BossModModule : ICombatModule, IDisposable
if (_configuration.General.CombatModule != Configuration.ECombatModule.BossMod)
return false;
try
{
return _getPreset.HasFunction;
}
catch (IpcError)
{
return false;
}
return _bossModIpc.IsSupported();
}
public bool Start(CombatController.CombatData combatData)
{
try
{
if (_getPreset.InvokeFunc("Questionable") == null)
if (_bossModIpc.GetPreset("Questionable") == null)
{
using var reader = new StreamReader(Preset);
_logger.LogInformation("Loading Questionable BossMod Preset: {LoadedState}", _createPreset.InvokeFunc(reader.ReadToEnd(), true));
_logger.LogInformation("Loading Questionable BossMod Preset: {LoadedState}", _bossModIpc.CreatePreset(reader.ReadToEnd(), true));
}
_setPreset.InvokeFunc("Questionable");
_bossModIpc.SetPreset("Questionable");
return true;
}
catch (IpcError e)
@ -76,7 +62,7 @@ internal sealed class BossModModule : ICombatModule, IDisposable
{
try
{
_clearPreset.InvokeFunc();
_bossModIpc.ClearPreset();
return true;
}
catch (IpcError e)

View File

@ -598,14 +598,14 @@ internal sealed class InteractionUiController : IDisposable
if (checkAllSteps)
{
var sequence = quest.FindSequence(currentQuest.Sequence);
if (sequence != null && HandleDefaultYesNo(addonSelectYesno, quest,
sequence.Steps.SelectMany(x => x.DialogueChoices).ToList(), actualPrompt))
if (sequence != null &&
sequence.Steps.Any(step => HandleDefaultYesNo(addonSelectYesno, quest, step, step.DialogueChoices, actualPrompt)))
return true;
}
else
{
var step = quest.FindSequence(currentQuest.Sequence)?.FindStep(currentQuest.Step);
if (step != null && HandleDefaultYesNo(addonSelectYesno, quest, step.DialogueChoices, actualPrompt))
if (step != null && HandleDefaultYesNo(addonSelectYesno, quest, step, step.DialogueChoices, actualPrompt))
return true;
}
@ -619,7 +619,7 @@ internal sealed class InteractionUiController : IDisposable
Yes = true
};
if (HandleDefaultYesNo(addonSelectYesno, quest, [dialogueChoice], actualPrompt))
if (HandleDefaultYesNo(addonSelectYesno, quest, null, [dialogueChoice], actualPrompt))
return true;
}
@ -630,7 +630,7 @@ internal sealed class InteractionUiController : IDisposable
}
private unsafe bool HandleDefaultYesNo(AddonSelectYesno* addonSelectYesno, Quest quest,
List<DialogueChoice> dialogueChoices, string actualPrompt)
QuestStep? step, List<DialogueChoice> dialogueChoices, string actualPrompt)
{
_logger.LogTrace("DefaultYesNo: Choice count: {Count}", dialogueChoices.Count);
foreach (var dialogueChoice in dialogueChoices)
@ -659,6 +659,13 @@ internal sealed class InteractionUiController : IDisposable
return true;
}
if (step is { InteractionType: EInteractionType.SinglePlayerDuty, BossModEnabled: true })
{
_logger.LogTrace("DefaultYesNo: probably Single Player Duty");
addonSelectYesno->AtkUnitBase.FireCallbackInt(0);
return true;
}
return false;
}

View File

@ -150,7 +150,8 @@ internal sealed class QuestRegistry
foreach (var quest in _quests.Values)
{
foreach (var dutyStep in quest.AllSteps().Where(x =>
x.Step.InteractionType == EInteractionType.Duty && x.Step.ContentFinderConditionId != null))
x.Step.InteractionType is EInteractionType.Duty or EInteractionType.SinglePlayerDuty
&& x.Step.ContentFinderConditionId != null))
{
_contentFinderConditionIds[dutyStep.Step.ContentFinderConditionId!.Value] = (quest.Id, dutyStep.Step);
}

View File

@ -26,7 +26,8 @@ internal static class SendNotification
new Task(step.InteractionType, step.ContentFinderConditionId.HasValue
? territoryData.GetContentFinderCondition(step.ContentFinderConditionId.Value)?.Name
: step.Comment),
EInteractionType.SinglePlayerDuty => new Task(step.InteractionType, quest.Info.Name),
EInteractionType.SinglePlayerDuty when !step.BossModEnabled =>
new Task(step.InteractionType, quest.Info.Name),
_ => null,
};
}

View File

@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using Dalamud.Plugin.Services;
using Questionable.Controller.Steps.Shared;
using Questionable.Data;
using Questionable.External;
using Questionable.Model;
using Questionable.Model.Questing;
namespace Questionable.Controller.Steps.Interactions;
internal static class SinglePlayerDuty
{
internal sealed class Factory : ITaskFactory
{
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
{
if (step.InteractionType != EInteractionType.SinglePlayerDuty)
yield break;
if (step.BossModEnabled)
{
ArgumentNullException.ThrowIfNull(step.ContentFinderConditionId);
yield return new StartSinglePlayerDuty(step.ContentFinderConditionId.Value);
yield return new EnableAi();
yield return new WaitSinglePlayerDuty(step.ContentFinderConditionId.Value);
yield return new DisableAi();
yield return new WaitAtEnd.WaitNextStepOrSequence();
}
}
}
internal sealed record StartSinglePlayerDuty(uint ContentFinderConditionId) : ITask
{
public override string ToString() => $"Wait(BossMod, entered instance {ContentFinderConditionId})";
}
internal sealed class StartSinglePlayerDutyExecutor(
TerritoryData territoryData,
IClientState clientState) : TaskExecutor<StartSinglePlayerDuty>
{
protected override bool Start() => true;
public override ETaskResult Update()
{
if (!territoryData.TryGetContentFinderCondition(Task.ContentFinderConditionId,
out var cfcData))
throw new TaskException("Failed to get territory ID for content finder condition");
return clientState.TerritoryType == cfcData.TerritoryId
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;
}
public override bool ShouldInterruptOnDamage() => false;
}
internal sealed record EnableAi : ITask
{
public override string ToString() => "BossMod.EnableAi";
}
internal sealed class EnableAiExecutor(
BossModIpc bossModIpc) : TaskExecutor<EnableAi>
{
protected override bool Start()
{
bossModIpc.EnableAi();
return true;
}
public override ETaskResult Update() => ETaskResult.TaskComplete;
public override bool ShouldInterruptOnDamage() => false;
}
internal sealed record WaitSinglePlayerDuty(uint ContentFinderConditionId) : ITask
{
public override string ToString() => $"Wait(BossMod, left instance {ContentFinderConditionId})";
}
internal sealed class WaitSinglePlayerDutyExecutor(
TerritoryData territoryData,
IClientState clientState,
BossModIpc bossModIpc) : TaskExecutor<WaitSinglePlayerDuty>, IStoppableTaskExecutor
{
protected override bool Start() => true;
public override ETaskResult Update()
{
if (!territoryData.TryGetContentFinderCondition(Task.ContentFinderConditionId,
out var cfcData))
throw new TaskException("Failed to get territory ID for content finder condition");
return clientState.TerritoryType != cfcData.TerritoryId
? ETaskResult.TaskComplete
: ETaskResult.StillRunning;
}
public void StopNow() => bossModIpc.DisableAi();
public override bool ShouldInterruptOnDamage() => false;
}
internal sealed record DisableAi : ITask
{
public override string ToString() => "BossMod.DisableAi";
}
internal sealed class DisableAiExecutor(
BossModIpc bossModIpc) : TaskExecutor<DisableAi>
{
protected override bool Start()
{
bossModIpc.DisableAi();
return true;
}
public override ETaskResult Update() => ETaskResult.TaskComplete;
public override bool ShouldInterruptOnDamage() => false;
}
}

View File

@ -53,7 +53,7 @@ internal static class WaitAtEnd
return [new WaitNextStepOrSequence()];
case EInteractionType.Duty when !autoDutyIpc.IsConfiguredToRunContent(step.ContentFinderConditionId, step.AutoDutyEnabled):
case EInteractionType.SinglePlayerDuty:
case EInteractionType.SinglePlayerDuty when !step.BossModEnabled:
return [new EndAutomation()];
case EInteractionType.WalkTo:

View File

@ -1,8 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps.Interactions;
using Questionable.Data;
using Questionable.Model;
using Questionable.Model.Questing;
@ -11,11 +14,19 @@ namespace Questionable.Controller.Steps;
internal sealed class TaskCreator
{
private readonly IServiceProvider _serviceProvider;
private readonly TerritoryData _territoryData;
private readonly IClientState _clientState;
private readonly ILogger<TaskCreator> _logger;
public TaskCreator(IServiceProvider serviceProvider, ILogger<TaskCreator> logger)
public TaskCreator(
IServiceProvider serviceProvider,
TerritoryData territoryData,
IClientState clientState,
ILogger<TaskCreator> logger)
{
_serviceProvider = serviceProvider;
_territoryData = territoryData;
_clientState = clientState;
_logger = logger;
}
@ -40,6 +51,31 @@ internal sealed class TaskCreator
return tasks;
})
.ToList();
var singlePlayerDutyTask = newTasks
.Where(y => y is SinglePlayerDuty.StartSinglePlayerDuty)
.Cast<SinglePlayerDuty.StartSinglePlayerDuty>()
.FirstOrDefault();
if (singlePlayerDutyTask != null &&
_territoryData.TryGetContentFinderCondition(singlePlayerDutyTask.ContentFinderConditionId,
out var cfcData))
{
// if we have a single player duty in queue, we check if we're in the matching territory
// if yes, skip all steps before (e.g. teleporting, waiting for navmesh, moving, interacting)
if (_clientState.TerritoryType == cfcData.TerritoryId)
{
int index = newTasks.IndexOf(singlePlayerDutyTask);
_logger.LogWarning(
"Skipping {SkippedTaskCount} out of {TotalCount} tasks, questionable was started while in single player duty",
index + 1, newTasks.Count);
newTasks.RemoveRange(0, index + 1);
_logger.LogInformation("Next actual task: {NextTask}, total tasks left: {RemainingTaskCount}",
newTasks.FirstOrDefault(),
newTasks.Count);
}
}
if (newTasks.Count == 0)
_logger.LogInformation("Nothing to execute for step?");
else

View File

@ -45,7 +45,7 @@ internal sealed class TerritoryData
.ToImmutableDictionary(x => x.Content.RowId, x => x.Name.ToDalamudString().ToString());
_contentFinderConditions = dataManager.GetExcelSheet<ContentFinderCondition>()
.Where(x => x.RowId > 0 && x.Content.RowId != 0 && x.ContentLinkType == 1 && x.ContentType.RowId != 6)
.Where(x => x.RowId > 0 && x.Content.RowId != 0 && x.ContentLinkType is 1 or 5 && x.ContentType.RowId != 6)
.Select(x => new ContentFinderConditionData(x, dataManager.Language))
.ToImmutableDictionary(x => x.ContentFinderConditionId, x => x);
}

73
Questionable/External/BossModIpc.cs vendored Normal file
View File

@ -0,0 +1,73 @@
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc;
using Dalamud.Plugin.Ipc.Exceptions;
using Dalamud.Plugin.Services;
namespace Questionable.External;
internal sealed class BossModIpc
{
private readonly ICommandManager _commandManager;
private const string Name = "BossMod";
private readonly ICallGateSubscriber<string, string?> _getPreset;
private readonly ICallGateSubscriber<string, bool, bool> _createPreset;
private readonly ICallGateSubscriber<string, bool> _setPreset;
private readonly ICallGateSubscriber<bool> _clearPreset;
public BossModIpc(IDalamudPluginInterface pluginInterface, ICommandManager commandManager)
{
_commandManager = commandManager;
_getPreset = pluginInterface.GetIpcSubscriber<string, string?>($"{Name}.Presets.Get");
_createPreset = pluginInterface.GetIpcSubscriber<string, bool, bool>($"{Name}.Presets.Create");
_setPreset = pluginInterface.GetIpcSubscriber<string, bool>($"{Name}.Presets.SetActive");
_clearPreset = pluginInterface.GetIpcSubscriber<bool>($"{Name}.Presets.ClearActive");
}
public bool IsSupported()
{
try
{
return _getPreset.HasFunction;
}
catch (IpcError)
{
return false;
}
}
public string? GetPreset(string name)
{
return _getPreset.InvokeFunc(name);
}
public bool CreatePreset(string name, bool overwrite)
{
return _createPreset.InvokeFunc(name, overwrite);
}
public void SetPreset(string name)
{
_setPreset.InvokeFunc(name);
}
public void ClearPreset()
{
_clearPreset.InvokeFunc();
}
// TODO this should use your actual rotation plugin, not always vbm
public void EnableAi(string presetName = "VBM Default")
{
_commandManager.ProcessCommand("/vbmai on");
_commandManager.ProcessCommand("/vbm cfg ZoneModuleConfig EnableQuestBattles true");
SetPreset(presetName);
}
public void DisableAi()
{
_commandManager.ProcessCommand("/vbmai off");
_commandManager.ProcessCommand("/vbm cfg ZoneModuleConfig EnableQuestBattles false");
ClearPreset();
}
}

View File

@ -131,6 +131,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton<NotificationMasterIpc>();
serviceCollection.AddSingleton<AutomatonIpc>();
serviceCollection.AddSingleton<AutoDutyIpc>();
serviceCollection.AddSingleton<BossModIpc>();
serviceCollection.AddSingleton<GearStatsCalculator>();
}
@ -222,6 +223,14 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddTaskExecutor<InitiateLeve.Initiate, InitiateLeve.InitiateExecutor>();
serviceCollection.AddTaskExecutor<InitiateLeve.SelectDifficulty, InitiateLeve.SelectDifficultyExecutor>();
serviceCollection.AddTaskFactory<SinglePlayerDuty.Factory>();
serviceCollection
.AddTaskExecutor<SinglePlayerDuty.StartSinglePlayerDuty, SinglePlayerDuty.StartSinglePlayerDutyExecutor>();
serviceCollection.AddTaskExecutor<SinglePlayerDuty.EnableAi, SinglePlayerDuty.EnableAiExecutor>();
serviceCollection
.AddTaskExecutor<SinglePlayerDuty.WaitSinglePlayerDuty, SinglePlayerDuty.WaitSinglePlayerDutyExecutor>();
serviceCollection.AddTaskExecutor<SinglePlayerDuty.DisableAi, SinglePlayerDuty.DisableAiExecutor>();
serviceCollection.AddTaskExecutor<WaitCondition.Task, WaitCondition.WaitConditionExecutor>();
serviceCollection.AddTaskFactory<WaitAtEnd.Factory>();
serviceCollection.AddTaskExecutor<WaitAtEnd.WaitDelay, WaitAtEnd.WaitDelayExecutor>();