290 lines
8.9 KiB
C#
290 lines
8.9 KiB
C#
#define RELEASE
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using Dalamud.Plugin;
|
|
using Dalamud.Plugin.Ipc;
|
|
using Dalamud.Plugin.Services;
|
|
using LLib.GameData;
|
|
using Microsoft.Extensions.Logging;
|
|
using Questionable.Data;
|
|
using Questionable.Model;
|
|
using Questionable.Model.Questing;
|
|
using Questionable.QuestPaths;
|
|
using Questionable.Validation;
|
|
using Questionable.Validation.Validators;
|
|
|
|
namespace Questionable.Controller;
|
|
|
|
internal sealed class QuestRegistry
|
|
{
|
|
private readonly IDalamudPluginInterface _pluginInterface;
|
|
|
|
private readonly QuestData _questData;
|
|
|
|
private readonly QuestValidator _questValidator;
|
|
|
|
private readonly JsonSchemaValidator _jsonSchemaValidator;
|
|
|
|
private readonly ILogger<QuestRegistry> _logger;
|
|
|
|
private readonly TerritoryData _territoryData;
|
|
|
|
private readonly IChatGui _chatGui;
|
|
|
|
private readonly ICallGateProvider<object> _reloadDataIpc;
|
|
|
|
private readonly Dictionary<ElementId, Quest> _quests = new Dictionary<ElementId, Quest>();
|
|
|
|
private readonly Dictionary<uint, (ElementId QuestId, QuestStep Step)> _contentFinderConditionIds = new Dictionary<uint, (ElementId, QuestStep)>();
|
|
|
|
private readonly List<(uint ContentFinderConditionId, ElementId QuestId, int Sequence)> _lowPriorityContentFinderConditionQuests = new List<(uint, ElementId, int)>();
|
|
|
|
public IEnumerable<Quest> AllQuests => _quests.Values;
|
|
|
|
public int Count => _quests.Count<KeyValuePair<ElementId, Quest>>((KeyValuePair<ElementId, Quest> x) => !x.Value.Root.Disabled);
|
|
|
|
public int ValidationIssueCount => _questValidator.IssueCount;
|
|
|
|
public int ValidationErrorCount => _questValidator.ErrorCount;
|
|
|
|
public IReadOnlyList<(uint ContentFinderConditionId, ElementId QuestId, int Sequence)> LowPriorityContentFinderConditionQuests => _lowPriorityContentFinderConditionQuests;
|
|
|
|
public event EventHandler? Reloaded;
|
|
|
|
public QuestRegistry(IDalamudPluginInterface pluginInterface, QuestData questData, QuestValidator questValidator, JsonSchemaValidator jsonSchemaValidator, ILogger<QuestRegistry> logger, TerritoryData territoryData, IChatGui chatGui)
|
|
{
|
|
_pluginInterface = pluginInterface;
|
|
_questData = questData;
|
|
_questValidator = questValidator;
|
|
_jsonSchemaValidator = jsonSchemaValidator;
|
|
_logger = logger;
|
|
_territoryData = territoryData;
|
|
_chatGui = chatGui;
|
|
_reloadDataIpc = _pluginInterface.GetIpcProvider<object>("Questionable.ReloadData");
|
|
}
|
|
|
|
public void Reload()
|
|
{
|
|
_questValidator.Reset();
|
|
_quests.Clear();
|
|
_contentFinderConditionIds.Clear();
|
|
_lowPriorityContentFinderConditionQuests.Clear();
|
|
LoadQuestsFromAssembly();
|
|
try
|
|
{
|
|
LoadFromDirectory(new DirectoryInfo(Path.Combine(_pluginInterface.ConfigDirectory.FullName, "Quests")), Quest.ESource.UserDirectory);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
_logger.LogError(exception, "Failed to load all quests from user directory (some may have been successfully loaded)");
|
|
}
|
|
LoadCfcIds();
|
|
ValidateQuests();
|
|
this.Reloaded?.Invoke(this, EventArgs.Empty);
|
|
try
|
|
{
|
|
_reloadDataIpc.SendMessage();
|
|
}
|
|
catch (Exception exception2)
|
|
{
|
|
_logger.LogWarning(exception2, "Error during Reload.SendMessage IPC");
|
|
}
|
|
_logger.LogInformation("Loaded {Count} quests in total", _quests.Count);
|
|
}
|
|
|
|
[Conditional("RELEASE")]
|
|
private void LoadQuestsFromAssembly()
|
|
{
|
|
_logger.LogInformation("Loading quests from assembly");
|
|
foreach (var (elementId2, root) in AssemblyQuestLoader.GetQuests())
|
|
{
|
|
try
|
|
{
|
|
IQuestInfo questInfo = _questData.GetQuestInfo(elementId2);
|
|
Quest quest = new Quest
|
|
{
|
|
Id = elementId2,
|
|
Root = root,
|
|
Info = questInfo,
|
|
Source = Quest.ESource.Assembly
|
|
};
|
|
_quests[quest.Id] = quest;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning("Not loading unknown quest {QuestId} from assembly: {Message}", elementId2, ex.Message);
|
|
}
|
|
}
|
|
_logger.LogInformation("Loaded {Count} quests from assembly", _quests.Count);
|
|
}
|
|
|
|
[Conditional("DEBUG")]
|
|
private void LoadQuestsFromProjectDirectory()
|
|
{
|
|
DirectoryInfo directoryInfo = _pluginInterface.AssemblyLocation.Directory?.Parent?.Parent;
|
|
if (directoryInfo == null)
|
|
{
|
|
return;
|
|
}
|
|
DirectoryInfo directoryInfo2 = new DirectoryInfo(Path.Combine(directoryInfo.FullName, "QuestPaths"));
|
|
if (!directoryInfo2.Exists)
|
|
{
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
foreach (string value in ExpansionData.ExpansionFolders.Values)
|
|
{
|
|
LoadFromDirectory(new DirectoryInfo(Path.Combine(directoryInfo2.FullName, value)), Quest.ESource.ProjectDirectory, LogLevel.Trace);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_quests.Clear();
|
|
_chatGui.PrintError("Unable to load quests - " + ex.GetType().Name + ": " + ex.Message, "Questionable", 576);
|
|
_logger.LogError(ex, "Failed to load quests from project directory");
|
|
}
|
|
}
|
|
|
|
private void LoadCfcIds()
|
|
{
|
|
foreach (Quest value in _quests.Values)
|
|
{
|
|
foreach (QuestSequence item in value.AllSequences())
|
|
{
|
|
foreach (QuestStep item2 in item.Steps.Where(delegate(QuestStep x)
|
|
{
|
|
EInteractionType interactionType = x.InteractionType;
|
|
return (uint)(interactionType - 18) <= 1u;
|
|
}))
|
|
{
|
|
if (item2 != null && item2.InteractionType == EInteractionType.Duty)
|
|
{
|
|
DutyOptions dutyOptions = item2.DutyOptions;
|
|
if (dutyOptions != null)
|
|
{
|
|
_contentFinderConditionIds[dutyOptions.ContentFinderConditionId] = (value.Id, item2);
|
|
if (dutyOptions.LowPriority)
|
|
{
|
|
_lowPriorityContentFinderConditionQuests.Add((dutyOptions.ContentFinderConditionId, value.Id, item.Sequence));
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
if (item2.InteractionType == EInteractionType.SinglePlayerDuty && _territoryData.TryGetContentFinderConditionForSoloInstance(value.Id, item2.SinglePlayerDutyIndex, out TerritoryData.ContentFinderConditionData contentFinderConditionData))
|
|
{
|
|
_contentFinderConditionIds[contentFinderConditionData.ContentFinderConditionId] = (value.Id, item2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ValidateQuests()
|
|
{
|
|
_questValidator.Validate(_quests.Values.Where((Quest x) => x.Source != Quest.ESource.Assembly).ToList());
|
|
}
|
|
|
|
private void LoadQuestFromStream(string fileName, Stream stream, Quest.ESource source)
|
|
{
|
|
if (source == Quest.ESource.UserDirectory)
|
|
{
|
|
_logger.LogTrace("Loading quest from '{FileName}'", fileName);
|
|
}
|
|
ElementId elementId = ExtractQuestIdFromName(fileName);
|
|
if (!(elementId == null))
|
|
{
|
|
JsonNode jsonNode = JsonNode.Parse(stream);
|
|
_jsonSchemaValidator.Enqueue(elementId, jsonNode);
|
|
QuestRoot root = jsonNode.Deserialize<QuestRoot>();
|
|
IQuestInfo questInfo = _questData.GetQuestInfo(elementId);
|
|
Quest quest = new Quest
|
|
{
|
|
Id = elementId,
|
|
Root = root,
|
|
Info = questInfo,
|
|
Source = source
|
|
};
|
|
_quests[quest.Id] = quest;
|
|
}
|
|
}
|
|
|
|
private void LoadFromDirectory(DirectoryInfo directory, Quest.ESource source, LogLevel logLevel = LogLevel.Information)
|
|
{
|
|
if (!directory.Exists)
|
|
{
|
|
_logger.LogInformation("Not loading quests from {DirectoryName} (doesn't exist)", directory);
|
|
return;
|
|
}
|
|
if (source == Quest.ESource.UserDirectory)
|
|
{
|
|
_logger.Log(logLevel, "Loading quests from {DirectoryName}", directory);
|
|
}
|
|
FileInfo[] files = directory.GetFiles("*.json");
|
|
foreach (FileInfo fileInfo in files)
|
|
{
|
|
try
|
|
{
|
|
using FileStream stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read);
|
|
LoadQuestFromStream(fileInfo.Name, stream, source);
|
|
}
|
|
catch (Exception innerException)
|
|
{
|
|
throw new InvalidDataException("Unable to load file " + fileInfo.FullName, innerException);
|
|
}
|
|
}
|
|
DirectoryInfo[] directories = directory.GetDirectories();
|
|
foreach (DirectoryInfo directory2 in directories)
|
|
{
|
|
LoadFromDirectory(directory2, source, logLevel);
|
|
}
|
|
}
|
|
|
|
private static ElementId? ExtractQuestIdFromName(string resourceName)
|
|
{
|
|
string text = resourceName.Substring(0, resourceName.Length - ".json".Length);
|
|
text = text.Substring(text.LastIndexOf('.') + 1);
|
|
if (!text.Contains('_', StringComparison.Ordinal))
|
|
{
|
|
return null;
|
|
}
|
|
return ElementId.FromString(text.Split('_', 2)[0]);
|
|
}
|
|
|
|
public bool IsKnownQuest(ElementId questId)
|
|
{
|
|
return _quests.ContainsKey(questId);
|
|
}
|
|
|
|
public bool TryGetQuest(ElementId questId, [NotNullWhen(true)] out Quest? quest)
|
|
{
|
|
return _quests.TryGetValue(questId, out quest);
|
|
}
|
|
|
|
public List<QuestInfo> GetKnownClassJobQuests(EClassJob classJob, bool includeRoleQuests = true)
|
|
{
|
|
List<QuestInfo> list = _questData.GetClassJobQuests(classJob, includeRoleQuests).ToList();
|
|
if (classJob.AsJob() != classJob)
|
|
{
|
|
list.AddRange(_questData.GetClassJobQuests(classJob.AsJob(), includeRoleQuests));
|
|
}
|
|
return list.Where((QuestInfo x) => IsKnownQuest(x.QuestId)).ToList();
|
|
}
|
|
|
|
public bool TryGetDutyByContentFinderConditionId(uint cfcId, [NotNullWhen(true)] out DutyOptions? dutyOptions)
|
|
{
|
|
if (_contentFinderConditionIds.TryGetValue(cfcId, out (ElementId, QuestStep) value))
|
|
{
|
|
dutyOptions = value.Item2.DutyOptions;
|
|
return dutyOptions != null;
|
|
}
|
|
dutyOptions = null;
|
|
return false;
|
|
}
|
|
}
|