qstbak/Questionable/Questionable.Controller/DefinitionRegistry.cs
2026-08-17 20:29:32 +10:00

188 lines
5.3 KiB
C#

#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<TDefinition> where TDefinition : class
{
private readonly IDalamudPluginInterface _pluginInterface;
private readonly IDefinitionSchemaValidator _schemaValidator;
private readonly QuestValidator _questValidator;
private readonly ILogger _logger;
private readonly Dictionary<ushort, TDefinition> _definitions = new Dictionary<ushort, TDefinition>();
public IReadOnlyDictionary<ushort, TDefinition> 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<ushort, TDefinition> 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<ushort, TDefinition> 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<TDefinition>();
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<ushort, TDefinition> 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;
}
}