Глобальный рефакторинг пространств имён

This commit is contained in:
2025-10-02 13:29:30 +04:00
parent d23d81fb53
commit 2fa94851cb
3176 changed files with 27146 additions and 30584 deletions
@@ -0,0 +1,115 @@
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Pf2e.Database.Model.Base.Targets;
namespace Pf2e.Scrapper.Importers.Implementations.Aonprd.Helpers;
/// <summary>
/// Класс-помощник, хранящий необходимые глобальные данные.
/// </summary>
internal class AonprdBuilderGlobals
{
/// <summary>
/// Цель внешних ссылок Архивов Нефисов.
/// </summary>
public ExternalTarget AonprdTarget { get; set; } = null!;
/// <summary>
/// Словарь идентификаторов источников.
/// </summary>
public Dictionary<string, int> SourceIds { get; } = [];
/// <summary>
/// Словарь идентификаторов признаков.
/// </summary>
public Dictionary<string, int> TraitIds { get; } = [];
/// <summary>
/// Список идентификаторов сущностей, которые нужно игнорировать при обработке.
/// </summary>
public List<string> Ignore { get; } = [];
/// <summary>
/// Словарей суффиксов для сущностей
/// </summary>
public Dictionary<string, string> SlugSuffixes { get; } = [];
/// <summary>
/// Словарь допольнительных проверок связей.
/// Ключ - устаревшая сущность, значение - обновлённая.
/// </summary>
public Dictionary<string, string> LegacyRemasterExtra { get; } = [];
/// <summary>
/// Словарь сущностей, которые нужно слить с другой/другими.
/// Ключ - пропускаемая сущность. Значение - целевая сущность.
/// </summary>
public Dictionary<string, string> MergeSources { get; } = [];
/// <summary>
/// Словарь сущностей, которые нужно слить с другой/другими.
/// Ключ - пропускаемая сущность. Значение - целевая сущность.
/// </summary>
public Dictionary<string, string> SubclassGroups { get; } = [];
/// <summary>
/// Конструктор класса <see cref="AonprdBuilderGlobals"/>.
/// </summary>
public AonprdBuilderGlobals()
{
using StreamReader fileReader = new("Resources\\ImportAdaptation.json");
JObject? json = JObject
.Parse(fileReader.ReadToEnd())
.SelectToken("Aonprd")
?.ToObject<JObject>();
if (json is not null)
{
Ignore = json.SelectToken("Ignore")?.ToObject<List<string>>() ?? [];
SlugSuffixes = JsonToDictionary(json, "SlugSuffixes");
LegacyRemasterExtra = JsonToDictionary(json, "LegacyRemasterExtra");
MergeSources = JsonToDictionary(json, "MergeSources");
SubclassGroups = JsonToDictionary(json, "SubclassGroups");
}
}
/// <summary>
/// Получить суффикс для ссылки.
/// </summary>
/// <param name="id">Идентификатор Aonprd.</param>
/// <returns>Суффикс для ссылки или пустая строка.</returns>
public string GetSlugSuffix(string id)
{
return SlugSuffixes.TryGetValue(id, out string? slug) ? $"-{slug}" : string.Empty;
}
/// <summary>
/// Преобразовать JSON-объект в словарь.
/// </summary>
/// <param name="json">Исходный JSON-объект.</param>
/// <param name="tokenPath">Путь к токену в JSON.</param>
/// <returns>Результирующий словарь.</returns>
public static Dictionary<string, string> JsonToDictionary(JObject json, string tokenPath)
{
return json.SelectToken(tokenPath)?.ToObject<Dictionary<string, string>>() ?? [];
}
}
/*
Проверка уникальности:
SELECT
E.type,
E.slug,
COUNT(*),
string_agg(E.id::text, ', ' ORDER BY E.id),
string_agg(EE.external_id::text, ', ' ORDER BY E.id)
FROM public.entities AS E
INNER JOIN public.entity_externals AS EE ON EE.entity_id = E.id
GROUP BY E.type, E.slug
HAVING COUNT(*) > 1 AND E.type NOT IN ('rule', 'action')
*/
@@ -0,0 +1,197 @@
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Pf2e.Scrapper.Processors;
using Pf2e.Database.Model.Enum;
namespace Pf2e.Scrapper.Importers.Implementations.Aonprd.Helpers;
internal static class AonprdProcessorInputExtension
{
/// <summary>
/// Обработать строку, содержащую информацию о количестве действий.
/// </summary>
/// <param name="_"></param>
/// <param name="input">Входная строка с данными.</param>
/// <returns>Структура, хранящая информацию о количетсве действий.</returns>
public static ActionStruct ResolveActionString(this ProcessorInput _, string input)
{
ActionStruct result = new();
switch (input)
{
case "Single Action":
result.Actions = result.MaxActions = 1;
break;
case "Two Actions":
result.Actions = result.MaxActions = 2;
break;
case "Three Actions":
result.Actions = result.MaxActions = 3;
break;
case "Reaction":
result.IsReaction = true;
break;
case "Free Action":
result.IsFreeAction = false;
break;
case "Single Action to Three Actions":
case "Single Action or more Actions":
result.Actions = 1;
result.MaxActions = 3;
break;
case "Single Action or Two Actions":
case "Single Action to Two Actions":
result.Actions = 1;
result.MaxActions = 2;
break;
case "Two Actions or Three Actions":
result.Actions = 2;
result.MaxActions = 3;
break;
case "Two Actions to 2 rounds":
result.Actions = 2;
result.MaxActions = 3;
result.TwoRounds = true;
break;
case "Reaction or One Action":
result.IsReaction = true;
result.Actions = result.MaxActions = 1;
break;
case "Reaction or Two Actions":
result.IsReaction = true;
result.Actions = result.MaxActions = 2;
break;
case "Free Action or One Action":
result.IsFreeAction = false;
result.Actions = result.MaxActions = 1;
break;
default:
result.IsPassive = true;
break;
}
return result;
}
/// <summary>
/// Класс структуры, хранящей информацию о количетсве действий.
/// </summary>
public class ActionStruct
{
/// <summary>
/// Количество действий
/// </summary>
public int Actions { get; set; }
/// <summary>
/// Максимальное количество действий
/// </summary>
public int MaxActions { get; set; }
/// <summary>
/// Флаг, указывающий, что действия не требуются.
/// </summary>
public bool IsPassive { get; set; }
/// <summary>
/// Флаг, указывающий, что действие может быть использовано как реакция.
/// </summary>
public bool IsReaction { get; set; }
/// <summary>
/// Флаг, указывающий, что можно использовать свободное действие.
/// </summary>
public bool IsFreeAction { get; set; }
/// <summary>
/// Флаг, указывающий, что действие может быть продлено до 2 раундов.
/// </summary>
public bool TwoRounds { get; set; }
}
public class SourceCategorySolver : EnumSolver<SourceCategory>
{
public override Dictionary<string, SourceCategory> Data { get; } =
new()
{
{ "Rulebooks", SourceCategory.Rulebooks },
{ "Adventure Paths", SourceCategory.AdventurePaths },
{ "Lost Omens", SourceCategory.LostOmens },
{ "Adventures", SourceCategory.Adventures },
{ "Comics", SourceCategory.Comics },
{ "Blog Posts", SourceCategory.BlogPosts },
{ "Society", SourceCategory.Society },
};
}
public class VisionEnumSolver : EnumSolver<VisionEnum>
{
public override Dictionary<string, VisionEnum> Data { get; } =
new()
{
{ "Low-Light VisionEnum", VisionEnum.LowLightVision },
{ "Darkvision", VisionEnum.DarkVision },
};
}
public class AttributeValuesSolver : EnumSolver<AttributeValues>
{
public override Dictionary<string, AttributeValues> Data { get; } =
new()
{
{ "Consitution", AttributeValues.Constitution },
{ "Other", AttributeValues.Free },
{ "other", AttributeValues.Free },
};
}
public class ActionEnumSolver : EnumSolver<ActionEnum>
{
public override Dictionary<string, ActionEnum> Data { get; } =
new()
{
{ "Single Action", ActionEnum.SingleAction },
{ "Two Actions", ActionEnum.TwoActions },
{ "Three Actions", ActionEnum.ThreeActions },
{ "Reaction", ActionEnum.Reaction },
{ "Free Action", ActionEnum.FreeAction },
{ "Single Action to Three Actions", ActionEnum.SingleActionToThreeActions },
{ "Single Action or more Actions", ActionEnum.SingleActionToThreeActions },
{ "Single Action or Two Actions", ActionEnum.SingleActionToTwoActions },
{ "Single Action to Two Actions", ActionEnum.SingleActionToTwoActions },
{ "Two Actions or Three Actions", ActionEnum.TwoActionsToThreeActions },
{ "Two Actions to 2 rounds", ActionEnum.TwoActionsToTwoRounds },
{ "Reaction or One Action", ActionEnum.ReactionOrOneAction },
{ "Reaction or Two Actions", ActionEnum.ReactionOrTwoAction },
{ "Reaction or Three Actions", ActionEnum.ReactionOrThreeAction },
{ "Free Action or One Action", ActionEnum.FreeActionOrOneAction },
{ "1 Minute", ActionEnum.OneMinute },
{ "1 minute", ActionEnum.OneMinute },
{ "5 minutes", ActionEnum.FiveMinutes },
{ "10 minutes", ActionEnum.TenMinutes },
{ "30 minutes", ActionEnum.ThirtyMinutes },
{ "1 hour", ActionEnum.OneHour },
{ "<i>Nethys Note: No cast time was printed</i>", ActionEnum.Unknown },
};
}
public static void RegisterServices(IServiceCollection serviceCollection)
{
serviceCollection.AddTransient<IEnumSolver, SourceCategorySolver>();
serviceCollection.AddTransient<IEnumSolver, VisionEnumSolver>();
serviceCollection.AddTransient<IEnumSolver, AttributeValuesSolver>();
serviceCollection.AddTransient<IEnumSolver, ActionEnumSolver>();
}
}
@@ -0,0 +1,341 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Pf2e.Database.Model.Base.Source;
namespace Pf2e.Scrapper.Importers.Implementations.Aonprd.Helpers;
/// <summary>
/// Класс коллекции книг, содержащих правила.
/// Реализует шаблон <c>Singleton</c>.
/// </summary>
internal class RuleTocCollection
{
/// <summary>
/// Список книг, содержащих правила.
/// </summary>
public List<RuleTocBook> Books { get; } = [];
/// <summary>
/// Поиск книги по числовому идентификатору.
/// </summary>
public RuleTocBook this[int id] =>
Books.FirstOrDefault(i => i.SourceId == id) ?? throw new($"Book with id {id} not found");
/// <summary>
/// Поиск книги по CLR-представлению источника.
/// </summary>
public RuleTocBook this[Source source]
{
get
{
RuleTocBook? result = Books.FirstOrDefault(i => i.SourceId == source.Id);
if (result is not null)
{
return result;
}
result = new(source);
Books.Add(result);
return result;
}
}
/// <summary>
/// Поиск книги по имени.
/// </summary>
public RuleTocBook? FindName(string name)
{
foreach (RuleTocBook book in Books)
{
if (book.IsNameExists(name))
{
return book;
}
}
return null;
}
/// <summary>
/// Поиск правила по идентификатору.
/// </summary>
public RuleTocItem? FindEntityId(int id)
{
foreach (RuleTocBook book in Books)
{
RuleTocItem? result = book.GetByEntityId(id);
if (result is not null)
{
return result;
}
}
return null;
}
/// <summary>
/// Возвращает строковое представление коллекции в файле YAML.
/// </summary>
public override string ToString()
{
return string.Join("\n", Books.Select(book => book.ToString()).ToList());
}
/// <summary>
/// Сохраняет коллекцию в файле YAML.
/// </summary>
public void SaveFile()
{
string fileName =
$"rules-toc.{new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds()}.debug.yaml";
using FileStream fileStream = File.Create(fileName);
using StreamWriter writer = new(fileStream);
writer.Write(ToString());
}
/// <summary>
/// Экземпляр класса <see cref="RuleTocCollection"/>.
/// </summary>
public static RuleTocCollection Instance { get; } = new();
}
/// <summary>
/// Класс представляет собой книгу, содержащую правила.
/// </summary>
internal class RuleTocBook(Source source)
{
/// <summary>
/// Идентификатор источника.
/// </summary>
public int SourceId { get; } = source.Id;
/// <summary>
/// Названия источника.
/// </summary>
public string SourceName { get; } = source.Name;
/// <summary>
/// Список дочерних правил.
/// </summary>
/// <summary>
/// Список дочерних элементов.
/// </summary>
public List<RuleTocItem> Children { get; } = [];
/// <summary>
/// Находит элемент по хлебным крошкам.
/// </summary>
/// <param name="breadcrumbs">Массив хлебных крошек, указывающих на путь в дереве.</param>
/// <returns><see cref="RuleTocItem"/> или <c>null</c> если элемент не найден.</returns>
public RuleTocItem Get(string[] breadcrumbs)
{
RuleTocItem? node = null;
while (true)
{
List<RuleTocItem> children = node?.Children ?? Children;
if (breadcrumbs.Length == 0)
{
return node!;
}
string root = breadcrumbs[0];
RuleTocItem? item = children.FirstOrDefault(i => i.Name == root);
if (item is null)
{
item = new(root, node);
children.Add(item);
}
breadcrumbs = breadcrumbs[1..];
node = item;
}
}
/// <summary>
/// Проверяет, существует ли имя в текущей книге или дочерних элементах.
/// </summary>
/// <param name="name">Имя для проверки.</param>
/// <param name="node">Начальный элемент для поиска.</param>
/// <returns>true если имя.</returns>
public bool IsNameExists(string name, RuleTocItem? node = null)
{
foreach (RuleTocItem item in node?.Children ?? Children)
{
if (item.Name == name || IsNameExists(name, item))
{
return true;
}
}
return false;
}
/// <summary>
/// Находит элемент по идентификатору сущности.
/// </summary>
/// <param name="id">Идентификатор сущности.</param>
/// <param name="node">Начальный узел для поиска.</param>
/// <returns><see cref="RuleTocItem"/> или <c>null</c> если элемент не найден.</returns>
public RuleTocItem? GetByEntityId(int id, RuleTocItem? node = null)
{
if (node?.RuleId == id)
{
return node;
}
foreach (RuleTocItem item in node?.Children ?? Children)
{
RuleTocItem? result = GetByEntityId(id, item);
if (result is not null)
{
return result;
}
}
return null;
}
/// <summary>
/// Возвращает строковое представление книги в файле YAML.
/// </summary>
/// <returns>Строковое представление книги.</returns>
public override string ToString()
{
return $"\"{SourceName}\":\n"
+ string.Join(
"\n",
Children.Select(child => RuleTocItem.Indent(child.ToString())).ToList()
);
}
}
/// <summary>
/// Класс представляет собой элемент дерева правил.
/// </summary>
internal class RuleTocItem(string name, RuleTocItem? parent)
{
public string? Id { get; set; } = null;
/// <summary>
/// Имя элемента.
/// </summary>
public string Name { get; } = name;
/// <summary>
/// Идентификатор правила.
/// </summary>
public int? RuleId { get; set; } = null;
/// <summary>
/// Указывает, что элемент обрабатывается.
/// </summary>
public bool IsHandled { get; set; } = false;
/// <summary>
/// Родительский элемент дерева.
/// </summary>
public RuleTocItem? Parent { get; } = parent;
/// <summary>
/// Порядок дочерних элементов.
/// </summary>
public List<string>? ChildrenOrder { get; set; } = null;
/// <summary>
/// Коллекция дочерних элементов.
/// </summary>
public List<RuleTocItem> Children { get; } = [];
/// <summary>
/// Предыдущий идентификатор.
/// </summary>
public string? PreviousId { get; set; }
/// <summary>
/// Следующий идентификатор.
/// </summary>
public string? NextId { get; set; }
/// <summary>
/// Идентификатор первого дочернего элемента.
/// </summary>
public string? FirstChildId { get; set; }
/// <summary>
/// Идентификатор последнего дочернего элемента.
/// </summary>
public string? LastChildId { get; set; }
/// <summary>
/// Возвращает строковое представление элемента содержания в файле YAML.
/// </summary>
/// <returns>Строковое представление элемента содержания.</returns>
public override string ToString()
{
List<string> rows =
[
$"{Name}:",
$"{Tab}aonprd: {Id ?? "null"}",
$"{Tab}entity: {RuleId?.ToString() ?? "null"}",
];
if (Children.Count == 0)
{
// rows.Add($"{TAB}children: None");
return string.Join("\n", rows);
}
rows.Add($"{Tab}children:");
foreach (RuleTocItem child in Children)
{
rows.Add(child.Indent(2));
}
return string.Join("\n", rows);
}
/// <summary>
/// Увеличение отступа строкового представления элемента содержания в файле YAML.
/// </summary>
/// <param name="amount">Количество отступов.</param>
/// <returns>Строковое представление элемента содержания с отступом.</returns>
/// <seealso cref="Tab"/>
public string Indent(int amount = 1)
{
string result = ToString();
for (int i = 0; i < amount; i++)
{
result = Indent(result);
}
return result;
}
/// <summary>
/// Увеличение отступа строки.
/// </summary>
/// <param name="str">Входная строка.</param>
/// <returns>Строка с отступом.</returns>
/// <seealso cref="Tab"/>
public static string Indent(string str)
{
return Tab + str.Replace("\n", $"\n{Tab}");
}
/// <summary>
/// Строка отступа строки.
/// </summary>
/// <seealso cref="Indent(int)"/>
/// <seealso cref="Indent(string)"/>
public const string Tab = " ";
}
@@ -0,0 +1,123 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
namespace Pf2e.Scrapper.Importers.Implementations.Aonprd.Helpers;
/// <summary>
/// Класс-помощник, для обработки строкового ввода с Архивов Нефиса.
/// Реализует шаблон <c>Singleton</c>.
/// </summary>
internal partial class StringHelper
{
/// <summary>
/// Конструктор класса <see cref="StringHelper"/>
/// </summary>
public StringHelper() { }
/// <summary>
/// Разбиение строки по нестабильным строковым разделителям (например, <c>", "</c>, <c>" or "</c> и т.д.).
/// </summary>
/// <param name="value">Исходная строка.</param>
/// <returns>Коллекция строк.</returns>
public List<string> RowSplit(string value)
{
return value
.Replace(", or", ",")
.Replace(" or ", ", ")
.Replace(";", ",")
.Split(", ")
.Where(s => !string.IsNullOrWhiteSpace(s))
.ToList();
}
/// <summary>
/// Разбор списка ссылок в исходной строке.
/// </summary>
/// <param name="input">Исходная строка.</param>
/// <returns>Структура, содеражащая информацию разбиении строки по ссылкам.</returns>
public MarkdownLinkSplit ParseMarkdownLinks(string input)
{
MarkdownLinkSplit result = new();
MatchCollection matches = LinkRegex().Matches(input);
foreach (Match match in matches)
{
result.Links.Add(
new()
{
Prefix = match.Groups[1].Value.Trim(),
Text = match.Groups[2].Value.Trim(),
Url = match.Groups[3].Value.Trim(),
}
);
}
result.Suffix = LinkRegex().Replace(input, "");
return result;
}
/// <summary>
/// Извлечение суффикса из описания источника.
/// </summary>
/// <param name="input">Исходная строка.</param>
/// <returns>Суффикс.</returns>
public string ResolveSourceSuffix(string input)
{
Match match = SourceSuffixRegex().Match(input);
return match.Captures.Count > 0 ? match.Groups[1].Value : "";
}
/// <summary>
/// Регулярное выражение для извлечения источника из строки с Markdown-ссылкой.
/// </summary>
/// <returns>Регулярное выражение.</returns>
[GeneratedRegex(@"^\[.*?\]\(.*?\)\s+(.*)$")]
public static partial Regex SourceSuffixRegex();
/// <summary>
/// Регулярное выражение для выявления Markdown-ссылок в строке.
/// </summary>
/// <returns>Регулярное выражение.</returns>
[GeneratedRegex(@"(.*?)\[(.*?)\]\((.*?)\)")]
public static partial Regex LinkRegex();
}
/// <summary>
/// Класс структуры, содеражащая информацию разбиении строки по ссылкам.
/// </summary>
internal class MarkdownLinkSplit
{
/// <summary>
/// Список содержащихся ссылок.
/// </summary>
public List<MarkdownLink> Links { get; } = [];
/// <summary>
/// Завершающая часть ссылки.
/// </summary>
public string Suffix { get; set; } = "";
}
/// <summary>
/// Информация о ссылке в строке.
/// </summary>
internal class MarkdownLink
{
/// <summary>
/// Данные, содержащиеся до ссылки.
/// </summary>
public string Prefix { get; init; } = "";
/// <summary>
/// Текст ссылки.
/// </summary>
public string Text { get; init; } = "";
/// <summary>
/// URL ссылки.
/// </summary>
public string Url { get; init; } = "";
}