#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 _logger; private readonly TerritoryData _territoryData; private readonly IChatGui _chatGui; private readonly ICallGateProvider _reloadDataIpc; private readonly Dictionary _quests = new Dictionary(); private readonly Dictionary _contentFinderConditionIds = new Dictionary(); private readonly List<(uint ContentFinderConditionId, ElementId QuestId, int Sequence)> _lowPriorityContentFinderConditionQuests = new List<(uint, ElementId, int)>(); public IEnumerable AllQuests => _quests.Values; public int Count => _quests.Count>((KeyValuePair 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 logger, TerritoryData territoryData, IChatGui chatGui) { _pluginInterface = pluginInterface; _questData = questData; _questValidator = questValidator; _jsonSchemaValidator = jsonSchemaValidator; _logger = logger; _territoryData = territoryData; _chatGui = chatGui; _reloadDataIpc = _pluginInterface.GetIpcProvider("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(); 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 GetKnownClassJobQuests(EClassJob classJob, bool includeRoleQuests = true) { List 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; } }