Second draft for auto-completing quest battles
This commit is contained in:
parent
92873554cc
commit
097c67ed5d
@ -274,7 +274,7 @@ public sealed class RendererPlugin : IDalamudPlugin
|
||||
locationOverride?.MaximumDistance ?? x.CalculateMaximumDistance(),
|
||||
minimumAngle, maximumAngle, color | 0xFF000000);
|
||||
|
||||
drawList.AddText(x.Position, 0xFFFFFFFF, $"{location.Root.Groups.IndexOf(group)} // {node.DataId} / {node.Locations.IndexOf(x)} || {minimumAngle}, {maximumAngle}", 1f);
|
||||
drawList.AddText(x.Position, isUnsaved ? 0xFFFF0000 : 0xFFFFFFFF, $"{location.Root.Groups.IndexOf(group)} // {node.DataId} / {node.Locations.IndexOf(x)} || {minimumAngle}, {maximumAngle}", 1f);
|
||||
#if false
|
||||
var a = GatheringMath.CalculateLandingLocation(x, 0, 0);
|
||||
var b = GatheringMath.CalculateLandingLocation(x, 1, 1);
|
||||
|
@ -126,6 +126,9 @@ internal static class QuestStepExtensions
|
||||
Assignment(nameof(QuestStep.BossModEnabled),
|
||||
step.BossModEnabled, emptyStep.BossModEnabled)
|
||||
.AsSyntaxNodeOrToken(),
|
||||
Assignment(nameof(QuestStep.SinglePlayerDutyIndex),
|
||||
step.SinglePlayerDutyIndex, emptyStep.SinglePlayerDutyIndex)
|
||||
.AsSyntaxNodeOrToken(),
|
||||
Assignment(nameof(QuestStep.SkipConditions), step.SkipConditions,
|
||||
emptyStep.SkipConditions)
|
||||
.AsSyntaxNodeOrToken(),
|
||||
|
@ -57,6 +57,7 @@
|
||||
},
|
||||
"TerritoryId": 153,
|
||||
"InteractionType": "SinglePlayerDuty",
|
||||
"SinglePlayerDutyIndex": 1,
|
||||
"Fly": true
|
||||
}
|
||||
]
|
||||
|
@ -62,6 +62,7 @@
|
||||
},
|
||||
"TerritoryId": 154,
|
||||
"InteractionType": "SinglePlayerDuty",
|
||||
"SinglePlayerDutyIndex": 1,
|
||||
"AetheryteShortcut": "North Shroud - Fallgourd Float",
|
||||
"Fly": true
|
||||
}
|
||||
|
@ -119,7 +119,8 @@
|
||||
"Z": 29.06836
|
||||
},
|
||||
"TerritoryId": 152,
|
||||
"InteractionType": "SinglePlayerDuty"
|
||||
"InteractionType": "SinglePlayerDuty",
|
||||
"SinglePlayerDutyIndex": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -92,6 +92,7 @@
|
||||
},
|
||||
"TerritoryId": 130,
|
||||
"InteractionType": "SinglePlayerDuty",
|
||||
"SinglePlayerDutyIndex": 1,
|
||||
"AetheryteShortcut": "Ul'dah"
|
||||
}
|
||||
]
|
||||
|
@ -96,7 +96,6 @@
|
||||
"TerritoryId": 138,
|
||||
"InteractionType": "SinglePlayerDuty",
|
||||
"Fly": true,
|
||||
"ContentFinderConditionId": 393,
|
||||
"BossModEnabled": true
|
||||
}
|
||||
]
|
||||
|
@ -59,7 +59,6 @@
|
||||
},
|
||||
"TerritoryId": 401,
|
||||
"InteractionType": "SinglePlayerDuty",
|
||||
"ContentFinderConditionId": 395,
|
||||
"BossModEnabled": true
|
||||
}
|
||||
]
|
||||
|
@ -79,7 +79,6 @@
|
||||
"[Ishgard] The Forgotten Knight",
|
||||
"[Ishgard] The Tribunal"
|
||||
],
|
||||
"ContentFinderConditionId": 396,
|
||||
"BossModEnabled": true
|
||||
}
|
||||
]
|
||||
|
@ -29,7 +29,6 @@
|
||||
},
|
||||
"TerritoryId": 145,
|
||||
"InteractionType": "SinglePlayerDuty",
|
||||
"ContentFinderConditionId": 400,
|
||||
"BossModEnabled": true
|
||||
}
|
||||
]
|
||||
|
@ -79,7 +79,6 @@
|
||||
"TerritoryId": 397,
|
||||
"InteractionType": "SinglePlayerDuty",
|
||||
"DisableNavmesh": true,
|
||||
"ContentFinderConditionId": 397,
|
||||
"BossModEnabled": true
|
||||
}
|
||||
]
|
||||
|
@ -75,7 +75,6 @@
|
||||
},
|
||||
"TerritoryId": 418,
|
||||
"InteractionType": "SinglePlayerDuty",
|
||||
"ContentFinderConditionId": 398,
|
||||
"BossModEnabled": true
|
||||
}
|
||||
]
|
||||
|
@ -57,7 +57,6 @@
|
||||
"InteractionType": "SinglePlayerDuty",
|
||||
"Emote": "lookout",
|
||||
"StopDistance": 0.25,
|
||||
"ContentFinderConditionId": 401,
|
||||
"BossModEnabled": true
|
||||
}
|
||||
]
|
||||
|
@ -48,7 +48,6 @@
|
||||
"[Idyllshire] Aetheryte Plaza",
|
||||
"[Idyllshire] Epilogue Gate (Eastern Hinterlands)"
|
||||
],
|
||||
"ContentFinderConditionId": 422,
|
||||
"BossModEnabled": true
|
||||
}
|
||||
]
|
||||
|
@ -69,7 +69,6 @@
|
||||
},
|
||||
"TerritoryId": 402,
|
||||
"InteractionType": "SinglePlayerDuty",
|
||||
"ContentFinderConditionId": 399,
|
||||
"BossModEnabled": true
|
||||
}
|
||||
]
|
||||
|
@ -104,6 +104,7 @@
|
||||
"StopDistance": 5,
|
||||
"TerritoryId": 829,
|
||||
"InteractionType": "SinglePlayerDuty",
|
||||
"SinglePlayerDutyIndex": 1,
|
||||
"DialogueChoices": [
|
||||
{
|
||||
"Type": "List",
|
||||
|
@ -1267,13 +1267,14 @@
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"ContentFinderConditionId": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0,
|
||||
"exclusiveMaximum": 3000
|
||||
},
|
||||
"BossModEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"SinglePlayerDutyIndex": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"description": "If a quest has multiple solo instances (which affects 5 quests total), indicates which one this is"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,6 +91,8 @@ public abstract class ElementId : IComparable<ElementId>, IEquatable<ElementId>
|
||||
|
||||
public sealed class QuestId(ushort value) : ElementId(value)
|
||||
{
|
||||
public static QuestId FromRowId(uint rowId) => new((ushort)(rowId & 0xFFFF));
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value.ToString(CultureInfo.InvariantCulture);
|
||||
|
@ -76,6 +76,7 @@ public sealed class QuestStep
|
||||
public uint? ContentFinderConditionId { get; set; }
|
||||
public bool AutoDutyEnabled { get; set; }
|
||||
public bool BossModEnabled { get; set; }
|
||||
public byte SinglePlayerDutyIndex { get; set; }
|
||||
public SkipConditions? SkipConditions { get; set; }
|
||||
|
||||
public List<List<QuestWorkValue>?> RequiredQuestVariables { get; set; } = new();
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "/liza/Questionable/raw/branch/master/Questionable.Model/common-aethernetshard.json",
|
||||
"$id": "https://git.carvel.li/liza/Questionable/raw/branch/master/Questionable.Model/common-aethernetshard.json",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"[Gridania] Aetheryte Plaza",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "/liza/Questionable/raw/branch/master/Questionable.Model/common-aetheryte.json",
|
||||
"$id": "https://git.carvel.li/liza/Questionable/raw/branch/master/Questionable.Model/common-aetheryte.json",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Gridania",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "/liza/Questionable/raw/branch/master/Questionable.Model/common-classjob.json",
|
||||
"$id": "https://git.carvel.li//liza/Questionable/raw/branch/master/Questionable.Model/common-classjob.json",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Gladiator",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "/liza/Questionable/raw/branch/master/Questionable.Model/common-completionflags.json",
|
||||
"$id": "https://git.carvel.li//liza/Questionable/raw/branch/master/Questionable.Model/common-completionflags.json",
|
||||
"type": "array",
|
||||
"description": "Quest Variables that dictate whether or not this step is skipped: null is don't check, positive values need to be set, negative values need to be unset",
|
||||
"items": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "/liza/Questionable/raw/branch/master/Questionable.Model/common-vector3.json",
|
||||
"$id": "https://git.carvel.li//liza/Questionable/raw/branch/master/Questionable.Model/common-vector3.json",
|
||||
"type": "object",
|
||||
"description": "Position in the world",
|
||||
"properties": {
|
||||
|
@ -14,6 +14,7 @@ internal sealed class Configuration : IPluginConfiguration
|
||||
public int PluginSetupCompleteVersion { get; set; }
|
||||
public GeneralConfiguration General { get; } = new();
|
||||
public DutyConfiguration Duties { get; } = new();
|
||||
public SoloDutyConfiguration SoloDuties { get; } = new();
|
||||
public NotificationConfiguration Notifications { get; } = new();
|
||||
public AdvancedConfiguration Advanced { get; } = new();
|
||||
public WindowConfig DebugWindowConfig { get; } = new();
|
||||
@ -41,6 +42,13 @@ internal sealed class Configuration : IPluginConfiguration
|
||||
public HashSet<uint> BlacklistedDutyCfcIds { get; set; } = [];
|
||||
}
|
||||
|
||||
internal sealed class SoloDutyConfiguration
|
||||
{
|
||||
public bool RunSoloInstancesWithBossMod { get; set; }
|
||||
public HashSet<uint> WhitelistedSoloDutyCfcIds { get; set; } = [];
|
||||
public HashSet<uint> BlacklistedSoloDutyCfcIds { get; set; } = [];
|
||||
}
|
||||
|
||||
internal sealed class NotificationConfiguration
|
||||
{
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
@ -27,6 +27,7 @@ internal sealed class QuestRegistry
|
||||
private readonly JsonSchemaValidator _jsonSchemaValidator;
|
||||
private readonly ILogger<QuestRegistry> _logger;
|
||||
private readonly LeveData _leveData;
|
||||
private readonly TerritoryData _territoryData;
|
||||
|
||||
private readonly ICallGateProvider<object> _reloadDataIpc;
|
||||
private readonly Dictionary<ElementId, Quest> _quests = [];
|
||||
@ -34,7 +35,7 @@ internal sealed class QuestRegistry
|
||||
|
||||
public QuestRegistry(IDalamudPluginInterface pluginInterface, QuestData questData,
|
||||
QuestValidator questValidator, JsonSchemaValidator jsonSchemaValidator,
|
||||
ILogger<QuestRegistry> logger, LeveData leveData)
|
||||
ILogger<QuestRegistry> logger, LeveData leveData, TerritoryData territoryData)
|
||||
{
|
||||
_pluginInterface = pluginInterface;
|
||||
_questData = questData;
|
||||
@ -42,6 +43,7 @@ internal sealed class QuestRegistry
|
||||
_jsonSchemaValidator = jsonSchemaValidator;
|
||||
_logger = logger;
|
||||
_leveData = leveData;
|
||||
_territoryData = territoryData;
|
||||
_reloadDataIpc = _pluginInterface.GetIpcProvider<object>("Questionable.ReloadData");
|
||||
}
|
||||
|
||||
@ -150,10 +152,15 @@ internal sealed class QuestRegistry
|
||||
foreach (var quest in _quests.Values)
|
||||
{
|
||||
foreach (var dutyStep in quest.AllSteps().Where(x =>
|
||||
x.Step.InteractionType is EInteractionType.Duty or EInteractionType.SinglePlayerDuty
|
||||
&& x.Step.ContentFinderConditionId != null))
|
||||
x.Step.InteractionType is EInteractionType.Duty or EInteractionType.SinglePlayerDuty))
|
||||
{
|
||||
_contentFinderConditionIds[dutyStep.Step.ContentFinderConditionId!.Value] = (quest.Id, dutyStep.Step);
|
||||
if (dutyStep.Step is { InteractionType: EInteractionType.Duty, ContentFinderConditionId: not null })
|
||||
_contentFinderConditionIds[dutyStep.Step.ContentFinderConditionId!.Value] =
|
||||
(quest.Id, dutyStep.Step);
|
||||
else if (dutyStep.Step.InteractionType == EInteractionType.SinglePlayerDuty &&
|
||||
_territoryData.TryGetContentFinderConditionForSoloInstance(quest.Id,
|
||||
dutyStep.Step.SinglePlayerDutyIndex, out var cfcData))
|
||||
_contentFinderConditionIds[cfcData.ContentFinderConditionId] = (quest.Id, dutyStep.Step);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ internal static class SendNotification
|
||||
internal sealed class Factory(
|
||||
AutomatonIpc automatonIpc,
|
||||
AutoDutyIpc autoDutyIpc,
|
||||
BossModIpc bossModIpc,
|
||||
TerritoryData territoryData) : SimpleTaskFactory
|
||||
{
|
||||
public override ITask? CreateTask(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
@ -26,7 +27,7 @@ internal static class SendNotification
|
||||
new Task(step.InteractionType, step.ContentFinderConditionId.HasValue
|
||||
? territoryData.GetContentFinderCondition(step.ContentFinderConditionId.Value)?.Name
|
||||
: step.Comment),
|
||||
EInteractionType.SinglePlayerDuty when !step.BossModEnabled =>
|
||||
EInteractionType.SinglePlayerDuty when !bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyIndex, step.BossModEnabled) =>
|
||||
new Task(step.InteractionType, quest.Info.Name),
|
||||
_ => null,
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using Questionable.Controller.Steps.Shared;
|
||||
using Questionable.Data;
|
||||
using Questionable.External;
|
||||
@ -11,20 +12,23 @@ namespace Questionable.Controller.Steps.Interactions;
|
||||
|
||||
internal static class SinglePlayerDuty
|
||||
{
|
||||
internal sealed class Factory : ITaskFactory
|
||||
internal sealed class Factory(
|
||||
BossModIpc bossModIpc,
|
||||
TerritoryData territoryData) : ITaskFactory
|
||||
{
|
||||
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
{
|
||||
if (step.InteractionType != EInteractionType.SinglePlayerDuty)
|
||||
yield break;
|
||||
|
||||
if (step.BossModEnabled)
|
||||
if (bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyIndex, step.BossModEnabled))
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(step.ContentFinderConditionId);
|
||||
if (!territoryData.TryGetContentFinderConditionForSoloInstance(quest.Id, step.SinglePlayerDutyIndex, out var cfcData))
|
||||
throw new TaskException("Failed to get content finder condition for solo instance");
|
||||
|
||||
yield return new StartSinglePlayerDuty(step.ContentFinderConditionId.Value);
|
||||
yield return new StartSinglePlayerDuty(cfcData.ContentFinderConditionId);
|
||||
yield return new EnableAi();
|
||||
yield return new WaitSinglePlayerDuty(step.ContentFinderConditionId.Value);
|
||||
yield return new WaitSinglePlayerDuty(cfcData.ContentFinderConditionId);
|
||||
yield return new DisableAi();
|
||||
yield return new WaitAtEnd.WaitNextStepOrSequence();
|
||||
}
|
||||
@ -36,19 +40,13 @@ internal static class SinglePlayerDuty
|
||||
public override string ToString() => $"Wait(BossMod, entered instance {ContentFinderConditionId})";
|
||||
}
|
||||
|
||||
internal sealed class StartSinglePlayerDutyExecutor(
|
||||
TerritoryData territoryData,
|
||||
IClientState clientState) : TaskExecutor<StartSinglePlayerDuty>
|
||||
internal sealed class StartSinglePlayerDutyExecutor : TaskExecutor<StartSinglePlayerDuty>
|
||||
{
|
||||
protected override bool Start() => true;
|
||||
|
||||
public override ETaskResult Update()
|
||||
public override unsafe 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
|
||||
return GameMain.Instance()->CurrentContentFinderConditionId == Task.ContentFinderConditionId
|
||||
? ETaskResult.TaskComplete
|
||||
: ETaskResult.StillRunning;
|
||||
}
|
||||
@ -81,19 +79,13 @@ internal static class SinglePlayerDuty
|
||||
}
|
||||
|
||||
internal sealed class WaitSinglePlayerDutyExecutor(
|
||||
TerritoryData territoryData,
|
||||
IClientState clientState,
|
||||
BossModIpc bossModIpc) : TaskExecutor<WaitSinglePlayerDuty>, IStoppableTaskExecutor
|
||||
{
|
||||
protected override bool Start() => true;
|
||||
|
||||
public override ETaskResult Update()
|
||||
public override unsafe 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
|
||||
return GameMain.Instance()->CurrentContentFinderConditionId != Task.ContentFinderConditionId
|
||||
? ETaskResult.TaskComplete
|
||||
: ETaskResult.StillRunning;
|
||||
}
|
||||
|
@ -21,7 +21,8 @@ internal static class WaitAtEnd
|
||||
IClientState clientState,
|
||||
ICondition condition,
|
||||
TerritoryData territoryData,
|
||||
AutoDutyIpc autoDutyIpc)
|
||||
AutoDutyIpc autoDutyIpc,
|
||||
BossModIpc bossModIpc)
|
||||
: ITaskFactory
|
||||
{
|
||||
public IEnumerable<ITask> CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step)
|
||||
@ -53,7 +54,7 @@ internal static class WaitAtEnd
|
||||
return [new WaitNextStepOrSequence()];
|
||||
|
||||
case EInteractionType.Duty when !autoDutyIpc.IsConfiguredToRunContent(step.ContentFinderConditionId, step.AutoDutyEnabled):
|
||||
case EInteractionType.SinglePlayerDuty when !step.BossModEnabled:
|
||||
case EInteractionType.SinglePlayerDuty when !bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyIndex, step.BossModEnabled):
|
||||
return [new EndAutomation()];
|
||||
|
||||
case EInteractionType.WalkTo:
|
||||
|
@ -23,17 +23,17 @@ internal sealed class JournalData
|
||||
var genreLimsa = new Genre(uint.MaxValue - 3, "Starting in Limsa Lominsa", 1,
|
||||
new uint[] { 108, 109 }.Concat(limsaStart.QuestRedoParam.Select(x => x.Quest.RowId))
|
||||
.Where(x => x != 0)
|
||||
.Select(x => questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF))))
|
||||
.Select(x => questData.GetQuestInfo(QuestId.FromRowId(x)))
|
||||
.ToList());
|
||||
var genreGridania = new Genre(uint.MaxValue - 2, "Starting in Gridania", 1,
|
||||
new uint[] { 85, 123, 124 }.Concat(gridaniaStart.QuestRedoParam.Select(x => x.Quest.RowId))
|
||||
.Where(x => x != 0)
|
||||
.Select(x => questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF))))
|
||||
.Select(x => questData.GetQuestInfo(QuestId.FromRowId(x)))
|
||||
.ToList());
|
||||
var genreUldah = new Genre(uint.MaxValue - 1, "Starting in Ul'dah", 1,
|
||||
new uint[] { 568, 569, 570 }.Concat(uldahStart.QuestRedoParam.Select(x => x.Quest.RowId))
|
||||
.Where(x => x != 0)
|
||||
.Select(x => questData.GetQuestInfo(new QuestId((ushort)(x & 0xFFFF))))
|
||||
.Select(x => questData.GetQuestInfo(QuestId.FromRowId(x)))
|
||||
.ToList());
|
||||
genres.InsertRange(0, [genreLimsa, genreGridania, genreUldah]);
|
||||
genres.Single(x => x.Id == 1)
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
@ -7,6 +8,7 @@ using Dalamud.Game;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Questionable.Model.Questing;
|
||||
|
||||
namespace Questionable.Data;
|
||||
|
||||
@ -17,6 +19,7 @@ internal sealed class TerritoryData
|
||||
private readonly ImmutableDictionary<ushort, uint> _dutyTerritories;
|
||||
private readonly ImmutableDictionary<uint, string> _instanceNames;
|
||||
private readonly ImmutableDictionary<uint, ContentFinderConditionData> _contentFinderConditions;
|
||||
private readonly ImmutableDictionary<(ElementId QuestId, byte Index), uint> _questsToCfc;
|
||||
|
||||
public TerritoryData(IDataManager dataManager)
|
||||
{
|
||||
@ -48,6 +51,13 @@ internal sealed class TerritoryData
|
||||
.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);
|
||||
|
||||
_questsToCfc = dataManager.GetExcelSheet<Quest>()
|
||||
.Where(x => x is { RowId: > 0, IssuerLocation.RowId: > 0 })
|
||||
.SelectMany(GetQuestBattles)
|
||||
.Select(x => (x.QuestId, x.Index,
|
||||
CfcId: LookupContentFinderConditionForQuestBattle(dataManager, x.QuestBattleId)))
|
||||
.ToImmutableDictionary(x => (x.QuestId, x.Index), x => x.CfcId);
|
||||
}
|
||||
|
||||
public string? GetName(ushort territoryId) => _territoryNames.GetValueOrDefault(territoryId);
|
||||
@ -77,6 +87,18 @@ internal sealed class TerritoryData
|
||||
[NotNullWhen(true)] out ContentFinderConditionData? contentFinderConditionData) =>
|
||||
_contentFinderConditions.TryGetValue(cfcId, out contentFinderConditionData);
|
||||
|
||||
public bool TryGetContentFinderConditionForSoloInstance(ElementId questId, byte index,
|
||||
[NotNullWhen(true)] out ContentFinderConditionData? contentFinderConditionData)
|
||||
{
|
||||
if (_questsToCfc.TryGetValue((questId, index), out uint cfcId))
|
||||
return _contentFinderConditions.TryGetValue(cfcId, out contentFinderConditionData);
|
||||
else
|
||||
{
|
||||
contentFinderConditionData = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string FixName(string name, ClientLanguage language)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name) || language != ClientLanguage.English)
|
||||
@ -85,6 +107,27 @@ internal sealed class TerritoryData
|
||||
return string.Concat(name[0].ToString().ToUpper(CultureInfo.InvariantCulture), name.AsSpan(1));
|
||||
}
|
||||
|
||||
private static IEnumerable<(ElementId QuestId, byte Index, uint QuestBattleId)> GetQuestBattles(Quest quest)
|
||||
{
|
||||
foreach (Quest.QuestParamsStruct t in quest.QuestParams)
|
||||
{
|
||||
if (t.ScriptInstruction == "QUESTBATTLE0")
|
||||
yield return (QuestId.FromRowId(quest.RowId), 0, t.ScriptArg);
|
||||
else if (t.ScriptInstruction == "QUESTBATTLE1")
|
||||
yield return (QuestId.FromRowId(quest.RowId), 1, t.ScriptArg);
|
||||
else if (t.ScriptInstruction.IsEmpty)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static uint LookupContentFinderConditionForQuestBattle(IDataManager dataManager, uint questBattleId)
|
||||
{
|
||||
if (questBattleId >= 5000)
|
||||
return dataManager.GetExcelSheet<InstanceContent>().GetRow(questBattleId).Order;
|
||||
else
|
||||
return dataManager.GetExcelSheet<QuestBattleResident>().GetRow(questBattleId).Unknown0;
|
||||
}
|
||||
|
||||
public sealed record ContentFinderConditionData(
|
||||
uint ContentFinderConditionId,
|
||||
string Name,
|
||||
|
4
Questionable/External/AutoDutyIpc.cs
vendored
4
Questionable/External/AutoDutyIpc.cs
vendored
@ -31,7 +31,7 @@ internal sealed class AutoDutyIpc
|
||||
_stop = pluginInterface.GetIpcSubscriber<object>("AutoDuty.Stop");
|
||||
}
|
||||
|
||||
public bool IsConfiguredToRunContent(uint? cfcId, bool autoDutyEnabled)
|
||||
public bool IsConfiguredToRunContent(uint? cfcId, bool enabledByDefault)
|
||||
{
|
||||
if (cfcId == null)
|
||||
return false;
|
||||
@ -46,7 +46,7 @@ internal sealed class AutoDutyIpc
|
||||
_territoryData.TryGetContentFinderCondition(cfcId.Value, out _))
|
||||
return true;
|
||||
|
||||
return autoDutyEnabled && HasPath(cfcId.Value);
|
||||
return enabledByDefault && HasPath(cfcId.Value);
|
||||
}
|
||||
|
||||
public bool HasPath(uint cfcId)
|
||||
|
32
Questionable/External/BossModIpc.cs
vendored
32
Questionable/External/BossModIpc.cs
vendored
@ -2,22 +2,33 @@ using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using Dalamud.Plugin.Ipc.Exceptions;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Questionable.Data;
|
||||
using Questionable.Model.Questing;
|
||||
|
||||
namespace Questionable.External;
|
||||
|
||||
internal sealed class BossModIpc
|
||||
{
|
||||
private readonly ICommandManager _commandManager;
|
||||
private const string Name = "BossMod";
|
||||
|
||||
private readonly Configuration _configuration;
|
||||
private readonly ICommandManager _commandManager;
|
||||
private readonly TerritoryData _territoryData;
|
||||
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)
|
||||
public BossModIpc(
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
Configuration configuration,
|
||||
ICommandManager commandManager,
|
||||
TerritoryData territoryData)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_commandManager = commandManager;
|
||||
_territoryData = territoryData;
|
||||
|
||||
_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");
|
||||
@ -70,4 +81,21 @@ internal sealed class BossModIpc
|
||||
_commandManager.ProcessCommand("/vbm cfg ZoneModuleConfig EnableQuestBattles false");
|
||||
ClearPreset();
|
||||
}
|
||||
|
||||
public bool IsConfiguredToRunSoloInstance(ElementId questId, byte dutyIndex, bool enabledByDefault)
|
||||
{
|
||||
if (!_configuration.SoloDuties.RunSoloInstancesWithBossMod)
|
||||
return false;
|
||||
|
||||
if (!_territoryData.TryGetContentFinderConditionForSoloInstance(questId, dutyIndex, out var cfcData))
|
||||
return false;
|
||||
|
||||
if (_configuration.SoloDuties.BlacklistedSoloDutyCfcIds.Contains(cfcData.ContentFinderConditionId))
|
||||
return false;
|
||||
|
||||
if (_configuration.SoloDuties.WhitelistedSoloDutyCfcIds.Contains(cfcData.ContentFinderConditionId))
|
||||
return true;
|
||||
|
||||
return enabledByDefault;
|
||||
}
|
||||
}
|
||||
|
5
Questionable/External/QuestionableIpc.cs
vendored
5
Questionable/External/QuestionableIpc.cs
vendored
@ -41,10 +41,10 @@ internal sealed class QuestionableIpc : IDisposable
|
||||
eventInfoComponent.GetCurrentlyActiveEventQuests().Select(q => q.ToString()).ToList());
|
||||
|
||||
_startQuest = pluginInterface.GetIpcProvider<string, bool>(IpcStartQuest);
|
||||
_startQuest.RegisterFunc((string questId) => StartQuest(questController, questRegistry, questId, false));
|
||||
_startQuest.RegisterFunc((questId) => StartQuest(questController, questRegistry, questId, false));
|
||||
|
||||
_startSingleQuest = pluginInterface.GetIpcProvider<string, bool>(IpcStartSingleQuest);
|
||||
_startSingleQuest.RegisterFunc((string questId) => StartQuest(questController, questRegistry, questId, true));
|
||||
_startSingleQuest.RegisterFunc((questId) => StartQuest(questController, questRegistry, questId, true));
|
||||
}
|
||||
|
||||
private static bool StartQuest(QuestController qc, QuestRegistry qr, string questId, bool single)
|
||||
@ -63,6 +63,7 @@ internal sealed class QuestionableIpc : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_startSingleQuest.UnregisterFunc();
|
||||
_startQuest.UnregisterFunc();
|
||||
_getCurrentlyActiveEventQuests.UnregisterFunc();
|
||||
_getCurrentQuestId.UnregisterFunc();
|
||||
|
@ -7,6 +7,7 @@ using Lumina.Excel.Sheets;
|
||||
using Questionable.Model.Questing;
|
||||
using ExcelQuest = Lumina.Excel.Sheets.Quest;
|
||||
using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany;
|
||||
using QQuestId = Questionable.Model.Questing.QuestId;
|
||||
|
||||
namespace Questionable.Model;
|
||||
|
||||
@ -14,7 +15,7 @@ internal sealed class QuestInfo : IQuestInfo
|
||||
{
|
||||
public QuestInfo(ExcelQuest quest, uint newGamePlusChapter, byte startingCity, JournalGenreOverrides journalGenreOverrides)
|
||||
{
|
||||
QuestId = new QuestId((ushort)(quest.RowId & 0xFFFF));
|
||||
QuestId = QQuestId.FromRowId(quest.RowId);
|
||||
|
||||
string suffix = QuestId.Value switch
|
||||
{
|
||||
@ -41,15 +42,15 @@ internal sealed class QuestInfo : IQuestInfo
|
||||
PreviousQuests =
|
||||
new List<PreviousQuestInfo>
|
||||
{
|
||||
new(ReplaceOldQuestIds((ushort)(quest.PreviousQuest[0].RowId & 0xFFFF)), quest.Unknown7),
|
||||
new(ReplaceOldQuestIds((ushort)(quest.PreviousQuest[1].RowId & 0xFFFF))),
|
||||
new(ReplaceOldQuestIds((ushort)(quest.PreviousQuest[2].RowId & 0xFFFF)))
|
||||
new(ReplaceOldQuestIds(QQuestId.FromRowId(quest.PreviousQuest[0].RowId)), quest.Unknown7),
|
||||
new(ReplaceOldQuestIds(QQuestId.FromRowId(quest.PreviousQuest[1].RowId))),
|
||||
new(ReplaceOldQuestIds(QQuestId.FromRowId(quest.PreviousQuest[2].RowId)))
|
||||
}
|
||||
.Where(x => x.QuestId.Value != 0)
|
||||
.ToImmutableList();
|
||||
PreviousQuestJoin = (EQuestJoin)quest.PreviousQuestJoin;
|
||||
QuestLocks = quest.QuestLock
|
||||
.Select(x => new QuestId((ushort)(x.RowId & 0xFFFFF)))
|
||||
.Select(x => QQuestId.FromRowId(x.RowId))
|
||||
.Where(x => x.Value != 0)
|
||||
.ToImmutableList();
|
||||
QuestLockJoin = (EQuestJoin)quest.QuestLockJoin;
|
||||
@ -85,13 +86,13 @@ internal sealed class QuestInfo : IQuestInfo
|
||||
Expansion = (EExpansionVersion)quest.Expansion.RowId;
|
||||
}
|
||||
|
||||
private static QuestId ReplaceOldQuestIds(ushort questId)
|
||||
private static QuestId ReplaceOldQuestIds(QuestId questId)
|
||||
{
|
||||
return new QuestId(questId switch
|
||||
return questId.Value switch
|
||||
{
|
||||
524 => 4522,
|
||||
524 => new QuestId(4522),
|
||||
_ => questId,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
public ElementId QuestId { get; }
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Immutable;
|
||||
using LLib.GameData;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Questionable.Model.Questing;
|
||||
using QQuestId = Questionable.Model.Questing.QuestId;
|
||||
|
||||
namespace Questionable.Model;
|
||||
|
||||
@ -16,7 +17,7 @@ internal sealed class SatisfactionSupplyInfo : IQuestInfo
|
||||
Level = npc.LevelUnlock;
|
||||
SortKey = QuestId.Value;
|
||||
Expansion = (EExpansionVersion)npc.QuestRequired.Value.Expansion.RowId;
|
||||
PreviousQuests = [new PreviousQuestInfo(new QuestId((ushort)(npc.QuestRequired.RowId & 0xFFFF)))];
|
||||
PreviousQuests = [new PreviousQuestInfo(QQuestId.FromRowId(npc.QuestRequired.RowId))];
|
||||
}
|
||||
|
||||
public ElementId QuestId { get; }
|
||||
|
@ -311,6 +311,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin
|
||||
serviceCollection.AddSingleton<IQuestValidator, AethernetShortcutValidator>();
|
||||
serviceCollection.AddSingleton<IQuestValidator, DialogueChoiceValidator>();
|
||||
serviceCollection.AddSingleton<IQuestValidator, ClassQuestShouldHaveShortcutValidator>();
|
||||
serviceCollection.AddSingleton<IQuestValidator, UniqueSinglePlayerInstanceValidator>();
|
||||
serviceCollection.AddSingleton<JsonSchemaValidator>();
|
||||
serviceCollection.AddSingleton<IQuestValidator>(sp => sp.GetRequiredService<JsonSchemaValidator>());
|
||||
}
|
||||
|
@ -18,4 +18,5 @@ public enum EIssueType
|
||||
InvalidAethernetShortcut,
|
||||
InvalidExcelRef,
|
||||
ClassQuestWithoutAetheryteShortcut,
|
||||
DuplicateSinglePlayerInstance,
|
||||
}
|
||||
|
@ -0,0 +1,32 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Questionable.Model;
|
||||
using Questionable.Model.Questing;
|
||||
|
||||
namespace Questionable.Validation.Validators;
|
||||
|
||||
internal sealed class UniqueSinglePlayerInstanceValidator : IQuestValidator
|
||||
{
|
||||
public IEnumerable<ValidationIssue> Validate(Quest quest)
|
||||
{
|
||||
var singlePlayerInstances = quest.AllSteps()
|
||||
.Where(x => x.Step.InteractionType == EInteractionType.SinglePlayerDuty)
|
||||
.Select(x => (x.Sequence, x.StepId, x.Step.SinglePlayerDutyIndex))
|
||||
.ToList();
|
||||
if (singlePlayerInstances.DistinctBy(x => x.SinglePlayerDutyIndex).Count() < singlePlayerInstances.Count)
|
||||
{
|
||||
foreach (var singlePlayerInstance in singlePlayerInstances)
|
||||
{
|
||||
yield return new ValidationIssue
|
||||
{
|
||||
ElementId = quest.Id,
|
||||
Sequence = (byte)singlePlayerInstance.Sequence.Sequence,
|
||||
Step = singlePlayerInstance.StepId,
|
||||
Type = EIssueType.DuplicateSinglePlayerInstance,
|
||||
Severity = EIssueSeverity.Error,
|
||||
Description = $"Duplicate singleplayer duty index: {singlePlayerInstance.SinglePlayerDutyIndex}",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -119,7 +119,7 @@ internal sealed class QuestSelectionWindow : LWindow
|
||||
|
||||
foreach (var unacceptedQuest in Map.Instance()->UnacceptedQuestMarkers)
|
||||
{
|
||||
QuestId questId = new QuestId((ushort)(unacceptedQuest.ObjectiveId & 0xFFFF));
|
||||
QuestId questId = QuestId.FromRowId(unacceptedQuest.ObjectiveId);
|
||||
if (_quests.All(q => q.QuestId != questId))
|
||||
_quests.Add(_questData.GetQuestInfo(questId));
|
||||
}
|
||||
|
7
global.json
Normal file
7
global.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "8.0.0",
|
||||
"rollForward": "latestMinor",
|
||||
"allowPrerelease": false
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user