#define RELEASE using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Text.Json; using System.Text.Json.Nodes; using Dalamud.Plugin; using Microsoft.Extensions.Logging; using Questionable.Controller.Utils; using Questionable.Validation; namespace Questionable.Controller; internal abstract class DefinitionRegistry where TDefinition : class { private readonly IDalamudPluginInterface _pluginInterface; private readonly IDefinitionSchemaValidator _schemaValidator; private readonly QuestValidator _questValidator; private readonly ILogger _logger; private readonly Dictionary _definitions = new Dictionary(); public IReadOnlyDictionary Definitions => _definitions; protected abstract string TypeLabel { get; } protected abstract string ConfigDirectoryName { get; } protected abstract string ProjectDirectoryName { get; } protected abstract EPathType PathType { get; } protected DefinitionRegistry(IDalamudPluginInterface pluginInterface, IDefinitionSchemaValidator schemaValidator, QuestValidator questValidator, ILogger logger) { _pluginInterface = pluginInterface; _schemaValidator = schemaValidator; _questValidator = questValidator; _logger = logger; Reload(); } protected abstract IReadOnlyDictionary GetAssemblyDefinitions(); protected virtual DateTime? GetExpiry(TDefinition definition) { return null; } protected virtual string? GetName(TDefinition definition) { return null; } public void Reload() { _definitions.Clear(); _schemaValidator.Reset(); LoadFromAssembly(); try { LoadFromDirectory(new DirectoryInfo(Path.Combine(_pluginInterface.ConfigDirectory.FullName, ConfigDirectoryName))); } catch (Exception exception) { _logger.LogError(exception, "Failed to load {TypeLabel} definitions from user directory (some may have been successfully loaded)", TypeLabel); } foreach (ValidationIssue item in _schemaValidator.Validate()) { _questValidator.AddValidationIssue(item); } RemoveExpiredDefinitions(); _logger.LogDebug("Loaded {Count} {TypeLabel} definitions in total", _definitions.Count, TypeLabel); } [Conditional("RELEASE")] private void LoadFromAssembly() { IReadOnlyDictionary assemblyDefinitions = GetAssemblyDefinitions(); foreach (var (key, value) in assemblyDefinitions) { _definitions[key] = value; } _logger.LogDebug("Loaded {Count} {TypeLabel} definitions from assembly", assemblyDefinitions.Count, TypeLabel); } [Conditional("DEBUG")] private void LoadFromProjectDirectory() { DirectoryInfo directoryInfo = _pluginInterface.AssemblyLocation.Directory?.Parent?.Parent; if (directoryInfo == null) { return; } DirectoryInfo directory = new DirectoryInfo(Path.Combine(directoryInfo.FullName, "Paths", ProjectDirectoryName)); try { LoadFromDirectory(directory); } catch (Exception exception) { _definitions.Clear(); _logger.LogError(exception, "Failed to load {TypeLabel} definitions from project directory", TypeLabel); } } private void LoadFromDirectory(DirectoryInfo directory) { if (!directory.Exists) { _logger.LogDebug("Not loading {TypeLabel} definitions from {DirectoryName} (doesn't exist)", TypeLabel, directory); return; } FileInfo[] files = directory.GetFiles("*.json"); foreach (FileInfo fileInfo in files) { ushort? num = ExtractIdFromName(fileInfo.Name); if (!num.HasValue) { continue; } try { using FileStream utf8Json = fileInfo.OpenRead(); JsonNode jsonNode = JsonNode.Parse(utf8Json); if (jsonNode != null) { _schemaValidator.Enqueue(num.Value, jsonNode); TDefinition val = jsonNode.Deserialize(); if (val != null) { _definitions[num.Value] = val; } } } catch (JsonException ex) { _logger.LogError(ex, "Unable to parse {TypeLabel} definition file {FileName}", TypeLabel, fileInfo.FullName); _questValidator.AddValidationIssue(new ValidationIssue { PathType = PathType, Id = num.Value.ToString(CultureInfo.InvariantCulture), Location = null, Type = EIssueType.InvalidJsonSyntax, Severity = EIssueSeverity.Error, Description = "JSON syntax error in " + fileInfo.Name + ": " + ex.Message }); } catch (Exception exception) { _logger.LogError(exception, "Unable to load {TypeLabel} definition file {FileName}", TypeLabel, fileInfo.FullName); } } } private void RemoveExpiredDefinitions() { foreach (ushort item in (from kvp in _definitions.Where(delegate(KeyValuePair kvp) { DateTime? expiry = GetExpiry(kvp.Value); if (expiry.HasValue) { DateTime valueOrDefault = expiry.GetValueOrDefault(); return ExpiryUtils.NormalizeExpiry(valueOrDefault) < DateTime.UtcNow; } return false; }) select kvp.Key).ToList()) { _logger.LogDebug("Removing expired {TypeLabel} definition {Id} '{Name}'", TypeLabel, item, GetName(_definitions[item]) ?? item.ToString(CultureInfo.InvariantCulture)); _definitions.Remove(item); } } private static ushort? ExtractIdFromName(string fileName) { if (!ushort.TryParse(Path.GetFileNameWithoutExtension(fileName).Split('_', 2)[0], out var result)) { return null; } return result; } }