Compare commits

...
2 Commits
79 changed files with 4927 additions and 406 deletions
+1 -1
View File
@@ -121,7 +121,7 @@ csharp_style_conditional_delegate_call = true
# Предпочтения модификатора # Предпочтения модификатора
csharp_prefer_static_anonymous_function = true csharp_prefer_static_anonymous_function = true
csharp_prefer_static_local_function = true csharp_prefer_static_local_function = true
csharp_preferred_modifier_order = public, private, protected, file, static, extern, internal, virtual, abstract, override, sealed, readonly, new, unsafe, required, volatile, async:suggestion csharp_preferred_modifier_order = public, private, protected, internal, file, static, extern, virtual, abstract, override, sealed, readonly, new, unsafe, required, volatile, async:suggestion
csharp_style_prefer_readonly_struct = true csharp_style_prefer_readonly_struct = true
csharp_style_prefer_readonly_struct_member = true csharp_style_prefer_readonly_struct_member = true
+34 -5
View File
@@ -22,15 +22,44 @@ public class ProcessorAttribute(string name) : Attribute
/// </summary> /// </summary>
public ProcessorGroupEnum Group { get; init; } = ProcessorGroupEnum.General; public ProcessorGroupEnum Group { get; init; } = ProcessorGroupEnum.General;
/// <summary>
/// Коллекция названий обработчиков, которые должны быть выполнены до этого оброботчика.
/// </summary>
public string? InnerType { get; init; } = null;
/// <summary> /// <summary>
/// Процессор включён по умолчанию. /// Процессор включён по умолчанию.
/// </summary> /// </summary>
public bool Active { get; init; } = true; public bool Active { get; init; } = true;
/// <summary>
/// Разрешить выполнять процессов в параллельном режиме
/// </summary>
public bool Parallel { get; init; } = true;
/// <summary>
/// Обработчик набора элементов
/// </summary>
public virtual bool Collection { get; } = false;
}
/// <summary>
/// Аттрибут обработчика набор элементов.
/// </summary>
/// <param name="name">Название обработчика.</param>
[AttributeUsage(AttributeTargets.Class)]
public class ProcessorCollectionAttribute(string name) : ProcessorAttribute(name)
{
/// <summary>
/// Название типов, который обрабатывает этот оброботчик.
/// <c>null</c> - обработать все типы.
/// </summary>
public required string? InnerType { get; init; }
/// <summary>
/// Тип используемого компендиума.
/// </summary>
public required ProcessorCompendiumEnum CompendiumType { get; init; }
/// <summary>
/// Обработчик набора элементов.
/// </summary>
public override bool Collection { get; } = true;
} }
/// <summary> /// <summary>
+31 -7
View File
@@ -1,5 +1,4 @@
using System.ComponentModel; using System.Reflection;
using System.Reflection;
using Pf2eDataScrapper.Processors; using Pf2eDataScrapper.Processors;
namespace Pf2eDataScrapper.App; namespace Pf2eDataScrapper.App;
@@ -25,7 +24,7 @@ internal class ProcessorCollection
/// </summary> /// </summary>
/// <param name="name"></param> /// <param name="name"></param>
/// <returns></returns> /// <returns></returns>
public ProcessorCollectionItem this[string name] => Items.First(item => item.Name == name); public ProcessorCollectionItem this[string name] => AllItems.First(item => item.Name == name);
/// <summary> /// <summary>
/// Конструктор класса <see cref="ProcessorCollection"/>. /// Конструктор класса <see cref="ProcessorCollection"/>.
@@ -66,7 +65,19 @@ internal class ProcessorCollection
ProcessorDependenciesAttribute? dependenciesAttribute = ProcessorDependenciesAttribute? dependenciesAttribute =
type.GetCustomAttribute<ProcessorDependenciesAttribute>(); type.GetCustomAttribute<ProcessorDependenciesAttribute>();
bool isCollection = processorAttribute.InnerType is not null; bool isCollection = processorAttribute.Collection;
string? innerType = null;
ProcessorCompendiumEnum? compendium = null;
if (processorAttribute.GetType() == typeof(ProcessorCollectionAttribute))
{
ProcessorCollectionAttribute processorCollectionAttribute =
(ProcessorCollectionAttribute)processorAttribute;
innerType = processorCollectionAttribute.InnerType;
compendium = processorCollectionAttribute.CompendiumType;
}
return new() return new()
{ {
@@ -75,8 +86,10 @@ internal class ProcessorCollection
Description = processorAttribute.Description, Description = processorAttribute.Description,
Group = processorAttribute.Group, Group = processorAttribute.Group,
Dependencies = dependenciesAttribute?.Dependencies ?? [], Dependencies = dependenciesAttribute?.Dependencies ?? [],
IsCollectionProcessor = isCollection, IsCollection = isCollection,
InnerType = processorAttribute.InnerType, InnerType = innerType,
Compendium = compendium,
Active = processorAttribute.Active,
}; };
} }
@@ -202,6 +215,11 @@ internal class ProcessorCollectionItem
/// </summary> /// </summary>
public ICollection<string> Dependencies { get; init; } = []; public ICollection<string> Dependencies { get; init; } = [];
/// <summary>
/// Процессор включён по умолчанию.
/// </summary>
public bool Active { get; init; } = true;
/// <summary> /// <summary>
/// Приоритет выполнения обработчика. /// Приоритет выполнения обработчика.
/// </summary> /// </summary>
@@ -211,10 +229,16 @@ internal class ProcessorCollectionItem
/// Флаг, указывающий, обрабатывает ли обработчик коллекцию элементов. /// Флаг, указывающий, обрабатывает ли обработчик коллекцию элементов.
/// Внутренний тип элементов указывается в <see cref="InnerType"/>. /// Внутренний тип элементов указывается в <see cref="InnerType"/>.
/// </summary> /// </summary>
public required bool IsCollectionProcessor { get; init; } public required bool IsCollection { get; init; }
/// <summary> /// <summary>
/// Флаг, указывающий внутренний тип коллекции обрабатываемых элементов. /// Флаг, указывающий внутренний тип коллекции обрабатываемых элементов.
/// </summary> /// </summary>
public string? InnerType { get; init; } public string? InnerType { get; init; }
/// <summary>
/// Название типов, который обрабатывает этот оброботчик.
/// <c>null</c> - обработать все типы.
/// </summary>
public ProcessorCompendiumEnum? Compendium { get; init; }
} }
@@ -24,6 +24,12 @@ public enum ProcessorGroupEnum
FoundryEnEntities, FoundryEnEntities,
} }
public enum ProcessorCompendiumEnum
{
Aonprd,
FoundryEn,
}
public static class ProcessorGroupEnumExtension public static class ProcessorGroupEnumExtension
{ {
public static string GetDescription(this ProcessorGroupEnum group) public static string GetDescription(this ProcessorGroupEnum group)
+1 -13
View File
@@ -66,19 +66,7 @@ internal class ApplicationSettings
#region Основные настройки #region Основные настройки
/// <summary> //
/// Флаг, указывающий, что все старые записи необходимо удалить.
/// </summary>
[Browsable(true)]
[DisplayName("Удаление всех записей перед запуском.")]
[Description("Редактирование через .env-файл.")]
public bool TruncateAllTables { get; set; } = true;
/// <summary>
/// Список обработчиков, которые должны быть выполнены.
/// Пустой список означает, что должны быть выполнены все обработчики.
/// </summary>
public ICollection<string> ProcessorNames { get; } = [];
#endregion #endregion
} }
@@ -0,0 +1,22 @@
namespace Pf2eViewer;
/// <summary>
/// Элемент управления прокрутки страниц
/// </summary>
internal sealed class CoreBindingNavigator : BindingNavigator
{
public CoreBindingNavigator()
{
AddStandardItems();
if (DeleteItem != null)
{
DeleteItem.Visible = false;
}
if (AddNewItem != null)
{
AddNewItem.Visible = false;
}
}
}
@@ -0,0 +1,130 @@
using System.Collections.Concurrent;
using Pf2eModel;
using Pf2eModel.Model.Base;
using Pf2eModel.Model.Enum;
namespace Pf2eViewer.DatabaseViewer;
/// <summary>
/// Класс, выполняющий страничные запросы к базе данных.
/// </summary>
internal class DatabaseQueryPerformer
{
/// <summary>
/// Тип сущностей, которую необходимо получить.
/// </summary>
public required EntityType Type;
/// <summary>
/// Номер страницы, которую необходимо получить.
/// </summary>
public required int Page;
/// <summary>
/// Количество элементов на странице.
/// </summary>
public required int PerPage;
/// <summary>
/// Поисковая строка, по которой будет производиться фильтрация.
/// </summary>
public required string Search = "";
/// <summary>
/// Поток, в котором будет выполняться запрос к базе данных.
/// </summary>
private readonly Thread _queryThread;
/// <summary>
/// Конструктор класса, который инициализирует поток для выполнения запроса к базе данных.
/// </summary>
public DatabaseQueryPerformer()
{
_queryThread = new(PerformQuery);
}
/// <summary>
/// Запускает поток для выполнения запроса к базе данных.
/// </summary>
public void Query()
{
_queryThread.Start();
}
/// <summary>
/// Прерывает выполнение потока, если он ещё не завершён.
/// </summary>
public void Abort()
{
_queryThread.Interrupt();
}
/// <summary>
/// Выполняет запрос к базе данных и обрабатывает результаты.
/// </summary>
private void PerformQuery()
{
try
{
Pf2eDbContext dbContext = new();
IQueryable<Entity> query =
Search == ""
? dbContext.Entities.Where(e => e.Type == Type)
:
// ReSharper disable once EntityFramework.UnsupportedServerSideFunctionCall
dbContext.Entities.Where(e =>
e.Type == Type && Pf2eDbContext.LocaleSearch(e.Name, Search)
);
int count = query.Count();
List<Entity> entities = query
.OrderBy(e => e.Id)
.Skip((Page - 1) * PerPage)
.Take(PerPage)
.ToList();
DatabaseQueryResult result = new(count, entities);
OnQueryCompleted?.Invoke(this, result);
}
catch (ThreadInterruptedException)
{
// Ignore
}
}
/// <summary>
/// Делегат, который будет вызван по завершении выполнения запроса к базе данных.
/// </summary>
/// <param name="sender">Объект класса, выполневшего запрос.</param>
/// <param name="result">Результат запроса.</param>
public delegate void DatabaseQueryPerformerDelegate(
DatabaseQueryPerformer sender,
DatabaseQueryResult result
);
/// <summary>
/// Событие, которое будет вызвано по завершении выполнения запроса к базе данных.
/// </summary>
public event DatabaseQueryPerformerDelegate? OnQueryCompleted;
}
/// <summary>
/// Класс, который содержит результат выполнения запроса к базе данных.
/// </summary>
/// <param name="count">Общее количество найденных сущностей.</param>
/// <param name="entities">Набор найденных сущностей.</param>
internal class DatabaseQueryResult(int count, ICollection<Entity> entities)
{
/// <summary>
/// Общее количество найденных сущностей.
/// </summary>
public int Count { get; } = count;
/// <summary>
/// Набор найденных сущностей.
/// </summary>
public ConcurrentBag<Entity> Entities { get; } = new(entities);
}
@@ -0,0 +1,148 @@
namespace Pf2eViewer.DatabaseViewer;
partial class DatabaseViewerControl
{
/// <summary>
/// Обязательная переменная конструктора.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Освободить все используемые ресурсы.
/// </summary>
/// <param name="disposing">истинно, если управляемый ресурс должен быть удален; иначе ложно.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Код, автоматически созданный конструктором компонентов
/// <summary>
/// Требуемый метод для поддержки конструктора — не изменяйте
/// содержимое этого метода с помощью редактора кода.
/// </summary>
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
BindingNavigator = new CoreBindingNavigator();
SearchTextBox = new TextBox();
DataGridView = new DataGridView();
DbBindingSource = new BindingSource(components);
PagesBindingSource = new BindingSource(components);
Id = new DataGridViewTextBoxColumn();
EntityName = new DataGridViewTextBoxColumn();
Rarity = new DataGridViewTextBoxColumn();
(BindingNavigator).BeginInit();
((System.ComponentModel.ISupportInitialize)DataGridView).BeginInit();
((System.ComponentModel.ISupportInitialize)DbBindingSource).BeginInit();
((System.ComponentModel.ISupportInitialize)PagesBindingSource).BeginInit();
SuspendLayout();
//
// BindingNavigator
//
BindingNavigator.AddNewItem = null;
BindingNavigator.DeleteItem = null;
BindingNavigator.ImeMode = ImeMode.NoControl;
BindingNavigator.LayoutStyle = ToolStripLayoutStyle.HorizontalStackWithOverflow;
BindingNavigator.Location = new Point(0, 0);
BindingNavigator.Name = "BindingNavigator";
BindingNavigator.Size = new Size(345, 25);
BindingNavigator.Stretch = true;
BindingNavigator.TabIndex = 1;
BindingNavigator.Text = "coreBindingNavigator1";
BindingNavigator.RefreshItems += BindingNavigator_RefreshItems;
//
// SearchTextBox
//
SearchTextBox.Dock = DockStyle.Top;
SearchTextBox.Location = new Point(0, 25);
SearchTextBox.Name = "SearchTextBox";
SearchTextBox.PlaceholderText = "Поиск";
SearchTextBox.Size = new Size(345, 23);
SearchTextBox.TabIndex = 3;
SearchTextBox.TextChanged += SearchTextBox_TextChanged;
SearchTextBox.Leave += SearchTextBox_Leave;
//
// DataGridView
//
DataGridView.AllowUserToAddRows = false;
DataGridView.AllowUserToDeleteRows = false;
DataGridView.AutoGenerateColumns = false;
DataGridView.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCellsExceptHeader;
DataGridView.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.AllCellsExceptHeaders;
DataGridView.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
DataGridView.Columns.AddRange(new DataGridViewColumn[] { Id, EntityName, Rarity });
DataGridView.DataSource = DbBindingSource;
DataGridView.Dock = DockStyle.Fill;
DataGridView.Location = new Point(0, 48);
DataGridView.MultiSelect = false;
DataGridView.Name = "DataGridView";
DataGridView.ReadOnly = true;
DataGridView.RowHeadersVisible = false;
DataGridView.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
DataGridView.Size = new Size(345, 345);
DataGridView.TabIndex = 4;
DataGridView.RowStateChanged += DataGridView_RowStateChanged;
//
// Id
//
Id.DataPropertyName = "Id";
Id.HeaderText = "Id";
Id.Name = "Id";
Id.ReadOnly = true;
Id.Resizable = DataGridViewTriState.False;
Id.SortMode = DataGridViewColumnSortMode.NotSortable;
Id.Width = 5;
//
// EntityName
//
EntityName.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
EntityName.DataPropertyName = "Name";
EntityName.HeaderText = "Name";
EntityName.Name = "EntityName";
EntityName.ReadOnly = true;
EntityName.Resizable = DataGridViewTriState.False;
EntityName.SortMode = DataGridViewColumnSortMode.NotSortable;
//
// Rarity
//
Rarity.DataPropertyName = "Rarity";
Rarity.HeaderText = "Rarity";
Rarity.Name = "Rarity";
Rarity.ReadOnly = true;
Rarity.Resizable = DataGridViewTriState.False;
Rarity.SortMode = DataGridViewColumnSortMode.NotSortable;
Rarity.Width = 5;
//
// DatabaseViewerControl
//
AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font;
Controls.Add(DataGridView);
Controls.Add(SearchTextBox);
Controls.Add(BindingNavigator);
Name = "DatabaseViewerControl";
Size = new Size(345, 393);
(BindingNavigator).EndInit();
((System.ComponentModel.ISupportInitialize)DataGridView).EndInit();
((System.ComponentModel.ISupportInitialize)DbBindingSource).EndInit();
((System.ComponentModel.ISupportInitialize)PagesBindingSource).EndInit();
ResumeLayout(false);
PerformLayout();
}
#endregion
private CoreBindingNavigator BindingNavigator;
private TextBox SearchTextBox;
private DataGridView DataGridView;
private BindingSource DbBindingSource;
private BindingSource PagesBindingSource;
private DataGridViewTextBoxColumn Id;
private DataGridViewTextBoxColumn EntityName;
private DataGridViewTextBoxColumn Rarity;
}
@@ -0,0 +1,390 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using Pf2eModel;
using Pf2eModel.Model.Base;
using Pf2eModel.Model.Common.I18n;
using Pf2eModel.Model.Enum;
using Timer = System.Windows.Forms.Timer;
namespace Pf2eViewer.DatabaseViewer;
/// <summary>
/// Класс элемента управления, который отображает сущности в виде таблицы.
/// </summary>
public partial class DatabaseViewerControl : UserControl
{
#region "Поля и свойства"
/// <summary>
/// Контекст базы данных, который используется для получения сущностей.
/// </summary>
public readonly Pf2eDbContext Context = new();
/// <summary>
/// Количество элементов на странице.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public int PerPage
{
get => _perPage;
set
{
_perPage = value;
UpdateView();
}
}
private int _perPage = 30;
/// <summary>
/// Номер страницы, которую необходимо отобразить.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public int Page
{
get => _page;
set
{
_page = value;
UpdateView();
}
}
private int _page = 1;
/// <summary>
/// Тип сущности, которую необходимо отобразить.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public EntityType Type
{
get => _type;
set
{
_page = 1;
_type = value;
UpdateView();
}
}
private EntityType _type;
/// <summary>
/// Количество элементов, которые были получены из базы данных.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public int Count { get; private set; }
/// <summary>
/// Номер последней страницы, которая может быть отображена.
/// </summary>
public int MaxPage => Convert.ToInt32(Math.Ceiling(1.0 * Count / PerPage));
/// <summary>
/// Выбранная в элементе управления сущность.
/// </summary>
[AllowNull]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public EntityView CurrentEntity { get; private set; }
/// <summary>
/// Флаг, указывающий, что элемент управления занят выполнением запроса к базе данных.
/// </summary>
private bool _isBusy = true;
/// <summary>
/// Объект, который выполняет запрос к базе данных.
/// </summary>
private DatabaseQueryPerformer? _performer;
/// <summary>
/// Таймер, который используется для задержки обработки события изменения текста в поле поиска.
/// </summary>
private Timer? _typingTimer;
/// <summary>
/// Строка, которая хранит последнее значение, введенное в поле поиска.
/// </summary>
private string _lastSearch = "";
#endregion
/// <summary>
/// Конструктор класса, который инициализирует элемент управления и связывает его с источником данных.
/// </summary>
public DatabaseViewerControl()
{
InitializeComponent();
BindingNavigator.BindingSource = DbBindingSource;
//DataGridView.AutoGenerateColumns = true;
DataGridView.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
_isBusy = false;
}
#region "События элемента управления"
/// <summary>
/// Обработчик события, который вызывается при изменении текущей страницы в элементе управления.
/// </summary>
/// <param name="sender">Источник события.</param>
/// <param name="e">Объект, содержащий данные события.</param>
private void BindingNavigator_RefreshItems(object sender, EventArgs e)
{
if (_isBusy)
{
return;
}
Page = (int)(BindingNavigator?.BindingSource?.Current ?? 1);
UpdateView();
}
/// <summary>
/// Обработчик события, который вызывается при изменении выделенной строки в элементе управления DataGridView.
/// </summary>
/// <param name="sender">Источник события.</param>
/// <param name="e">Объект, содержащий данные события.</param>
private void DataGridView_RowStateChanged(object sender, DataGridViewRowStateChangedEventArgs e)
{
if (_isBusy)
{
return;
}
UpdateEntity();
}
/// <summary>
/// Обработчик события, который вызывается когда поле поиска теряет фокус.
/// </summary>
/// <param name="sender">Источник события.</param>
/// <param name="e">Объект, содержащий данные события.</param>
private void SearchTextBox_Leave(object sender, EventArgs e)
{
if (_isBusy || SearchTextBox.Text == _lastSearch)
{
return;
}
UpdateView();
}
/// <summary>
/// Обработчик события, который вызывается при изменении текста в поле поиска.
/// </summary>
/// <param name="sender">Источник события.</param>
/// <param name="e">Объект, содержащий данные события.</param>
private void SearchTextBox_TextChanged(object sender, EventArgs e)
{
if (_isBusy)
{
return;
}
if (_typingTimer == null)
{
_typingTimer = new();
_typingTimer.Interval = 3000;
_typingTimer.Tick += HandleTypingTimerTimeout;
}
_typingTimer.Stop(); // Resets the timer
_typingTimer.Tag = (sender as TextBox)!.Text; // This should be done with EventArgs
_typingTimer.Start();
}
/// <summary>
/// Обработчик события, который вызывается при истечении времени таймера ввода текста.
/// </summary>
/// <param name="sender">Источник события.</param>
/// <param name="e">Объект, содержащий данные события.</param>
private void HandleTypingTimerTimeout(object? sender, EventArgs e)
{
if (_isBusy || sender is not Timer timer)
{
return;
}
UpdateView();
timer.Stop();
}
#endregion
#region "Обновление данных"
/// <summary>
/// Устанавливает статус занятости элемента управления.
/// </summary>
/// <param name="busy">Новый статус занятости.</param>
private void SetBusyStatus(bool busy)
{
if (InvokeRequired)
{
Invoke(SetBusyStatus, busy);
return;
}
_isBusy = busy;
BindingNavigator.Enabled = !busy;
DataGridView.Enabled = !busy;
SearchTextBox.Enabled = !busy;
DataGridView.ColumnHeadersHeightSizeMode = !busy
? DataGridViewColumnHeadersHeightSizeMode.AutoSize
: DataGridViewColumnHeadersHeightSizeMode.DisableResizing;
}
/// <summary>
/// Обновляет данные в элементе управления.
/// </summary>
/// <param name="force">Проигнорировать флаг занятости.</param>
public void UpdateView(bool force = false)
{
if (_isBusy && !force)
{
return;
}
SetBusyStatus(true);
_performer?.Abort();
_performer = new()
{
Type = Type,
Page = Page,
PerPage = PerPage,
Search = SearchTextBox.Text,
};
_performer.OnQueryCompleted += HandleQueryCompleted;
_performer.Query();
}
/// <summary>
/// Обработчик события, который вызывается по завершении выполнения запроса к базе данных.
/// </summary>
/// <param name="sender">Источник события.</param>
/// <param name="result">Результат выполнения события</param>
private void HandleQueryCompleted(DatabaseQueryPerformer sender, DatabaseQueryResult result)
{
if (sender != _performer)
{
return;
}
Count = result.Count;
_lastSearch = SearchTextBox.Text;
InvokeControl(
BindingNavigator,
b =>
{
PagesBindingSource.DataSource = Enumerable.Range(1, MaxPage).ToList();
b.BindingSource = PagesBindingSource;
}
);
InvokeControl(
DataGridView,
view =>
{
view.DataSource = result
.Entities.Select(e => new EntityGridRow(e))
.OrderBy(r => r.Id)
.ToList();
SetBusyStatus(false);
if (view.Rows.Count > 0)
{
view.Rows[0].Selected = true;
UpdateEntity();
}
}
);
}
/// <summary>
/// Вызывает действие на указанном элементе управления в нужном потоке , если это необходимо.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="control"></param>
/// <param name="action"></param>
private void InvokeControl<T>(T control, Action<T> action)
where T : Control
{
if (control.InvokeRequired)
{
control.Invoke(action, control);
}
else
{
action(control);
}
}
/// <summary>
/// Обновить сущность, которая была выбрана в элементе управления DataGridView.
/// </summary>
private void UpdateEntity()
{
if (DataGridView.SelectedRows.Count == 0)
{
return;
}
EntityGridRow? data = (EntityGridRow?)DataGridView.SelectedRows[0].DataBoundItem;
try
{
CurrentEntity = data is not null ? new(Context, Type, data.Id) : null;
}
catch (Exception)
{
// ignored
}
EntityChange?.Invoke(CurrentEntity);
}
#endregion
#region Output events
/// <summary>
/// Делегат, который будет вызван при изменении выбранной сущности.
/// </summary>
/// <param name="newEntity">Новая выбранная сущность.</param>
public delegate void EntityChangeHandler(EntityView newEntity);
/// <summary>
/// Событие, которое вызывается при изменении выбранной сущности.
/// </summary>
public event EntityChangeHandler? EntityChange;
#endregion
/// <summary>
/// Класс, который представляет строку в таблице сущностей.
/// </summary>
/// <param name="entity">Сущность, связанная со строкой</param>
internal class EntityGridRow(Entity entity)
{
/// <summary>
/// Идентификатор сущности.
/// </summary>
public int Id { get; set; } = entity.Id;
/// <summary>
/// Название сущности.
/// </summary>
public string Name { get; set; } = entity.Name[LocaleType.English];
/// <summary>
/// Редкость сущности.
/// </summary>
public EntityRarity Rarity { get; set; } = entity.Rarity;
}
}
@@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="Id.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="Rarity.UserAddedColumn" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="DbBindingSource.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<metadata name="PagesBindingSource.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>163, 17</value>
</metadata>
</root>
@@ -0,0 +1,60 @@
namespace Pf2eViewer.EntityTreeViewer;
sealed partial class EntityTreeViewerControl
{
/// <summary>
/// Обязательная переменная конструктора.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Освободить все используемые ресурсы.
/// </summary>
/// <param name="disposing">истинно, если управляемый ресурс должен быть удален; иначе ложно.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Код, автоматически созданный конструктором компонентов
/// <summary>
/// Требуемый метод для поддержки конструктора — не изменяйте
/// содержимое этого метода с помощью редактора кода.
/// </summary>
private void InitializeComponent()
{
ContentPanel = new PictureBox();
((System.ComponentModel.ISupportInitialize)ContentPanel).BeginInit();
SuspendLayout();
//
// ContentPanel
//
ContentPanel.Location = new Point(0, 0);
ContentPanel.Name = "ContentPanel";
ContentPanel.Size = new Size(400, 500);
ContentPanel.TabIndex = 0;
ContentPanel.TabStop = false;
ContentPanel.MouseMove += ContentPanel_MouseMove;
//
// EntityTreeViewerControl
//
AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font;
AutoScroll = true;
Controls.Add(ContentPanel);
Name = "EntityTreeViewerControl";
Size = new Size(400, 500);
Resize += EntityTreeViewerControl_Resize;
((System.ComponentModel.ISupportInitialize)ContentPanel).EndInit();
ResumeLayout(false);
}
#endregion
public PictureBox ContentPanel;
}
@@ -0,0 +1,394 @@
using System.ComponentModel;
namespace Pf2eViewer.EntityTreeViewer;
/// <summary>
/// Элемент управления для отображения структуры сущности.
/// </summary>
public sealed partial class EntityTreeViewerControl : UserControl
{
#region "Поля элемента управления"
/// <summary>
/// Высота строки поля структуры.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[Category("Entity Render")]
[Description("The height of each item in the tree view.")]
public int ItemHeight
{
get => _itemHeight;
set
{
_itemHeight = value;
UpdateView();
}
}
private int _itemHeight = 20;
/// <summary>
/// Сущность, которая будет отображаться в элементе управления.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public EntityView? EntityView
{
get => _entityView;
set
{
_entityView = value;
UpdateData();
}
}
private EntityView? _entityView;
/// <summary>
/// Ширина отступа глубины.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[Category("Entity Render")]
[Description("The size of tab.")]
public int DepthSize
{
get => _depthSize;
set
{
_depthSize = value;
UpdateView();
}
}
private int _depthSize = 20;
/// <summary>
/// Размер шрифта типа поля сущности.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[Category("Entity Render")]
[Description("The size of tab.")]
public float TypeFontSize
{
get => _typeFontSize;
set
{
_typeFontSize = value;
UpdateView();
}
}
private float _typeFontSize = 9.5f;
/// <summary>
/// Размер кнопки плюс/минус.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[Category("Entity Render")]
[Description("The size of plus/minus button.")]
public int ButtonSize
{
get => _buttonSize;
set
{
_buttonSize = value;
UpdateView();
}
}
private int _buttonSize = 20;
/// <summary>
/// Ширина имени поля сущности.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public int NameWidth { get; private set; } = 200;
/// <summary>
/// Цвет фона строки поля при наведении на элемент управления.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[Category("Entity Render")]
[Description("Color on item hover.")]
public Color HoverColor
{
get => _hoverColor;
set
{
_hoverColor = value;
UpdateView();
}
}
private Color _hoverColor = SystemColors.ControlDark;
/// <summary>
/// Изображение, используемое для отображения содержимого элемента управления.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public Bitmap ContentImage { get; set; }
#endregion
#region "Вычисляемые поля"
/// <summary>
/// Ширина области содержимого элемента управления.
/// </summary>
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public int ContentWidth => Width - SystemInformation.VerticalScrollBarWidth - 2;
#endregion
#region "Приватные поля"
/// <summary>
/// Коллекция полей структуры.
/// </summary>
private readonly List<StructureField> _rows = [];
/// <summary>
/// Коллекция строк полей структуры, которые будут отображаться в элементе управления.
/// </summary>
private List<EntityTreeViewerControlRow> _viewerRows = [];
/// <summary>
/// Флаг, указывающий, что элемент управления в данный момент перерисовывается.
/// </summary>
private bool _rendering;
#endregion
/// <summary>
/// Конструктор класса <see cref="EntityTreeViewerControl"/>.
/// </summary>
public EntityTreeViewerControl()
{
InitializeComponent();
ContentImage = new(ContentWidth, Height);
DoubleBuffered = true;
VerticalScroll.Visible = true;
HorizontalScroll.Visible = false;
ContentPanel.MouseWheel += ContentPanel_MouseWheel;
}
#region "Методы обновления"
/// <summary>
/// Обновление данных в элементе управления.
/// </summary>
public void UpdateData()
{
if (EntityView?.Entity is null)
{
return;
}
_rows.Clear();
ContentPanel.Controls.Clear();
StructureBuilder builder = new();
_rows.Add(builder.Create("Entity", EntityView.Entity));
if (EntityView.SubEntity is not null)
{
string typeName = EntityView.SubEntity.GetType().Name;
_rows.Add(builder.Create(typeName, EntityView.SubEntity));
}
_viewerRows = _rows
.SelectMany(r => r.GetRows(row => new EntityTreeViewerControlRow(this, row)))
.ToList();
UpdateRenders();
}
/// <summary>
/// Обновление отрисовок в элементе управления.
/// </summary>
public void UpdateRenders()
{
int maxNameWidth = -1;
foreach (EntityTreeViewerControlRow row in _viewerRows)
{
int nameWidth = row.NameWidth + row.Item.Depth * DepthSize;
maxNameWidth = Math.Max(maxNameWidth, nameWidth);
}
NameWidth = maxNameWidth + 3;
// Debug.WriteLine($@"Height Inner {ContentWidth} / Outer {Width}");
// Debug.WriteLine($@"Name {NameWidth} / Full {ContentWidth}");
foreach (EntityTreeViewerControlRow row in _viewerRows)
{
row.UpdateCache();
}
UpdateView();
}
/// <summary>
/// Обновление отображения в элементе управления.
/// </summary>
public void UpdateView()
{
_viewerRows.ForEach(row => row.UpdateButton());
List<EntityTreeViewerControlRow> rows = _viewerRows.Where(r => r.Item.Visible).ToList();
if (rows.Count == 0)
{
return;
}
BuildLadder(rows);
_rendering = true;
ContentImage = new(ContentWidth, rows.Count * ItemHeight);
Graphics g = Graphics.FromImage(ContentImage);
int viewRow = 0;
foreach (EntityTreeViewerControlRow row in rows)
{
row.ViewIndex = viewRow++;
row.Invalidate(g);
}
g.Save();
ContentPanel.Image = ContentImage;
ContentPanel.Width = ContentImage.Width;
ContentPanel.Height = ContentImage.Height;
_rendering = false;
}
#endregion
#region "Вспомогательные методы"
/// <summary>
/// Построение лестницы для строк полей структуры.
/// </summary>
/// <param name="rows">Набор строк полей структуры.</param>
private void BuildLadder(List<EntityTreeViewerControlRow> rows)
{
List<LadderType> lastLadder = [];
for (int i = rows.Count - 1; i >= 0; i--)
{
EntityTreeViewerControlRow row = rows[i];
List<LadderType> ladder;
if (row.Depth == 0)
{
ladder = [];
}
else if (row.Depth < lastLadder.Count)
{
ladder =
[
.. lastLadder[..(row.Depth - 1)],
lastLadder[row.Depth - 1] == LadderType.Empty
? LadderType.Last
: LadderType.Through,
];
}
else if (row.Depth == lastLadder.Count)
{
ladder = lastLadder;
}
else
{
ladder =
[
.. lastLadder,
.. Enumerable.Repeat(LadderType.Empty, row.Depth - lastLadder.Count - 1),
LadderType.Last,
];
}
row.Ladder = ladder;
lastLadder = ladder
.Select(l => (l == LadderType.Last) ? LadderType.Through : l)
.ToList();
}
}
#endregion
#region "События элемента управления"
/// <summary>
/// Обработчик события изменения размера элемента управления.
/// </summary>
/// <param name="sender">Источник события.</param>
/// <param name="e">Объект события.</param>
private void EntityTreeViewerControl_Resize(object sender, EventArgs e)
{
if (_rendering)
{
return;
}
ContentPanel.Width = Width;
UpdateRenders();
}
/// <summary>
/// Обработчик события прокрутки элемента управления.
/// </summary>
/// <param name="sender">Источник события.</param>
/// <param name="e">Объект события.</param>
private void ContentPanel_MouseWheel(object? sender, MouseEventArgs e)
{
/*
int initValue = VerticalScroll.Value;
int delta = -e.Delta / 120;
VerticalScroll.Value = Math.Clamp(
initValue + delta,
VerticalScrollBar.Minimum,
VerticalScrollBar.Maximum - VerticalScrollBar.LargeChange + 1
);
if (VerticalScrollBar.Value != initValue)
{
ContentPanel.Invalidate();
}
*/
}
/// <summary>
/// Обработчик события движения мыши над элементом управления.
/// </summary>
/// <param name="sender">Источник события.</param>
/// <param name="e">Объект события.</param>
private void ContentPanel_MouseMove(object sender, MouseEventArgs e)
{
PointF p = new(e.X, e.Y);
foreach (EntityTreeViewerControlRow row in _viewerRows)
{
bool contained = row.Target.Contains(p);
if (contained != row.Hovered)
{
Rectangle r = new();
row.RealTarget.Intersect(r);
ContentPanel.Invalidate(r);
}
row.Hovered = contained;
}
}
#endregion
}
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>
@@ -0,0 +1,606 @@
using FontAwesome.Sharp;
using Font = System.Drawing.Font;
using FontStyle = System.Drawing.FontStyle;
using Image = System.Drawing.Image;
namespace Pf2eViewer.EntityTreeViewer;
/// <summary>
/// Элемент управления для отображения строки поля сущности.
/// </summary>
internal class EntityTreeViewerControlRow
{
/// <summary>
/// Элемент управления, которому принадлежит эта строка.
/// </summary>
public readonly EntityTreeViewerControl ParentControl;
/// <summary>
/// Отображаемая строка поля сущности.
/// </summary>
public readonly StructureField Item;
#region "Поля строки"
/// <summary>
/// Кэш изображения строки.
/// </summary>
public Image Cache
{
get
{
if (_cache == null)
{
UpdateCache();
}
return _cache!;
}
set => _cache = value;
}
private Image? _cache;
/// <summary>
/// Кэш изображения строки при наведении.
/// </summary>
public Image HoveredCache
{
get
{
if (_hoveredCache == null)
{
UpdateCache();
}
return _hoveredCache!;
}
set => _hoveredCache = value;
}
private Image? _hoveredCache;
/// <summary>
/// Список типов лестницы.
/// </summary>
public List<LadderType> Ladder
{
get => _ladder;
set
{
_ladder = value;
_cache = null;
}
}
private List<LadderType> _ladder = [];
/// <summary>
/// Проверяет, находится ли курсор над строкой.
/// </summary>
public bool Hovered
{
get => _hovered;
set
{
if (value != _hovered)
{
_hovered = value;
Invalidate();
}
}
}
private bool _hovered = false;
/// <summary>
/// Индекс строки в представлении.
/// </summary>
public int ViewIndex { get; set; } // = -1;
/// <summary>
/// Кнопка раскрытия/сворачивания.
/// </summary>
public IconButton? Button { get; set; } = null;
#endregion
#region "Поля для рендеринга"
/// <inheritdoc cref="EntityTreeViewerControl.Font"/>
public Font Font
{
get => ParentControl.Font;
set => ParentControl.Font = value;
}
/// <inheritdoc cref="EntityTreeViewerControl.ForeColor"/>
public Color ForeColor
{
get => ParentControl.ForeColor;
set => ParentControl.ForeColor = value;
}
/// <inheritdoc cref="EntityTreeViewerControl.BackColor"/>
public Color BackColor
{
get => ParentControl.BackColor;
set => ParentControl.BackColor = value;
}
/// <inheritdoc cref="EntityTreeViewerControl.HoverColor"/>
public Color HoverColor
{
get => ParentControl.HoverColor;
set => ParentControl.HoverColor = value;
}
/// <inheritdoc cref="EntityTreeViewerControl.ItemHeight"/>
public int ItemHeight
{
get => ParentControl.ItemHeight;
set => ParentControl.ItemHeight = value;
}
/// <inheritdoc cref="EntityTreeViewerControl.DepthSize"/>
public int DepthSize
{
get => ParentControl.DepthSize;
set => ParentControl.DepthSize = value;
}
/// <inheritdoc cref="EntityTreeViewerControl.ButtonSize"/>
public int ButtonSize
{
get => ParentControl.ButtonSize;
set => ParentControl.ButtonSize = value;
}
/// <inheritdoc cref="EntityTreeViewerControl.TypeFontSize"/>
public float TypeFontSize
{
get => ParentControl.TypeFontSize;
set => ParentControl.TypeFontSize = value;
}
#endregion
#region "Вычисляемые поля"
/// <summary>
/// Ширина строки.
/// </summary>
public int Width => ParentControl.ContentWidth;
/// <summary>
/// Высота строки.
/// </summary>
public int Height => ItemHeight;
/// <summary>
/// Флаг наличия кнопки.
/// </summary>
public bool HasButton => Item.FieldType == StructureFieldType.Complex;
/// <summary>
/// Глубина отображения строки.
/// </summary>
public int Depth => Item.Depth;
/// <summary>
/// Ширина имени в строке.
/// </summary>
public int NameWidth
{
get
{
Graphics g = Graphics.FromImage(Cache);
return Convert.ToInt32(g.MeasureString(Item.Name, Font).Width + DepthSize);
}
}
/// <summary>
/// Строковое представление типа поля.
/// </summary>
public string TypeText
{
get
{
string result = Item.GetTypeString();
if (result != "")
{
result = $@"({result})";
}
return result;
}
}
/// <summary>
/// Шрифт для отображения типа поля.
/// </summary>
public Font TypeFont => new(Font.FontFamily, TypeFontSize);
/// <summary>
/// Строковое представление значения поля.
/// </summary>
public string ValueText => Item.GetValueString();
/// <summary>
/// Шрифт для отображения значения поля.
/// </summary>
public Font ValueFont =>
new(
Font.FontFamily,
Font.Size,
Item.FieldType == StructureFieldType.Complex ? FontStyle.Italic : FontStyle.Regular
);
/// <summary>
/// Прямоугольник, занимаемый строкой в представлении.
/// </summary>
public RectangleF Target => new(0, ViewIndex * Height, Width, Height);
/// <summary>
/// Прямоугольник, занимаемый строкой в родительском представлении.
/// </summary>
public RectangleF RealTarget =>
new(0, ViewIndex * Height - ParentControl.VerticalScroll.Value, Width, Height);
#endregion
/// <summary>
/// Список данных для рендеринга строк.
/// </summary>
private readonly List<StringRenderData> _renderData = [];
#region "Основные методы"
/// <summary>
/// Создание элемента управления строки поля сущности.
/// </summary>
/// <param name="parent">Родительский элемент управления.</param>
/// <param name="item">Строка поля структуры объекта.</param>
public EntityTreeViewerControlRow(EntityTreeViewerControl parent, StructureField item)
{
ParentControl = parent;
Item = item;
ApplyItem();
UpdateCache();
UpdateButton();
}
/// <summary>
/// Применение свойств к элементу управления.
/// </summary>
private void ApplyItem()
{
_renderData.Clear();
_renderData.Add(new(Item.Name, Font, ForeColor));
_renderData.Add(new(TypeText, TypeFont, GetValueColor()));
_renderData.Add(
new(ValueText.Replace("\r", "").Replace('\n', ' '), ValueFont, GetValueColor())
);
if (HasButton)
{
Button = new()
{
Width = ButtonSize,
Height = ButtonSize,
IconSize = ButtonSize,
IconChar = IconChar.MinusSquare,
ImageAlign = ContentAlignment.MiddleCenter,
TextAlign = ContentAlignment.MiddleCenter,
Visible = false,
};
Button.Click += Button_Click;
ParentControl.ContentPanel.Controls.Add(Button);
}
}
/// <summary>
/// Обновление кэша изображения строки.
/// </summary>
public void UpdateCache()
{
Cache = RenderCache(BackColor);
HoveredCache = RenderCache(HoverColor);
}
/// <summary>
/// Создание кэша изображения строки.
/// </summary>
/// <param name="backColor">Цвет заднего фона.</param>
/// <returns>Кеш изображения строки.</returns>
private Image RenderCache(Color backColor)
{
Image cache = new Bitmap(Width, Height);
Graphics g = Graphics.FromImage(cache);
g.FillRectangle(new SolidBrush(backColor), 0, 0, Width, Height);
RenderLadder(g);
RenderContent(g);
g.Save();
return cache;
}
/// <summary>
/// Запрос на перерисовку строки.
/// </summary>
/// <param name="g">Графический объект.</param>
public void Invalidate(Graphics? g = null)
{
g ??= Graphics.FromImage(ParentControl.ContentImage);
g.DrawImage(Hovered ? HoveredCache : Cache, Target);
UpdateButton();
}
#endregion
#region "Кнопка"
/// <summary>
/// Обновление кнопки раскрытия/сворачивания.
/// </summary>
public void UpdateButton()
{
if (Button is null)
{
return;
}
int cx = Convert.ToInt32((0.5 + Depth) * DepthSize - 0.5 * ButtonSize);
int cy = Convert.ToInt32((0.5 + ViewIndex) * ItemHeight - 0.5 * ButtonSize);
Button.Left = cx;
Button.Top = cy;
Button!.IconChar = ((StructureFieldComplex)Item).Collapsed
? IconChar.PlusSquare
: IconChar.MinusSquare;
Button.Visible = Item.Visible;
}
/// <summary>
/// Событие нажатия кнопки раскрытия/сворачивания.
/// </summary>
/// <param name="sender">Источник события.</param>
/// <param name="e">Событие нажатие кнопки</param>
private void Button_Click(object? sender, EventArgs e)
{
if (Item is StructureFieldComplex complex)
{
complex.Collapsed = !complex.Collapsed;
ParentControl.UpdateView();
}
}
#endregion
#region "Отрисовка"
/// <summary>
/// Отрисовка лестницы.
/// </summary>
/// <param name="g">Графический объект.</param>
private void RenderLadder(Graphics g)
{
int n = Depth;
int h = ItemHeight;
int w = DepthSize;
if (_ladder.Count < Depth)
{
return;
}
Pen pen = new(Color.FromArgb(127, ForeColor.R, ForeColor.G, ForeColor.B), 1);
pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dot;
float cy = 0.5f * h;
for (int i = 0; i < n; i++)
{
float cx = (i + 1.5f) * w;
switch (_ladder[i])
{
case LadderType.Last:
g.DrawLine(pen, cx, 0, cx, cy);
break;
case LadderType.Through:
g.DrawLine(pen, cx, 0, cx, h);
break;
case LadderType.Empty:
default:
break;
}
if (i == n - 1)
{
g.DrawLine(pen, cx, cy, (i + 2) * w, cy);
}
}
}
/// <summary>
/// Отрисовка содержимого строки.
/// </summary>
/// <param name="g">Графический объект.</param>
private void RenderContent(Graphics g)
{
float[] xs =
[
DepthSize * (Item.Depth + 1),
ParentControl.NameWidth,
ParentControl.NameWidth + g.MeasureString(TypeText, TypeFont).Width + 3,
Width,
];
Pen p = new(Color.Aqua, 1);
for (int i = 0; i < 3; i++)
{
StringRenderData item = _renderData[i];
item.Left = xs[i];
item.Width = xs[i + 1] - xs[i];
item.Height = ItemHeight;
// g.DrawRectangle(p, item.Rect);
item.Draw(g);
}
}
#endregion
#region Функции-помощники в отрисовке
/// <summary>
/// Смешивание цвета с цветом текста.
/// </summary>
/// <param name="color">Цвет оттенка.</param>
/// <param name="rate">Степень смешения.</param>
/// <returns>Смешенный цвет.</returns>
private Color BlendColor(Color color, double rate = 0.5)
{
return BlendColor(ForeColor, color, rate);
}
/// <summary>
/// Смешивание цветов.
/// </summary>
/// <param name="color1">Первый смешиваемый цвет.</param>
/// <param name="color2">Второй смешиваемый цвет.</param>
/// <param name="rate">Степень смешения.</param>
/// <returns>Смешенный цвет.</returns>
private Color BlendColor(Color color1, Color color2, double rate)
{
return Color.FromArgb(
Mix(color1.A, color2.A),
Mix(color1.R, color2.R),
Mix(color1.G, color2.G),
Mix(color1.B, color2.B)
);
int Mix(int a, int b) => Convert.ToInt32(Math.Round(a * rate + b * (1 - rate)));
}
/// <summary>
/// Получение цвета текста по типу значения поля.
/// </summary>
/// <returns>Цвет текста.</returns>
public Color GetValueColor()
{
return Item switch
{
StructureFieldPrimitive primitive => Type.GetTypeCode(primitive.Type) switch
{
TypeCode.Boolean => BlendColor(Color.DarkGoldenrod),
TypeCode.Int32 => BlendColor(Color.Blue),
TypeCode.Double => BlendColor(Color.Aqua),
TypeCode.String => BlendColor(Color.Green),
_ => ForeColor,
},
StructureFieldEnum => BlendColor(Color.Red),
StructureFieldNull => BlendColor(BackColor),
_ => ForeColor,
};
}
#endregion
/// <summary>
/// Класс для хранения данных рендеринга строки.
/// </summary>
/// <param name="text">Текст отображаемой строки.</param>
/// <param name="font">Шрифт отображаемой строки.</param>
/// <param name="color">Цвет отображаемой строки.</param>
private class StringRenderData(string text, Font font, Color color)
{
/// <summary>
/// Текст отображаемой строки.
/// </summary>
public string Text { get; } = text;
/// <summary>
/// Шрифт отображаемой строки.
/// </summary>
public Font Font { get; } = font;
/// <summary>
/// Цвет отображаемой строки.
/// </summary>
public Color Color { get; } = color;
/// <summary>
/// Левый край отображаемой строки.
/// </summary>
public float Left { get; set; }
/// <summary>
/// Ширина отображаемой строки.
/// </summary>
public float Width { get; set; }
/// <summary>
/// Высота отображаемой строки.
/// </summary>
public float Height { get; set; }
/// <summary>
/// Прямоугольник, занимаемый строкой.
/// </summary>
public RectangleF Rect => new(Left, 0, Width, Height);
/// <summary>
/// Уместить строку в занимаемый прямоугольник.
/// </summary>
/// <param name="g">Элемент графики.</param>
/// <param name="rect">Занимаемый прямоугольник</param>
/// <param name="filler">Строка, на которую будет заканчиваться значнеие при переполнении.</param>
/// <returns></returns>
private string Fit(Graphics g, RectangleF rect, string filler = "…")
{
if (Text.Length == 0 || g.MeasureString(Text, Font).Width <= rect.Width)
{
return Text;
}
string txt = Text.Length > 100 ? Text[..100] : Text;
int i = txt.Length;
while (g.MeasureString(txt + filler, Font).Width > rect.Width)
{
txt = txt[..--i];
if (i == 0)
break;
}
return txt + filler;
}
/// <summary>
/// Отрисовка строки.
/// </summary>
/// <param name="g">Элемент графики.</param>
public void Draw(Graphics g)
{
g.DrawString(
Fit(g, Rect),
Font,
new SolidBrush(Color),
Rect,
new StringFormat
{
Alignment = StringAlignment.Near,
LineAlignment = StringAlignment.Center,
FormatFlags = StringFormatFlags.NoWrap,
}
);
}
}
}
@@ -0,0 +1,627 @@
using System.Collections;
using System.Reflection;
using LinqToDB.Extensions;
using Pf2eModel.Model.Common.I18n;
namespace Pf2eViewer.EntityTreeViewer;
/// <summary>
/// Класс для построения структуры данных класса.
/// </summary>
internal class StructureBuilder
{
/// <summary>
/// Список обработанных объектов.
/// </summary>
private readonly List<StructureProcessedObject> _processedObjects = [];
/// <summary>
/// Создание поля структуры.
/// </summary>
/// <param name="name">Название поля.</param>
/// <param name="value">Значение поля.</param>
/// <param name="depth">Глубина, на котором поле отображается.</param>
/// <returns>Поле структуры.</returns>
public StructureFieldComplex Create(string name, object value, int depth = 0)
{
_ = RegisterProcessed(value);
StructureFieldComplex field = new(this, value, name, depth);
CompleteProcessed(value, field);
return field;
}
/// <summary>
/// Регистрация обработанного объекта.
/// </summary>
/// <param name="value">Регистрируемый объект.</param>
/// <returns>`true`, если объект был добавлен; `false`, если объект был добавлен ранее.</returns>
public bool RegisterProcessed(object value)
{
if (_processedObjects.Any(p => p.Value == value))
{
return false;
}
StructureProcessedObject processed = new(value);
_processedObjects.Add(processed);
return true;
}
/// <summary>
/// Дополнить обработанный объект.
/// </summary>
/// <param name="value">Значение.</param>
/// <param name="field">Поле объекта.</param>
public void CompleteProcessed(object value, StructureFieldComplex field)
{
StructureProcessedObject? q = _processedObjects.FirstOrDefault(p => p.Value == value);
if (q == null)
{
return;
}
q.Field = field;
}
/// <summary>
/// Выбор поля структуры по значению.
/// </summary>
/// <param name="name">Название поля.</param>
/// <param name="value">Значение поля.</param>
/// <param name="depth">Глубина, на котором поле отображается.</param>
/// <returns></returns>
public StructureField Resolve(string name, object? value, int depth)
{
switch (GetFieldType(value))
{
case StructureFieldType.Null:
return new StructureFieldNull(this, name, depth);
case StructureFieldType.Primitive:
return new StructureFieldPrimitive(this, value!, name, depth);
case StructureFieldType.Enum:
return new StructureFieldEnum(this, value!, name, depth);
case StructureFieldType.Reference:
break;
case StructureFieldType.Complex:
default:
StructureProcessedObject? processed = _processedObjects.FirstOrDefault(p =>
p.Value == value
);
if (processed is null)
{
return Create(name, value!, depth);
}
StructureFieldReference reference = new StructureFieldReference(
this,
name,
null,
depth
);
processed.AddReference(reference);
return reference;
}
throw new("Impossible");
}
/// <summary>
/// Получить тип поля структуры по значению.
/// </summary>
/// <param name="value">Значение поля.</param>
/// <returns>Тип поля структуры.</returns>
public static StructureFieldType GetFieldType(object? value)
{
if (value is null)
{
return StructureFieldType.Null;
}
Type type = value.GetType();
if (type.IsEnum)
{
return StructureFieldType.Enum;
}
if (type.IsPrimitive || type == typeof(string))
{
return StructureFieldType.Primitive;
}
return StructureFieldType.Complex;
}
}
/// <summary>
/// Класс для хранения обработанного объекта.
/// </summary>
/// <param name="value">Объект.</param>
internal class StructureProcessedObject(object value)
{
public readonly object Value = value;
private StructureFieldComplex? _field = null;
public StructureFieldComplex? Field
{
get => _field;
set
{
_field = value;
foreach (StructureFieldReference reference in _references)
{
reference.Reference = Field;
}
}
}
private readonly List<StructureFieldReference> _references = [];
public void AddReference(StructureFieldReference reference)
{
_references.Add(reference);
if (_field is not null)
{
reference.Reference = Field;
}
}
}
/// <summary>
/// Тип поля структуры.
/// </summary>
internal enum StructureFieldType
{
/// <summary>
/// Пустое поле.
/// </summary>
Null,
/// <summary>
/// Простое поле.
/// </summary>
Primitive,
/// <summary>
/// Поле с перечислением.
/// </summary>
Enum,
/// <summary>
/// Поле с объектом.
/// </summary>
Complex,
/// <summary>
/// Поле с ссылкой на объект.
/// </summary>
Reference,
}
/// <summary>
/// Тип лестницы глубины.
/// </summary>
internal enum LadderType
{
/// <summary>
/// Пустой элемент: [ ].
/// </summary>
Empty,
/// <summary>
/// Последний элемент: [└].
/// </summary>
Last,
/// <summary>
/// Проходной элемент: [├] или [│].
/// </summary>
Through,
}
/// <summary>
/// Базовый класс для полей структуры.
/// </summary>
/// <param name="builder">Класс построения структуры данных.</param>
/// <param name="name">Название поля.</param>
/// <param name="depth">Глубина поля.</param>
internal abstract class StructureField(StructureBuilder builder, string name, int depth)
{
/// <summary>
/// Название поля.
/// </summary>
public readonly string Name = name;
/// <summary>
/// Тип поля структуры.
/// </summary>
public abstract StructureFieldType FieldType { get; }
/// <summary>
/// Класс построения структуры данных.
/// </summary>
public readonly StructureBuilder Builder = builder;
/// <summary>
/// Флаг, указывающий, отображается ли поле.
/// </summary>
public virtual bool Visible { get; set; } = true;
/// <summary>
/// Глубина поля структуры.
/// </summary>
public readonly int Depth = depth;
/// <summary>
/// Получить строку, представляющую тип поля.
/// </summary>
/// <returns>Строка, представляющая тип поля.</returns>
public abstract string GetTypeString();
/// <summary>
/// Получить строку, представляющую значение поля.
/// </summary>
/// <returns>Строка, представляющая значение поля.</returns>
public abstract string GetValueString();
/// <summary>
/// Получить набор строк, используя результат функция.
/// </summary>
/// <typeparam name="T">Тип результирующей строки.</typeparam>
/// <param name="func">Функция, оборажения поля в результирующую строку.</param>
/// <returns>Коллекция, содержащая набор строк.</returns>
public virtual ICollection<T> GetRows<T>(Func<StructureField, T> func)
{
StructureField[] q = [this];
return q.Select(func).ToList();
}
}
/// <summary>
/// Пустое поле структуры.
/// </summary>
/// <param name="builder">Класс построения структуры данных.</param>
/// <param name="name">Название поля.</param>
/// <param name="depth">Глубина поля.</param>
internal class StructureFieldNull(StructureBuilder builder, string name, int depth)
: StructureField(builder, name, depth)
{
/// <inheritdoc/>
public override StructureFieldType FieldType => StructureFieldType.Null;
/// <inheritdoc/>
public override string GetTypeString() => "null";
/// <inheritdoc/>
public override string GetValueString() => "";
}
/// <summary>
/// Простое поле структуры.
/// </summary>
/// <param name="builder">Класс построения структуры данных.</param>
/// <param name="value">Значение поля.</param>
/// <param name="name">Название поля.</param>
/// <param name="depth">Глубина поля.</param>
internal class StructureFieldPrimitive(
StructureBuilder builder,
object value,
string name,
int depth
) : StructureField(builder, name, depth)
{
/// <inheritdoc/>
public override StructureFieldType FieldType => StructureFieldType.Primitive;
/// <summary>
/// Значение поля.
/// </summary>
public readonly object Value = value;
/// <summary>
/// Тип значения поля.
/// </summary>
public readonly Type Type = value.GetType();
/// <inheritdoc/>
public override string GetTypeString()
{
return Type.GetTypeCode(Type) switch
{
TypeCode.Boolean => "bool",
TypeCode.Int32 => "int",
TypeCode.String => "string",
TypeCode.Double => "double",
_ => Type.Name,
};
}
/// <inheritdoc/>
public override string GetValueString()
{
if (Type == typeof(string))
{
return "\"" + ((string)Value).Replace("\"", "\\\"") + "\"";
}
return Value.ToString() ?? string.Empty;
}
}
/// <summary>
/// Поле структуры с перечислением.
/// </summary>
/// <param name="builder">Класс построения структуры данных.</param>
/// <param name="value">Значение поля.</param>
/// <param name="name">Название поля.</param>
/// <param name="depth">Глубина поля.</param>
internal class StructureFieldEnum(StructureBuilder builder, object value, string name, int depth)
: StructureField(builder, name, depth)
{
/// <inheritdoc/>
public override StructureFieldType FieldType => StructureFieldType.Enum;
/// <summary>
/// Значение поля.
/// </summary>
public readonly object Value = value;
/// <summary>
/// Тип значения поля.
/// </summary>
public readonly Type Type = value.GetType();
/// <inheritdoc/>
public override string GetTypeString() => "enum";
/// <inheritdoc/>
public override string GetValueString() => $"{Type.Name}.{Value}";
}
/// <summary>
/// Поле стркутуры с объектом.
/// </summary>
internal class StructureFieldComplex : StructureField
{
/// <inheritdoc/>
public override StructureFieldType FieldType => StructureFieldType.Complex;
/// <summary>
/// Набор полей объекта.
/// </summary>
public readonly List<StructureField> Fields = [];
/// <summary>
/// Тип значения поля.
/// </summary>
public readonly Type Type;
/// <summary>
/// Флаг, указывающий, отображается ли поля объекта.
/// </summary>
public bool Collapsed
{
get => _collapsed;
set
{
_collapsed = value;
UpdateFieldVisibility();
}
}
private bool _collapsed = false;
/// <inheritdoc/>
public override bool Visible
{
get => _visible;
set
{
_visible = value;
UpdateFieldVisibility();
}
}
private bool _visible = true;
/// <summary>
/// Конструктор класса поля структуры с объектом.
/// </summary>
/// <param name="builder">Класс построения структуры данных.</param>
/// <param name="value">Значение поля.</param>
/// <param name="name">Название поля.</param>
/// <param name="depth">Глубина поля.</param>
public StructureFieldComplex(StructureBuilder builder, object value, string name, int depth)
: base(builder, name, depth)
{
Type = value.GetType();
SetFields((dynamic)value);
}
/// <summary>
/// Обновить видимость полей структуры.
/// </summary>
private void UpdateFieldVisibility()
{
foreach (StructureField field in Fields)
{
field.Visible = Visible && !Collapsed;
}
}
/// <summary>
/// Установить поля структуры `LocaleString`.
/// </summary>
/// <param name="localeString">Объект строки локализации.</param>
private void SetFields(LocaleString localeString)
{
SetFieldsFromRecords(
localeString
.Select(field => new FieldRecord(field.Locale.ToString(), field.Value))
.ToList()
);
}
/// <summary>
/// Установить поля структуры `IEnumerable`.
/// </summary>
/// <param name="enumerable">Объект-перечисление.</param>
private void SetFields(IEnumerable enumerable)
{
int i = 0;
List<FieldRecord> records = [];
foreach (object? item in enumerable)
{
records.Add(new($"[{i++}]", item));
}
SetFieldsFromRecords(records);
}
/// <summary>
/// Установить поля структуры общего объекта.
/// </summary>
/// <param name="value">Объект.</param>
private void SetFields(object value)
{
Type type = value.GetType();
PropertyInfo[] properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
SetFieldsFromRecords(
properties
.Where(property =>
property.GetAttribute<Newtonsoft.Json.JsonIgnoreAttribute>() is null
)
.Select(property => new FieldRecord(property.Name, property.GetValue(value, null)))
.ToList()
);
}
/// <summary>
/// Установить поля структуры из списка записей.
/// </summary>
/// <param name="records"></param>
private void SetFieldsFromRecords(List<FieldRecord> records)
{
foreach (
FieldRecord record in records.Where(record =>
record.FieldType == StructureFieldType.Complex
)
)
{
record.Allowed = Builder.RegisterProcessed(record.Value!);
}
foreach (FieldRecord record in records)
{
Fields.Add(
record is { FieldType: StructureFieldType.Complex, Allowed: true }
? Builder.Create(record.Name, record.Value!, Depth + 1)
: Builder.Resolve(record.Name, record.Value!, Depth + 1)
);
}
}
/// <summary>
/// Запись поля структуры.
/// </summary>
/// <param name="name">Название поля.</param>
/// <param name="value">Значение поля.</param>
private class FieldRecord(string name, object? value)
{
/// <summary>
/// Название поля.
/// </summary>
public readonly string Name = name;
/// <summary>
/// Значение поля.
/// </summary>
public readonly object? Value = value;
/// <summary>
/// Тип поля структуры.
/// </summary>
public StructureFieldType FieldType => StructureBuilder.GetFieldType(Value);
/// <summary>
/// Флаг, указывающий, использовать ли поле.
/// </summary>
public bool Allowed = true;
}
/// <inheritdoc/>
public override string GetTypeString()
{
if (Type.IsGenericEnumerableType())
{
Type elementType = Type.GetGenericArguments().Single();
return $"{elementType.Name}[]";
}
else
{
return Type.Name;
}
}
/// <inheritdoc/>
public override string GetValueString()
{
if (Type.IsGenericEnumerableType())
{
return Fields.Count.ToString() + (Fields.Count == 1 ? " item" : " items");
}
return "";
}
/// <inheritdoc/>
public override ICollection<T> GetRows<T>(Func<StructureField, T> func)
{
return [.. base.GetRows(func), .. Fields.SelectMany(f => f.GetRows(func)).ToList()];
}
}
/// <summary>
/// Поле структуры с ссылкой на объект.
/// </summary>
/// <param name="builder">Класс построения структуры данных.</param>
/// <param name="name">Название поля.</param>
/// <param name="reference">Ссылка на объект.</param>
/// <param name="depth">Глубина поля.</param>
internal class StructureFieldReference(
StructureBuilder builder,
string name,
StructureFieldComplex? reference,
int depth
) : StructureField(builder, name, depth)
{
/// <summary>
/// Ссылка на объект.
/// </summary>
public StructureFieldComplex? Reference = reference;
/// <inheritdoc/>
public override StructureFieldType FieldType => StructureFieldType.Reference;
/// <inheritdoc/>
public override string GetTypeString() => Reference?.GetTypeString() ?? "???";
/// <inheritdoc/>
public override string GetValueString() => "<Reference>";
}
@@ -6,16 +6,16 @@ namespace Pf2eDataScrapper.Forms.Controls;
internal partial class ProcessorCollectionControl : UserControl internal partial class ProcessorCollectionControl : UserControl
{ {
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public ProcessorCollection? Collection public ProcessorCollection Collection
{ {
get => _collection; get => _collection;
set set
{ {
_collection = value; _collection = value;
UpdateContent(); SetContent();
} }
} }
private ProcessorCollection? _collection = null; private ProcessorCollection _collection = null!;
public ProcessorCollectionControl() public ProcessorCollectionControl()
{ {
@@ -28,27 +28,24 @@ internal partial class ProcessorCollectionControl : UserControl
private Dictionary<string, CheckBox> ItemCheckBoxes { get; } = []; private Dictionary<string, CheckBox> ItemCheckBoxes { get; } = [];
private void UpdateContent() private void SetContent()
{ {
RootFlowLayoutPanel.Controls.Clear(); RootFlowLayoutPanel.Controls.Clear();
if (Collection == null)
{
return;
}
foreach (ProcessorGroupEnum group in Enum.GetValues<ProcessorGroupEnum>()) foreach (ProcessorGroupEnum group in Enum.GetValues<ProcessorGroupEnum>())
{ {
GroupCheckBoxes[group] = new() GroupCheckBoxes[group] = new()
{ {
Text = group.GetDescription(), Text = group.GetDescription(),
//AutoSize = false, Tag = group,
Dock = DockStyle.Fill, Dock = DockStyle.Fill,
ThreeState = true,
Checked = true, Checked = true,
AutoCheck = false,
CheckAlign = ContentAlignment.TopLeft, CheckAlign = ContentAlignment.TopLeft,
}; };
GroupCheckBoxes[group].CheckedChanged += GroupCheckBox_CheckedChanged; GroupCheckBoxes[group].Click += GroupCheckBox_Click;
GroupFlowLayoutPanels[group] = new() GroupFlowLayoutPanels[group] = new()
{ {
@@ -69,20 +66,108 @@ internal partial class ProcessorCollectionControl : UserControl
ItemCheckBoxes[item.Name] = new() ItemCheckBoxes[item.Name] = new()
{ {
Text = item.Description, Text = item.Description,
Tag = item,
AutoSize = true, AutoSize = true,
// Dock = DockStyle.Fill, // Dock = DockStyle.Fill,
// MaximumSize = new Size(Width, 20), // MaximumSize = new Size(Width, 20),
// Width = 200, // Width = 200,
Checked = true, Checked = item.Active,
CheckAlign = ContentAlignment.TopLeft, CheckAlign = ContentAlignment.TopLeft,
}; };
ItemCheckBoxes[item.Name].CheckedChanged += ItemCheckBox_CheckedChanged;
GroupFlowLayoutPanels[item.Group].Controls.Add(ItemCheckBoxes[item.Name]); GroupFlowLayoutPanels[item.Group].Controls.Add(ItemCheckBoxes[item.Name]);
} }
UpdateContent();
} }
private void GroupCheckBox_CheckedChanged(object? sender, EventArgs e) private void UpdateContent()
{ {
// Dictionary<ProcessorGroupEnum, CheckState> checkStates = [];
List<string> checkedItems = [];
foreach ((string key, CheckBox checkBox) in ItemCheckBoxes)
{
ProcessorCollectionItem item = Collection![key];
if (checkStates.TryGetValue(item.Group, out CheckState state))
{
/*
* | Checked | Unchecked | Indeterminate
* -------+---------------+---------------+---------------
* true | Checked | Indeterminate | Indeterminate
* false | Indeterminate | Unchecked | Indeterminate
*/
checkStates[item.Group] =
checkBox.Checked && state == CheckState.Checked ? CheckState.Checked
: !checkBox.Checked && state == CheckState.Unchecked ? CheckState.Unchecked
: CheckState.Indeterminate;
}
else
{
checkStates[item.Group] = checkBox.Checked
? CheckState.Checked
: CheckState.Unchecked;
}
if (checkBox.Checked)
{
checkedItems.Add(key);
}
}
foreach ((ProcessorGroupEnum groupEnum, CheckBox checkBox) in GroupCheckBoxes)
{
checkBox.CheckState = checkStates.GetValueOrDefault(
groupEnum,
CheckState.Indeterminate
);
}
Collection.FilterProcessors(checkedItems);
}
private void GroupCheckBox_Click(object? sender, EventArgs e)
{
if (sender is null)
{
return;
}
CheckBox groupCheckBox = (CheckBox)sender;
ProcessorGroupEnum groupEnum = GroupCheckBoxes
.First(pair => pair.Value == groupCheckBox)
.Key;
CheckState nextState = groupCheckBox.CheckState switch
{
CheckState.Unchecked => CheckState.Checked,
CheckState.Checked => CheckState.Unchecked,
CheckState.Indeterminate => CheckState.Checked,
_ => throw new ArgumentOutOfRangeException(
$"Неизвестное состояние {groupCheckBox.CheckState}"
),
};
groupCheckBox.CheckState = nextState;
foreach (
(string _, CheckBox checkBox) in ItemCheckBoxes.Where(pair =>
((ProcessorCollectionItem?)pair.Value.Tag)?.Group == groupEnum
)
)
{
checkBox.Checked = nextState == CheckState.Checked;
}
UpdateContent();
}
private void ItemCheckBox_CheckedChanged(object? sender, EventArgs e)
{
UpdateContent();
} }
} }
+244
View File
@@ -0,0 +1,244 @@
using Microsoft.EntityFrameworkCore.Scaffolding.Metadata;
using Pf2eViewer.DatabaseViewer;
namespace Pf2eViewer;
partial class EntityViewForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
RootSplitContainer = new SplitContainer();
InnerSplitContainer = new SplitContainer();
TypesListBox = new ListBox();
ResultSplitContainer = new SplitContainer();
DescriptionTextBox = new TextBox();
DatabaseViewer = new DatabaseViewerControl();
EntityTreeViewerControl = new Pf2eViewer.EntityTreeViewer.EntityTreeViewerControl();
MainMenuStrip = new MenuStrip();
FileToolStripMenuItem = new ToolStripMenuItem();
ExitToolStripMenuItem = new ToolStripMenuItem();
WindowsToolStripMenuItem = new ToolStripMenuItem();
ImportSettingsWindowsToolStripMenuItem = new ToolStripMenuItem();
EntityViewWindowToolStripMenuItem = new ToolStripMenuItem();
((System.ComponentModel.ISupportInitialize)RootSplitContainer).BeginInit();
RootSplitContainer.Panel1.SuspendLayout();
RootSplitContainer.Panel2.SuspendLayout();
RootSplitContainer.SuspendLayout();
((System.ComponentModel.ISupportInitialize)InnerSplitContainer).BeginInit();
InnerSplitContainer.Panel1.SuspendLayout();
InnerSplitContainer.SuspendLayout();
((System.ComponentModel.ISupportInitialize)ResultSplitContainer).BeginInit();
ResultSplitContainer.Panel2.SuspendLayout();
ResultSplitContainer.SuspendLayout();
MainMenuStrip.SuspendLayout();
SuspendLayout();
//
// RootSplitContainer
//
RootSplitContainer.Dock = DockStyle.Fill;
RootSplitContainer.Location = new Point(0, 24);
RootSplitContainer.Name = "RootSplitContainer";
//
// RootSplitContainer.Panel1
//
RootSplitContainer.Panel1.Controls.Add(InnerSplitContainer);
RootSplitContainer.Panel1MinSize = 200;
//
// RootSplitContainer.Panel2
//
RootSplitContainer.Panel2.Controls.Add(ResultSplitContainer);
RootSplitContainer.Panel2MinSize = 400;
RootSplitContainer.Size = new Size(1384, 837);
RootSplitContainer.SplitterDistance = 322;
RootSplitContainer.TabIndex = 0;
//
// InnerSplitContainer
//
InnerSplitContainer.Dock = DockStyle.Fill;
InnerSplitContainer.Location = new Point(0, 0);
InnerSplitContainer.Name = "InnerSplitContainer";
InnerSplitContainer.Orientation = Orientation.Horizontal;
//
// InnerSplitContainer.Panel1
//
InnerSplitContainer.Panel1.Controls.Add(TypesListBox);
//
// InnerSplitContainer.Panel2
//
InnerSplitContainer.Panel2.Controls.Add(DatabaseViewer);
InnerSplitContainer.Size = new Size(322, 861);
InnerSplitContainer.SplitterDistance = 126;
InnerSplitContainer.TabIndex = 1;
//
// TypesListBox
//
TypesListBox.Dock = DockStyle.Fill;
TypesListBox.FormattingEnabled = true;
TypesListBox.Location = new Point(0, 0);
TypesListBox.Name = "TypesListBox";
TypesListBox.Size = new Size(322, 122);
TypesListBox.TabIndex = 0;
TypesListBox.SelectedIndexChanged += TypesListBox_SelectedIndexChanged;
//
// ResultSplitContainer
//
ResultSplitContainer.Dock = DockStyle.Fill;
ResultSplitContainer.Location = new Point(0, 0);
ResultSplitContainer.Name = "ResultSplitContainer";
//
// ResultSplitContainer.Panel1
//
ResultSplitContainer.Panel1.Controls.Add(EntityTreeViewerControl);
//
// ResultSplitContainer.Panel2
//
ResultSplitContainer.Panel2.Controls.Add(DescriptionTextBox);
ResultSplitContainer.Size = new Size(1058, 837);
ResultSplitContainer.SplitterDistance = 582;
ResultSplitContainer.TabIndex = 1;
//
// DescriptionTextBox
//
DescriptionTextBox.Dock = DockStyle.Fill;
DescriptionTextBox.Font = new Font("Consolas", 9F);
DescriptionTextBox.Location = new Point(0, 0);
DescriptionTextBox.Multiline = true;
DescriptionTextBox.Name = "DescriptionTextBox";
DescriptionTextBox.ReadOnly = true;
DescriptionTextBox.ScrollBars = ScrollBars.Vertical;
DescriptionTextBox.Size = new Size(472, 837);
DescriptionTextBox.TabIndex = 0;
//
// DatabaseViewer
//
DatabaseViewer.Dock = DockStyle.Fill;
DatabaseViewer.Location = new Point(0, 0);
DatabaseViewer.Name = "DatabaseViewer";
DatabaseViewer.Size = new Size(322, 731);
DatabaseViewer.TabIndex = 1;
//
// EntityTreeViewerControl
//
EntityTreeViewerControl.AutoScroll = true;
EntityTreeViewerControl.ButtonSize = 15;
EntityTreeViewerControl.DepthSize = 15;
EntityTreeViewerControl.Dock = DockStyle.Fill;
EntityTreeViewerControl.Font = new Font("Consolas", 9F);
EntityTreeViewerControl.HoverColor = SystemColors.ControlDark;
EntityTreeViewerControl.ItemHeight = 20;
EntityTreeViewerControl.Location = new Point(0, 0);
EntityTreeViewerControl.Name = "EntityTreeViewerControl";
EntityTreeViewerControl.Size = new Size(582, 861);
EntityTreeViewerControl.TabIndex = 0;
EntityTreeViewerControl.TypeFontSize = 7F;
//
// MainMenuStrip
//
MainMenuStrip.Items.AddRange(new ToolStripItem[] { FileToolStripMenuItem, WindowsToolStripMenuItem });
MainMenuStrip.Location = new Point(0, 0);
MainMenuStrip.Name = "MainMenuStrip";
MainMenuStrip.Size = new Size(1384, 24);
MainMenuStrip.TabIndex = 1;
MainMenuStrip.Text = "menuStrip1";
//
// FileToolStripMenuItem
//
FileToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { ExitToolStripMenuItem });
FileToolStripMenuItem.Name = "FileToolStripMenuItem";
FileToolStripMenuItem.Size = new Size(48, 20);
FileToolStripMenuItem.Text = "Файл";
//
// ExitToolStripMenuItem
//
ExitToolStripMenuItem.Name = "ExitToolStripMenuItem";
ExitToolStripMenuItem.Size = new Size(180, 22);
ExitToolStripMenuItem.Text = "Выход";
ExitToolStripMenuItem.Click += ExitToolStripMenuItem_Click;
//
// WindowsToolStripMenuItem
//
WindowsToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { ImportSettingsWindowsToolStripMenuItem, EntityViewWindowToolStripMenuItem });
WindowsToolStripMenuItem.Name = "WindowsToolStripMenuItem";
WindowsToolStripMenuItem.Size = new Size(47, 20);
WindowsToolStripMenuItem.Text = "Окна";
//
// ImportSettingsWindowsToolStripMenuItem
//
ImportSettingsWindowsToolStripMenuItem.Name = "ImportSettingsWindowsToolStripMenuItem";
ImportSettingsWindowsToolStripMenuItem.Size = new Size(228, 22);
ImportSettingsWindowsToolStripMenuItem.Text = "Настройка импорта данных";
ImportSettingsWindowsToolStripMenuItem.Click += ImportSettingsWindowsToolStripMenuItem_Click;
//
// EntityViewWindowToolStripMenuItem
//
EntityViewWindowToolStripMenuItem.Enabled = false;
EntityViewWindowToolStripMenuItem.Name = "EntityViewWindowToolStripMenuItem";
EntityViewWindowToolStripMenuItem.Size = new Size(228, 22);
EntityViewWindowToolStripMenuItem.Text = "Просмотр сущностей";
//
// EntityViewForm
//
AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1384, 861);
Controls.Add(RootSplitContainer);
Controls.Add(MainMenuStrip);
Name = "EntityViewForm";
Text = "DataBaseViewer";
RootSplitContainer.Panel1.ResumeLayout(false);
RootSplitContainer.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)RootSplitContainer).EndInit();
RootSplitContainer.ResumeLayout(false);
InnerSplitContainer.Panel1.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)InnerSplitContainer).EndInit();
InnerSplitContainer.ResumeLayout(false);
ResultSplitContainer.Panel2.ResumeLayout(false);
ResultSplitContainer.Panel2.PerformLayout();
((System.ComponentModel.ISupportInitialize)ResultSplitContainer).EndInit();
ResultSplitContainer.ResumeLayout(false);
MainMenuStrip.ResumeLayout(false);
MainMenuStrip.PerformLayout();
ResumeLayout(false);
PerformLayout();
}
#endregion
private SplitContainer RootSplitContainer;
private SplitContainer InnerSplitContainer;
private ListBox TypesListBox;
private SplitContainer ResultSplitContainer;
private TextBox DescriptionTextBox;
private EntityTreeViewer.EntityTreeViewerControl EntityTreeViewerControl;
private DatabaseViewerControl DatabaseViewer;
private MenuStrip MainMenuStrip;
private ToolStripMenuItem FileToolStripMenuItem;
private ToolStripMenuItem ExitToolStripMenuItem;
private ToolStripMenuItem WindowsToolStripMenuItem;
private ToolStripMenuItem ImportSettingsWindowsToolStripMenuItem;
private ToolStripMenuItem EntityViewWindowToolStripMenuItem;
}
+99
View File
@@ -0,0 +1,99 @@
using System.ComponentModel;
using Pf2eDataScrapper;
using Pf2eModel;
using Pf2eModel.Model.Enum;
namespace Pf2eViewer;
/// <summary>
/// Основная форма приложения.
/// </summary>
internal partial class EntityViewForm : Form
{
public Pf2eDbContext Context { get; }
/// <summary>
/// Элемент управления для отображения дерева сущностей.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public EntityView? CurrentView
{
get => EntityTreeViewerControl.EntityView;
set
{
EntityTreeViewerControl.EntityView = value;
Application.DoEvents();
}
}
/// <summary>
/// Конструктор класса <see cref="EntityViewForm"/>.
/// </summary>
public EntityViewForm(Pf2eDbContext context)
{
Context = context;
InitializeComponent();
DatabaseViewer.EntityChange += DatabaseViewer_EntityChange;
Reset();
}
/// <summary>
/// Сброс состояния формы.
/// </summary>
public void Reset()
{
TypesListBox.DataSource = Context
.Entities.Select(e => e.Type)
.Distinct()
.OrderBy(t => t.ToString())
.ToList();
if (TypesListBox.Items.Count > 0)
{
TypesListBox.SelectedIndex = 0;
}
}
/// <summary>
/// Обработчик события изменения выбранного элемента в списке типов сущностей.
/// </summary>
/// <param name="sender">Источник события.</param>
/// <param name="_">Данные события.</param>
private void TypesListBox_SelectedIndexChanged(object sender, EventArgs _)
{
if (TypesListBox.SelectedItem is null)
{
return;
}
CurrentView = null;
DatabaseViewer.Type = (EntityType)TypesListBox.SelectedItem;
}
/// <summary>
/// Обработчик события изменения выбранной сущности в дереве сущностей.
/// </summary>
/// <param name="newEntity">Новая выбранная сущность.</param>
private void DatabaseViewer_EntityChange(EntityView newEntity)
{
CurrentView = newEntity;
DescriptionTextBox.Text = newEntity.Entity!.Description;
}
private void ExitToolStripMenuItem_Click(object sender, EventArgs e)
{
Close();
}
private void ImportSettingsWindowsToolStripMenuItem_Click(object sender, EventArgs e)
{
ApplicationStateChange?.Invoke(this, ApplicationStage.InitialSettings);
}
public event ImportApplication.ApplicationStateChangeDelegate? ApplicationStateChange;
}
+126
View File
@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="MainMenuStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<metadata name="$this.TrayHeight" type="System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>51</value>
</metadata>
</root>
+31 -7
View File
@@ -33,6 +33,9 @@ partial class ProgressForm
LocalLabel = new Label(); LocalLabel = new Label();
LocalProgressBar = new ProgressBar(); LocalProgressBar = new ProgressBar();
MessageListView = new ListView(); MessageListView = new ListView();
TimeColumnHeader = new ColumnHeader();
TypeColumnHeader = new ColumnHeader();
MessageColumnHeader = new ColumnHeader();
SuspendLayout(); SuspendLayout();
// //
// ProcessorsProgressBar // ProcessorsProgressBar
@@ -40,7 +43,7 @@ partial class ProgressForm
ProcessorsProgressBar.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; ProcessorsProgressBar.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
ProcessorsProgressBar.Location = new Point(12, 27); ProcessorsProgressBar.Location = new Point(12, 27);
ProcessorsProgressBar.Name = "ProcessorsProgressBar"; ProcessorsProgressBar.Name = "ProcessorsProgressBar";
ProcessorsProgressBar.Size = new Size(439, 23); ProcessorsProgressBar.Size = new Size(537, 23);
ProcessorsProgressBar.TabIndex = 0; ProcessorsProgressBar.TabIndex = 0;
// //
// ProcessorsLabel // ProcessorsLabel
@@ -48,7 +51,7 @@ partial class ProgressForm
ProcessorsLabel.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; ProcessorsLabel.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
ProcessorsLabel.Location = new Point(12, 9); ProcessorsLabel.Location = new Point(12, 9);
ProcessorsLabel.Name = "ProcessorsLabel"; ProcessorsLabel.Name = "ProcessorsLabel";
ProcessorsLabel.Size = new Size(439, 15); ProcessorsLabel.Size = new Size(537, 15);
ProcessorsLabel.TabIndex = 1; ProcessorsLabel.TabIndex = 1;
ProcessorsLabel.Text = "ProcessorsLabel"; ProcessorsLabel.Text = "ProcessorsLabel";
// //
@@ -57,7 +60,7 @@ partial class ProgressForm
LocalLabel.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; LocalLabel.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
LocalLabel.Location = new Point(12, 59); LocalLabel.Location = new Point(12, 59);
LocalLabel.Name = "LocalLabel"; LocalLabel.Name = "LocalLabel";
LocalLabel.Size = new Size(439, 15); LocalLabel.Size = new Size(537, 15);
LocalLabel.TabIndex = 2; LocalLabel.TabIndex = 2;
LocalLabel.Text = "LocalLabel"; LocalLabel.Text = "LocalLabel";
// //
@@ -66,24 +69,42 @@ partial class ProgressForm
LocalProgressBar.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; LocalProgressBar.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
LocalProgressBar.Location = new Point(12, 77); LocalProgressBar.Location = new Point(12, 77);
LocalProgressBar.Name = "LocalProgressBar"; LocalProgressBar.Name = "LocalProgressBar";
LocalProgressBar.Size = new Size(439, 23); LocalProgressBar.Size = new Size(537, 23);
LocalProgressBar.TabIndex = 3; LocalProgressBar.TabIndex = 3;
// //
// MessageListView // MessageListView
// //
MessageListView.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; MessageListView.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
MessageListView.Columns.AddRange(new ColumnHeader[] { TimeColumnHeader, TypeColumnHeader, MessageColumnHeader });
MessageListView.GridLines = true;
MessageListView.HeaderStyle = ColumnHeaderStyle.Nonclickable;
MessageListView.Location = new Point(12, 114); MessageListView.Location = new Point(12, 114);
MessageListView.Name = "MessageListView"; MessageListView.Name = "MessageListView";
MessageListView.Size = new Size(439, 316); MessageListView.Size = new Size(537, 450);
MessageListView.TabIndex = 4; MessageListView.TabIndex = 4;
MessageListView.UseCompatibleStateImageBehavior = false; MessageListView.UseCompatibleStateImageBehavior = false;
MessageListView.View = View.List; MessageListView.View = View.Details;
//
// TimeColumnHeader
//
TimeColumnHeader.Text = "Время";
TimeColumnHeader.Width = 80;
//
// TypeColumnHeader
//
TypeColumnHeader.Text = "Тип";
TypeColumnHeader.Width = 80;
//
// MessageColumnHeader
//
MessageColumnHeader.Text = "Сообщение";
MessageColumnHeader.Width = 2000;
// //
// ProgressForm // ProgressForm
// //
AutoScaleDimensions = new SizeF(7F, 15F); AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font; AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(463, 442); ClientSize = new Size(561, 576);
Controls.Add(MessageListView); Controls.Add(MessageListView);
Controls.Add(LocalProgressBar); Controls.Add(LocalProgressBar);
Controls.Add(LocalLabel); Controls.Add(LocalLabel);
@@ -101,4 +122,7 @@ partial class ProgressForm
private Label LocalLabel; private Label LocalLabel;
private ProgressBar LocalProgressBar; private ProgressBar LocalProgressBar;
private ListView MessageListView; private ListView MessageListView;
private ColumnHeader TimeColumnHeader;
private ColumnHeader TypeColumnHeader;
private ColumnHeader MessageColumnHeader;
} }
+25 -3
View File
@@ -15,6 +15,14 @@ internal partial class ProgressForm : Form
Progress.ProgressUpdate += Progress_ProgressUpdate; Progress.ProgressUpdate += Progress_ProgressUpdate;
Progress.ProgressMessage += Progress_ProgressMessage; Progress.ProgressMessage += Progress_ProgressMessage;
MessageListView
.GetType()
.GetProperty(
"DoubleBuffered",
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic
)!
.SetValue(MessageListView, true, null);
} }
private void Progress_ProgressUpdate(ProcessProgress sender) private void Progress_ProgressUpdate(ProcessProgress sender)
@@ -36,15 +44,29 @@ internal partial class ProgressForm : Form
Application.DoEvents(); Application.DoEvents();
} }
private void Progress_ProgressMessage(ProcessProgress sender, string message) private void Progress_ProgressMessage(ProcessProgress sender, LogLevel level, string message)
{ {
if (InvokeRequired) if (InvokeRequired)
{ {
Invoke(Progress_ProgressMessage, sender, message); Invoke(Progress_ProgressMessage, sender, level, message);
return; return;
} }
MessageListView.Items.Add(message); ListViewItem item = new([DateTime.Now.ToString("HH:mm:ss.fff"), level.ToString(), message])
{
BackColor = level switch
{
LogLevel.Important => Color.Salmon,
LogLevel.Warning => Color.Khaki,
LogLevel.Information => Color.White,
LogLevel.Progress => Color.LightGray,
_ => Color.White,
},
};
MessageListView.Items.Add(item);
//MessageListView.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent);
//MessageListView.AutoResizeColumns(ColumnHeaderAutoResizeStyle.HeaderSize);
Application.DoEvents(); Application.DoEvents();
} }
+77 -7
View File
@@ -36,18 +36,26 @@ partial class StartForm
SelectAllButton = new Button(); SelectAllButton = new Button();
DeselectAllButton = new Button(); DeselectAllButton = new Button();
StartButton = new Button(); StartButton = new Button();
MainMenuStrip = new MenuStrip();
FileToolStripMenuItem = new ToolStripMenuItem();
ExitToolStripMenuItem = new ToolStripMenuItem();
WindowsToolStripMenuItem = new ToolStripMenuItem();
ImportSettingsWindowsToolStripMenuItem = new ToolStripMenuItem();
EntityViewWindowToolStripMenuItem = new ToolStripMenuItem();
StartToolStripMenuItem = new ToolStripMenuItem();
((System.ComponentModel.ISupportInitialize)MainSplitContainer).BeginInit(); ((System.ComponentModel.ISupportInitialize)MainSplitContainer).BeginInit();
MainSplitContainer.Panel1.SuspendLayout(); MainSplitContainer.Panel1.SuspendLayout();
MainSplitContainer.Panel2.SuspendLayout(); MainSplitContainer.Panel2.SuspendLayout();
MainSplitContainer.SuspendLayout(); MainSplitContainer.SuspendLayout();
PropertiesGroupBox.SuspendLayout(); PropertiesGroupBox.SuspendLayout();
ProcessorsGroupBox.SuspendLayout(); ProcessorsGroupBox.SuspendLayout();
MainMenuStrip.SuspendLayout();
SuspendLayout(); SuspendLayout();
// //
// MainSplitContainer // MainSplitContainer
// //
MainSplitContainer.Dock = DockStyle.Fill; MainSplitContainer.Dock = DockStyle.Fill;
MainSplitContainer.Location = new Point(0, 0); MainSplitContainer.Location = new Point(0, 24);
MainSplitContainer.Name = "MainSplitContainer"; MainSplitContainer.Name = "MainSplitContainer";
// //
// MainSplitContainer.Panel1 // MainSplitContainer.Panel1
@@ -60,7 +68,7 @@ partial class StartForm
MainSplitContainer.Panel2.Controls.Add(ProcessorsGroupBox); MainSplitContainer.Panel2.Controls.Add(ProcessorsGroupBox);
MainSplitContainer.Panel2.Controls.Add(StartButton); MainSplitContainer.Panel2.Controls.Add(StartButton);
MainSplitContainer.Panel2MinSize = 300; MainSplitContainer.Panel2MinSize = 300;
MainSplitContainer.Size = new Size(931, 677); MainSplitContainer.Size = new Size(931, 653);
MainSplitContainer.SplitterDistance = 300; MainSplitContainer.SplitterDistance = 300;
MainSplitContainer.TabIndex = 0; MainSplitContainer.TabIndex = 0;
// //
@@ -70,7 +78,7 @@ partial class StartForm
PropertiesGroupBox.Controls.Add(SettingsPropertyGrid); PropertiesGroupBox.Controls.Add(SettingsPropertyGrid);
PropertiesGroupBox.Location = new Point(3, 3); PropertiesGroupBox.Location = new Point(3, 3);
PropertiesGroupBox.Name = "PropertiesGroupBox"; PropertiesGroupBox.Name = "PropertiesGroupBox";
PropertiesGroupBox.Size = new Size(294, 671); PropertiesGroupBox.Size = new Size(294, 647);
PropertiesGroupBox.TabIndex = 0; PropertiesGroupBox.TabIndex = 0;
PropertiesGroupBox.TabStop = false; PropertiesGroupBox.TabStop = false;
PropertiesGroupBox.Text = "Свойства"; PropertiesGroupBox.Text = "Свойства";
@@ -81,7 +89,7 @@ partial class StartForm
SettingsPropertyGrid.Dock = DockStyle.Fill; SettingsPropertyGrid.Dock = DockStyle.Fill;
SettingsPropertyGrid.Location = new Point(3, 19); SettingsPropertyGrid.Location = new Point(3, 19);
SettingsPropertyGrid.Name = "SettingsPropertyGrid"; SettingsPropertyGrid.Name = "SettingsPropertyGrid";
SettingsPropertyGrid.Size = new Size(288, 649); SettingsPropertyGrid.Size = new Size(288, 625);
SettingsPropertyGrid.TabIndex = 0; SettingsPropertyGrid.TabIndex = 0;
// //
// ProcessorsGroupBox // ProcessorsGroupBox
@@ -92,7 +100,7 @@ partial class StartForm
ProcessorsGroupBox.Controls.Add(DeselectAllButton); ProcessorsGroupBox.Controls.Add(DeselectAllButton);
ProcessorsGroupBox.Location = new Point(3, 3); ProcessorsGroupBox.Location = new Point(3, 3);
ProcessorsGroupBox.Name = "ProcessorsGroupBox"; ProcessorsGroupBox.Name = "ProcessorsGroupBox";
ProcessorsGroupBox.Size = new Size(621, 602); ProcessorsGroupBox.Size = new Size(621, 578);
ProcessorsGroupBox.TabIndex = 1; ProcessorsGroupBox.TabIndex = 1;
ProcessorsGroupBox.TabStop = false; ProcessorsGroupBox.TabStop = false;
ProcessorsGroupBox.Text = "Обработчики"; ProcessorsGroupBox.Text = "Обработчики";
@@ -102,7 +110,7 @@ partial class StartForm
ProcessorCollectionControl.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; ProcessorCollectionControl.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
ProcessorCollectionControl.Location = new Point(6, 48); ProcessorCollectionControl.Location = new Point(6, 48);
ProcessorCollectionControl.Name = "ProcessorCollectionControl"; ProcessorCollectionControl.Name = "ProcessorCollectionControl";
ProcessorCollectionControl.Size = new Size(609, 548); ProcessorCollectionControl.Size = new Size(609, 524);
ProcessorCollectionControl.TabIndex = 2; ProcessorCollectionControl.TabIndex = 2;
// //
// SelectAllButton // SelectAllButton
@@ -127,7 +135,7 @@ partial class StartForm
// //
StartButton.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; StartButton.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
StartButton.Font = new Font("Segoe UI", 18F, FontStyle.Bold); StartButton.Font = new Font("Segoe UI", 18F, FontStyle.Bold);
StartButton.Location = new Point(3, 611); StartButton.Location = new Point(3, 587);
StartButton.Name = "StartButton"; StartButton.Name = "StartButton";
StartButton.Size = new Size(621, 63); StartButton.Size = new Size(621, 63);
StartButton.TabIndex = 0; StartButton.TabIndex = 0;
@@ -135,12 +143,64 @@ partial class StartForm
StartButton.UseVisualStyleBackColor = true; StartButton.UseVisualStyleBackColor = true;
StartButton.Click += StartButton_Click; StartButton.Click += StartButton_Click;
// //
// MainMenuStrip
//
MainMenuStrip.Items.AddRange(new ToolStripItem[] { FileToolStripMenuItem, WindowsToolStripMenuItem, StartToolStripMenuItem });
MainMenuStrip.Location = new Point(0, 0);
MainMenuStrip.Name = "MainMenuStrip";
MainMenuStrip.Size = new Size(931, 24);
MainMenuStrip.TabIndex = 1;
MainMenuStrip.Text = "menuStrip1";
//
// FileToolStripMenuItem
//
FileToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { ExitToolStripMenuItem });
FileToolStripMenuItem.Name = "FileToolStripMenuItem";
FileToolStripMenuItem.Size = new Size(48, 20);
FileToolStripMenuItem.Text = "Файл";
//
// ExitToolStripMenuItem
//
ExitToolStripMenuItem.Name = "ExitToolStripMenuItem";
ExitToolStripMenuItem.Size = new Size(109, 22);
ExitToolStripMenuItem.Text = "Выход";
ExitToolStripMenuItem.Click += ExitToolStripMenuItem_Click;
//
// WindowsToolStripMenuItem
//
WindowsToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { ImportSettingsWindowsToolStripMenuItem, EntityViewWindowToolStripMenuItem });
WindowsToolStripMenuItem.Name = "WindowsToolStripMenuItem";
WindowsToolStripMenuItem.Size = new Size(47, 20);
WindowsToolStripMenuItem.Text = "Окна";
//
// ImportSettingsWindowsToolStripMenuItem
//
ImportSettingsWindowsToolStripMenuItem.Enabled = false;
ImportSettingsWindowsToolStripMenuItem.Name = "ImportSettingsWindowsToolStripMenuItem";
ImportSettingsWindowsToolStripMenuItem.Size = new Size(228, 22);
ImportSettingsWindowsToolStripMenuItem.Text = "Настройка импорта данных";
//
// EntityViewWindowToolStripMenuItem
//
EntityViewWindowToolStripMenuItem.Name = "EntityViewWindowToolStripMenuItem";
EntityViewWindowToolStripMenuItem.Size = new Size(228, 22);
EntityViewWindowToolStripMenuItem.Text = "Просмотр сущностей";
EntityViewWindowToolStripMenuItem.Click += EntityViewWindowToolStripMenuItem_Click;
//
// StartToolStripMenuItem
//
StartToolStripMenuItem.Name = "StartToolStripMenuItem";
StartToolStripMenuItem.Size = new Size(108, 20);
StartToolStripMenuItem.Text = "Запуск импорта";
StartToolStripMenuItem.Click += StartToolStripMenuItem_Click;
//
// StartForm // StartForm
// //
AutoScaleDimensions = new SizeF(7F, 15F); AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font; AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(931, 677); ClientSize = new Size(931, 677);
Controls.Add(MainSplitContainer); Controls.Add(MainSplitContainer);
Controls.Add(MainMenuStrip);
Name = "StartForm"; Name = "StartForm";
Text = "Настройки импорта данных"; Text = "Настройки импорта данных";
Load += StartForm_Load; Load += StartForm_Load;
@@ -150,7 +210,10 @@ partial class StartForm
MainSplitContainer.ResumeLayout(false); MainSplitContainer.ResumeLayout(false);
PropertiesGroupBox.ResumeLayout(false); PropertiesGroupBox.ResumeLayout(false);
ProcessorsGroupBox.ResumeLayout(false); ProcessorsGroupBox.ResumeLayout(false);
MainMenuStrip.ResumeLayout(false);
MainMenuStrip.PerformLayout();
ResumeLayout(false); ResumeLayout(false);
PerformLayout();
} }
#endregion #endregion
@@ -163,4 +226,11 @@ partial class StartForm
private Button SelectAllButton; private Button SelectAllButton;
private Button DeselectAllButton; private Button DeselectAllButton;
private Controls.ProcessorCollectionControl ProcessorCollectionControl; private Controls.ProcessorCollectionControl ProcessorCollectionControl;
private MenuStrip MainMenuStrip;
private ToolStripMenuItem FileToolStripMenuItem;
private ToolStripMenuItem ExitToolStripMenuItem;
private ToolStripMenuItem WindowsToolStripMenuItem;
private ToolStripMenuItem ImportSettingsWindowsToolStripMenuItem;
private ToolStripMenuItem EntityViewWindowToolStripMenuItem;
private ToolStripMenuItem StartToolStripMenuItem;
} }
+27 -7
View File
@@ -1,16 +1,16 @@
using System.ComponentModel; using Pf2eDataScrapper.App;
using System.Reflection;
using Pf2eDataScrapper.App;
namespace Pf2eDataScrapper.Forms; namespace Pf2eDataScrapper.Forms;
internal partial class StartForm : Form internal partial class StartForm : Form
{ {
private ProcessorCollection ProcessorCollection { get; } private ProcessorCollection ProcessorCollection { get; }
public ApplicationSettings Settings { get; }
public StartForm(ProcessorCollection processorCollection) public StartForm(ProcessorCollection processorCollection, ApplicationSettings settings)
{ {
ProcessorCollection = processorCollection; ProcessorCollection = processorCollection;
Settings = settings;
InitializeComponent(); InitializeComponent();
} }
@@ -18,6 +18,8 @@ internal partial class StartForm : Form
private void StartForm_Load(object sender, EventArgs e) private void StartForm_Load(object sender, EventArgs e)
{ {
ProcessorCollectionControl.Collection = ProcessorCollection; ProcessorCollectionControl.Collection = ProcessorCollection;
SettingsPropertyGrid.SelectedObject = Settings;
} }
/* /*
@@ -76,12 +78,30 @@ internal partial class StartForm : Form
private void StartButton_Click(object sender, EventArgs e) private void StartButton_Click(object sender, EventArgs e)
{ {
StartButtonPressed?.Invoke(); ApplicationStateChange?.Invoke(this, ApplicationStage.ImportProcess);
} }
public delegate void StartButtonPressedDelegate(); #endregion
public event StartButtonPressedDelegate? StartButtonPressed; #region "Основное меню"
private void ExitToolStripMenuItem_Click(object sender, EventArgs e)
{
Close();
}
private void EntityViewWindowToolStripMenuItem_Click(object sender, EventArgs e)
{
ApplicationStateChange?.Invoke(this, ApplicationStage.Viewer);
}
private void StartToolStripMenuItem_Click(object sender, EventArgs e)
{
ApplicationStateChange?.Invoke(this, ApplicationStage.ImportProcess);
}
public event ImportApplication.ApplicationStateChangeDelegate? ApplicationStateChange;
#endregion #endregion
} }
+3
View File
@@ -117,4 +117,7 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<metadata name="MainMenuStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
</root> </root>
+1 -1
View File
@@ -29,7 +29,7 @@ public class DatabaseHelper
/// </summary> /// </summary>
public int NextEntityId => ++_id; public int NextEntityId => ++_id;
private DatabaseHelper(Pf2eDbContext context, SlugHelper slugHelper) public DatabaseHelper(Pf2eDbContext context, SlugHelper slugHelper)
{ {
Context = context; Context = context;
Slugger = slugHelper; Slugger = slugHelper;
+138
View File
@@ -0,0 +1,138 @@
using Microsoft.EntityFrameworkCore;
using Pf2eModel;
using Pf2eModel.Model;
using Pf2eModel.Model.Base;
using Pf2eModel.Model.Enum;
using Pf2eModel.Model.Structure;
namespace Pf2eViewer;
/// <summary>
/// Класс, обрабатывающий запросы к сущностям и подключающий необходимые данные.
/// </summary>
public class EntityView
{
/// <summary>
/// Корневая сущность.
/// </summary>
public Entity? Entity { get; }
/// <summary>
/// Основная сущность.
/// </summary>
public IEntity? SubEntity { get; }
/// <summary>
/// Конструктор класса, который получает сущность из базы данных и заполняет её данными.
/// </summary>
/// <param name="context">Контекст базы данных.</param>
/// <param name="type">Тип запрашиваемой сущности.</param>
/// <param name="id">Идентификатор запрашиваемой сущности.</param>
public EntityView(Pf2eDbContext context, EntityType type, int id)
{
Getter? getter = _getters.FirstOrDefault(g => g.Type == type);
Entity fullEntity =
context
.Entities.Where(e => e.Id == id && e.Type == type)
.Include(e => e.Sources)
.ThenInclude(s => s.Source)
.Include(e => e.Keywords)
.Include(e => e.Externals)
.ThenInclude(x => x.Target)
.Include(e => e.Traits)
.ThenInclude(t => t.Entity)
.FirstOrDefault() ?? throw new("Entity is null");
IEntity? subEntity =
(getter != null)
? getter.Get(id, context)
: (IEntity?)context.Find(EntityOfTypeAttribute.Resolve(fullEntity), id);
Entity = fullEntity;
SubEntity = subEntity;
}
/// <summary>
/// Словарь, содержащий методы получения сущностей из базы данных.
/// </summary>
private readonly ICollection<Getter> _getters =
[
new Getter<Ancestry>(
EntityType.Ancestry,
context => context.Ancestries.Include(a => a.AncestryBoosts)
),
new Getter<Archetype>(
EntityType.Archetype,
context =>
context
.Archetypes.Include(a => a.DedicationFeat)
.ThenInclude(f => f.Entity)
.Include(a => a.Feats)
.ThenInclude(f => f.Entity)
),
new Getter<Background>(
EntityType.Background,
context =>
context
.Backgrounds.Include(b => b.BackgroundBoosts)
.Include(b => b.BackgroundSkills)
.ThenInclude(t => t.Training)
.Include(b => b.BackgroundItems)
),
new Getter<Class>(
EntityType.Class,
context =>
context
.Classes.Include(c => c.ClassKeyAbilities)
.Include(c => c.ClassTrainings)
.ThenInclude(t => t.Training)
.Include(c => c.ClassProgressions)
),
new Getter<Feat>(
EntityType.Feat,
context =>
context.Feats.Include(f => f.Archetypes).ThenInclude(a => a.Entity).AsQueryable()
),
new Getter<Spell>(
EntityType.Spell,
context => context.Spells.Include(s => s.SpellAreas).Include(s => s.SpellDefenses)
),
new Getter<Ritual>(
EntityType.Ritual,
context =>
context
.Rituals.Include(r => r.Area)
.Include(r => r.Checks)
.ThenInclude(c => c.Training)
.AsQueryable()
),
];
/// <summary>
/// Абстрактный класс, который определяет метод получения сущности из базы данных.
/// </summary>
/// <param name="type">Связанный тип сущности.</param>
private abstract class Getter(EntityType type)
{
public readonly EntityType Type = type;
public abstract IEntity? Get(int id, Pf2eDbContext context);
}
/// <summary>
/// Класс, определяющий метод получения сущности из базы данных.
/// </summary>
/// <typeparam name="T">Тип данных основной сущности.</typeparam>
/// <param name="type">Связанный тип сущности.</param>
/// <param name="method">Метод получения сущности из базы данных.</param>
private class Getter<T>(EntityType type, Func<Pf2eDbContext, IQueryable<T>> method)
: Getter(type)
where T : IEntity
{
public override IEntity? Get(int id, Pf2eDbContext context)
{
return method.Invoke(context).FirstOrDefault(c => c.Id == id);
}
}
}
+22
View File
@@ -0,0 +1,22 @@
namespace Pf2eDataScrapper.Helpers;
internal abstract class AbstractLogger
{
public abstract void Message(LogLevel level, string message);
public void Important(string message) => Message(LogLevel.Important, message);
public void Warning(string message) => Message(LogLevel.Warning, message);
public void Information(string message) => Message(LogLevel.Information, message);
public void Progress(string message) => Message(LogLevel.Progress, message);
}
internal enum LogLevel
{
Important,
Warning,
Information,
Progress,
}
+8 -4
View File
@@ -2,7 +2,7 @@
namespace Pf2eDataScrapper.Helpers; namespace Pf2eDataScrapper.Helpers;
internal class ProcessProgress internal class ProcessProgress : AbstractLogger
{ {
/// <summary> /// <summary>
/// Прогресс обработки обработчиков <see cref="IProcessor"/>. /// Прогресс обработки обработчиков <see cref="IProcessor"/>.
@@ -31,15 +31,19 @@ internal class ProcessProgress
ProgressUpdate?.Invoke(this); ProgressUpdate?.Invoke(this);
} }
public void Message(string message) public override void Message(LogLevel level, string message)
{ {
ProgressMessage?.Invoke(this, message); ProgressMessage?.Invoke(this, level, message);
} }
public delegate void ProgressUpdateDelegate(ProcessProgress sender); public delegate void ProgressUpdateDelegate(ProcessProgress sender);
public event ProgressUpdateDelegate? ProgressUpdate; public event ProgressUpdateDelegate? ProgressUpdate;
public delegate void ProgressMessageDelegate(ProcessProgress sender, string message); public delegate void ProgressMessageDelegate(
ProcessProgress sender,
LogLevel level,
string message
);
public event ProgressMessageDelegate? ProgressMessage; public event ProgressMessageDelegate? ProgressMessage;
/// <summary> /// <summary>
+44 -15
View File
@@ -4,6 +4,7 @@ using Pf2eDataScrapper.Forms;
using Pf2eDataScrapper.Helpers; using Pf2eDataScrapper.Helpers;
using Pf2eDataScrapper.Processors; using Pf2eDataScrapper.Processors;
using Pf2eModel; using Pf2eModel;
using Pf2eViewer;
using Slugify; using Slugify;
namespace Pf2eDataScrapper; namespace Pf2eDataScrapper;
@@ -50,10 +51,26 @@ internal sealed class ImportApplication : ApplicationContext
private ProgressForm? _progressForm; private ProgressForm? _progressForm;
private EntityViewForm? _entityViewForm;
#endregion #endregion
#region "Жизненный цикл" #region "Жизненный цикл"
private Form? CurrentForm { get; set; } = null;
public delegate void ApplicationStateChangeDelegate(object sender, ApplicationStage nextStage);
public void ApplicationStateChange(object sender, ApplicationStage nextStage)
{
if (sender != CurrentForm)
{
return;
}
CurrentStage = nextStage;
}
private void StartStage(ApplicationStage stage) private void StartStage(ApplicationStage stage)
{ {
switch (stage) switch (stage)
@@ -66,15 +83,14 @@ internal sealed class ImportApplication : ApplicationContext
case ApplicationStage.InitialSettings: case ApplicationStage.InitialSettings:
_startForm = ActivatorUtilities.CreateInstance<StartForm>(ServiceProvider); _startForm = ActivatorUtilities.CreateInstance<StartForm>(ServiceProvider);
_startForm.StartPosition = FormStartPosition.CenterParent; _startForm.StartPosition = FormStartPosition.CenterParent;
_startForm.StartButtonPressed += () => _startForm.ApplicationStateChange += ApplicationStateChange;
{
CurrentStage = ApplicationStage.ProcessorsWorking;
};
_startForm.FormClosing += HandleFormClosing; _startForm.FormClosing += HandleFormClosing;
_startForm.Show(); _startForm.Show();
CurrentForm = _startForm;
break; break;
case ApplicationStage.ProcessorsWorking: case ApplicationStage.ImportProcess:
ProcessorExecutor executor = ActivatorUtilities.CreateInstance<ProcessorExecutor>( ProcessorExecutor executor = ActivatorUtilities.CreateInstance<ProcessorExecutor>(
ServiceProvider ServiceProvider
); );
@@ -84,10 +100,21 @@ internal sealed class ImportApplication : ApplicationContext
_progressForm.FormClosing += HandleFormClosing; _progressForm.FormClosing += HandleFormClosing;
_progressForm.Show(); _progressForm.Show();
CurrentForm = _progressForm;
executor.Start(); executor.Start();
break; break;
case ApplicationStage.Viewer: case ApplicationStage.Viewer:
_entityViewForm = ActivatorUtilities.CreateInstance<EntityViewForm>(
ServiceProvider
);
_entityViewForm.StartPosition = FormStartPosition.CenterParent;
_entityViewForm.ApplicationStateChange += ApplicationStateChange;
_entityViewForm.FormClosing += HandleFormClosing;
_entityViewForm.Show();
CurrentForm = _entityViewForm;
break; break;
default: default:
@@ -99,6 +126,7 @@ internal sealed class ImportApplication : ApplicationContext
private void CloseStage(ApplicationStage stage) private void CloseStage(ApplicationStage stage)
{ {
CurrentForm = null;
switch (stage) switch (stage)
{ {
case ApplicationStage.BootUp: case ApplicationStage.BootUp:
@@ -111,10 +139,13 @@ internal sealed class ImportApplication : ApplicationContext
_startForm = null; _startForm = null;
break; break;
case ApplicationStage.ProcessorsWorking: case ApplicationStage.ImportProcess:
break; break;
case ApplicationStage.Viewer: case ApplicationStage.Viewer:
_entityViewForm!.Hide();
_entityViewForm!.Close();
_entityViewForm = null;
break; break;
default: default:
@@ -126,16 +157,13 @@ internal sealed class ImportApplication : ApplicationContext
private void HandleFormClosing(object? sender, FormClosingEventArgs e) private void HandleFormClosing(object? sender, FormClosingEventArgs e)
{ {
switch (sender) if (sender != CurrentForm)
{ {
case StartForm: e.Cancel = true;
e.Cancel = CurrentStage != ApplicationStage.InitialSettings; return;
if (!e.Cancel)
{
ExitThread();
}
return;
} }
ExitThread();
} }
#endregion #endregion
@@ -177,6 +205,7 @@ internal enum ApplicationStage
{ {
BootUp, BootUp,
InitialSettings, InitialSettings,
ProcessorsWorking, ImportProcess,
Viewer, Viewer,
Rules,
} }
@@ -33,6 +33,8 @@ internal class AonprdImporter : AbstractImporter
services.AddSingleton<LegacyEntityHandler>(); services.AddSingleton<LegacyEntityHandler>();
services.AddSingleton<RuleTocCollection>(); services.AddSingleton<RuleTocCollection>();
services.AddTransient<StringHelper>(); services.AddTransient<StringHelper>();
AonprdProcessorInputExtension.RegisterServices(services);
} }
#endregion #endregion
@@ -43,11 +43,11 @@ internal class AonprdRepository(AonprdImporter importer) : AbstractImporterRepos
if (CheckRevision()) if (CheckRevision())
{ {
progress.Local.Complete(); progress.Local.Complete();
progress.Message($"Данные Aonprd актуальны, текущая ревизия {Revision}."); progress.Information($"Данные Aonprd актуальны, текущая ревизия {Revision}.");
return; return;
} }
progress.Message( progress.Information(
$"Данные Aonprd устарели, обновляем {RepositoryDirPath} до ревизии {Revision}..." $"Данные Aonprd устарели, обновляем {RepositoryDirPath} до ревизии {Revision}..."
); );
@@ -1,4 +1,5 @@
using Newtonsoft.Json.Linq; using System.Collections.Concurrent;
using Newtonsoft.Json.Linq;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Compendium; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Compendium;
@@ -9,33 +10,10 @@ internal sealed class AonprdCompendium(AonprdRepository repository) : AbstractCo
{ {
public override AonprdRepository Repository { get; } = repository; public override AonprdRepository Repository { get; } = repository;
/// <summary>
/// Загрузка данных компендиума.
/// </summary>
public void Load()
{
using StreamReader indexFile = new(
Path.Combine(Repository.RepositoryDirPath, "index.json")
);
JObject indexJson = JObject.Parse(indexFile.ReadToEnd());
foreach ((string file, JToken? value) in indexJson)
{
JArray items = (JArray)value!;
foreach (JToken item in items)
{
AonprdCompendiumItem compendiumItem = new(item.ToString(), file);
Items.Add(compendiumItem);
}
}
}
/// <summary> /// <summary>
/// Коллекция всех записей компендиума. /// Коллекция всех записей компендиума.
/// </summary> /// </summary>
public List<AonprdCompendiumItem> Items { get; } = []; public ConcurrentBag<AonprdCompendiumItem> Items { get; } = [];
/// <summary> /// <summary>
/// Возвращает количество элементов в компендиуме. /// Возвращает количество элементов в компендиуме.
@@ -50,20 +28,6 @@ internal sealed class AonprdCompendium(AonprdRepository repository) : AbstractCo
/// <returns>Количество элементов.</returns> /// <returns>Количество элементов.</returns>
public override int Count(string type) => Items.Count(i => i.Type == type); public override int Count(string type) => Items.Count(i => i.Type == type);
/// <summary>
/// Возвращает список всех файлов.
/// </summary>
/// <returns>Список искомых файлов.</returns>
private IEnumerable<string> Files() => Items.Select(t => t.File).Distinct();
/// <summary>
/// Возвращает список файлов, в которых есть сущности указанного типа данных.
/// </summary>
/// <param name="type">Тип данных.</param>
/// <returns>Список искомых файлов.</returns>
private IEnumerable<string> Files(string type) =>
Items.Where(t => t.Type == type).Select(t => t.File).Distinct();
/// <summary> /// <summary>
/// Получение итератора сущностей. Если указан тип <paramref name="type"/> - то только указанного типа. /// Получение итератора сущностей. Если указан тип <paramref name="type"/> - то только указанного типа.
/// </summary> /// </summary>
@@ -71,7 +35,11 @@ internal sealed class AonprdCompendium(AonprdRepository repository) : AbstractCo
/// <returns>Итератор сущностей.</returns> /// <returns>Итератор сущностей.</returns>
public override IEnumerable<Tuple<AbstractCompendiumItem, JObject>> Iterate(string? type = null) public override IEnumerable<Tuple<AbstractCompendiumItem, JObject>> Iterate(string? type = null)
{ {
foreach (string file in type is null ? Files() : Files(type)) IEnumerable<string> files = type is null
? Items.Select(t => t.File).Distinct()
: Items.Where(t => t.Type == type).Select(t => t.File).Distinct();
foreach (string file in files)
{ {
Dictionary<string, AonprdCompendiumItem> fileReferences = Items Dictionary<string, AonprdCompendiumItem> fileReferences = Items
.Where(i => i.File == file) .Where(i => i.File == file)
@@ -16,10 +16,5 @@ internal class AonprdBuilderGlobals
/// <summary> /// <summary>
/// Конструктор класса <see cref="AonprdBuilderGlobals"/>. /// Конструктор класса <see cref="AonprdBuilderGlobals"/>.
/// </summary> /// </summary>
private AonprdBuilderGlobals() { } public AonprdBuilderGlobals() { }
/// <summary>
/// Экземпляр класса <see cref="AonprdBuilderGlobals"/>.
/// </summary>
public static readonly AonprdBuilderGlobals Instance = new();
} }
@@ -1,134 +0,0 @@
using Pf2eModel.Model.Enum;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Helpers;
/// <summary>
/// Класс, помогающий разрешить строковые значения с Архивов Нефиса в перечисления.
/// </summary>
public static class AonprdEnumResolver
{
/// <summary>
/// Коллекция классов, разрешающих строковые значения в перечисления.
/// </summary>
private static readonly ICollection<Solver> Solvers =
[
new Solver<SourceCategory>(
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 },
}
),
new Solver<VisionEnum>(
new()
{
{ "Low-Light VisionEnum", VisionEnum.LowLightVision },
{ "Darkvision", VisionEnum.DarkVision },
}
),
new Solver<AttributeValues>(
new()
{
{ "Consitution", AttributeValues.Constitution },
{ "Other", AttributeValues.Free },
{ "other", AttributeValues.Free },
}
),
new Solver<ActionEnum>(
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 },
}
),
];
/// <summary>
/// Разрешить строковое значение с Архивов Нефиса в перечисление.
/// </summary>
/// <typeparam name="T">Тип искомого перечисления.</typeparam>
/// <param name="value">Обрабатываемое строковоее значение.</param>
/// <returns>Значение искомого перечисления.</returns>
/// <exception cref="EnumResolverEnumUnknownException">
/// Исключение, если для указанного типа перечисления не указан класс, разрешающий строковые значения.
/// </exception>
/// <exception cref="EnumResolverValueUnknownException">
/// Исключение, если указанное значение не получилось преобразовать искомое значение.
/// </exception>
public static T Resolve<T>(string value)
where T : Enum
{
Type type = typeof(T);
Solver? solver = Solvers.FirstOrDefault(s => s.Type == type);
if (solver is null || solver.GetType() != typeof(Solver<T>))
{
throw new EnumResolverEnumUnknownException(type);
}
Solver<T> tSolver = (Solver<T>)solver;
return tSolver.Data.TryGetValue(value, out T? data)
? data
: throw new EnumResolverValueUnknownException(type, value);
}
/// <summary>
/// Класс, разрешающий строковые значения в перечисления.
/// </summary>
private abstract class Solver
{
public abstract Type Type { get; }
}
/// <summary>
/// Абстрактный общий класс, разрешающий строковые значения в перечисления.
/// </summary>
private class Solver<T>(Dictionary<string, T> data) : Solver
where T : Enum
{
public override Type Type { get; } = typeof(T);
public Dictionary<string, T> Data { get; } = data;
}
/// <summary>
/// Исключение, указывающее, что для указанного типа перечисления не указан класс, разрешающий строковые значения.
/// </summary>
/// <param name="t">Тип искомого перечисления.</param>
public class EnumResolverEnumUnknownException(Type t)
: Exception($"Can't resolve type '{t.FullName}': Solver not defined");
/// <summary>
/// Исключение, указывающее, что указанное значение не получилось преобразовать искомое значение.
/// </summary>
/// <param name="t">Тип искомого перечисления.</param>
/// <param name="value">Искомое значение.</param>
public class EnumResolverValueUnknownException(Type t, string value)
: Exception($"Can't resolve type '{t.FullName}': Value '{value}' is not matched");
}
@@ -1,25 +1,15 @@
using Pf2eDataScrapper.Processors; using Microsoft.Extensions.DependencyInjection;
using Pf2eDataScrapper.Processors;
using Pf2eModel.Model.Enum;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Helpers; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Helpers;
internal static class AonprdProcessorInputExtension internal static class AonprdProcessorInputExtension
{ {
public static T Enum<T>(this ProcessorInput input, string value)
where T : Enum
{
try
{
return AonprdEnumResolver.Resolve<T>(value);
}
catch
{
return input.Enum<T>(value);
}
}
/// <summary> /// <summary>
/// Обработать строку, содержащую информацию о количестве действий. /// Обработать строку, содержащую информацию о количестве действий.
/// </summary> /// </summary>
/// <param name="_"></param>
/// <param name="input">Входная строка с данными.</param> /// <param name="input">Входная строка с данными.</param>
/// <returns>Структура, хранящая информацию о количетсве действий.</returns> /// <returns>Структура, хранящая информацию о количетсве действий.</returns>
public static ActionStruct ResolveActionString(this ProcessorInput _, string input) public static ActionStruct ResolveActionString(this ProcessorInput _, string input)
@@ -129,4 +119,78 @@ internal static class AonprdProcessorInputExtension
/// </summary> /// </summary>
public bool TwoRounds { get; set; } 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>();
}
} }
@@ -11,7 +11,7 @@ internal partial class StringHelper
/// <summary> /// <summary>
/// Конструктор класса <see cref="StringHelper"/> /// Конструктор класса <see cref="StringHelper"/>
/// </summary> /// </summary>
private StringHelper() { } public StringHelper() { }
/// <summary> /// <summary>
/// Разбиение строки по нестабильным строковым разделителям (например, <c>", "</c>, <c>" or "</c> и т.д.). /// Разбиение строки по нестабильным строковым разделителям (например, <c>", "</c>, <c>" or "</c> и т.д.).
@@ -67,11 +67,6 @@ internal partial class StringHelper
return match.Captures.Count > 0 ? match.Captures[0].Value : ""; return match.Captures.Count > 0 ? match.Captures[0].Value : "";
} }
/// <summary>
/// Экземпляр класса <see cref="StringHelper"/>.
/// </summary>
public static StringHelper Instance { get; } = new();
/// <summary> /// <summary>
/// Регулярное выражение для извлечения источника из строки с Markdown-ссылкой. /// Регулярное выражение для извлечения источника из строки с Markdown-ссылкой.
/// </summary> /// </summary>
@@ -1,4 +1,6 @@
using System.Security.Cryptography; using System.Collections.Concurrent;
using System.Linq;
using System.Security.Cryptography;
using System.Text; using System.Text;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -11,6 +13,7 @@ using Pf2eDataScrapper.Processors;
using Pf2eModel; using Pf2eModel;
using Pf2eModel.Model; using Pf2eModel.Model;
using Pf2eModel.Model.Base; using Pf2eModel.Model.Base;
using Pf2eModel.Model.Base.Legacy;
using Pf2eModel.Model.Base.Source; using Pf2eModel.Model.Base.Source;
using Pf2eModel.Model.Base.Targets; using Pf2eModel.Model.Base.Targets;
using Pf2eModel.Model.Common.I18n; using Pf2eModel.Model.Common.I18n;
@@ -62,12 +65,7 @@ internal abstract class AbstractEntityProcessor(
/// <inheritdoc/> /// <inheritdoc/>
public override void Process() public override void Process()
{ {
if (Input.GetOptional<JArray>("remaster_id") is not null) ProcessRemaster();
{
LegacyEntityHandler handler = new(this);
LegacyHandlers.Add(handler);
throw new LegacyItemException(handler.LegacyName);
}
AddEntity(); AddEntity();
AddSource(); AddSource();
@@ -163,8 +161,9 @@ internal abstract class AbstractEntityProcessor(
{ {
Entity = Entity, Entity = Entity,
Target = Globals.AonprdTarget, Target = Globals.AonprdTarget,
ExternalId = Input.GetRequired<string>("id"),
Suffix = Input.GetRequired<string>("url"), Suffix = Input.GetRequired<string>("url"),
Md5 = Md5(Input.Input.ToString()), Md5 = Input.Md5(),
}; };
Context.EntityExternals.Add(external); Context.EntityExternals.Add(external);
@@ -199,7 +198,8 @@ internal abstract class AbstractEntityProcessor(
{ {
if (!traits.Any(t => t.Entity.Name.Contains(name))) if (!traits.Any(t => t.Entity.Name.Contains(name)))
{ {
throw new Exception($"Trait '{name}' is not found!!!"); //TODO
//throw new Exception($"Trait '{name}' is not found!!!");
} }
} }
@@ -250,6 +250,40 @@ internal abstract class AbstractEntityProcessor(
} }
} }
protected virtual void ProcessRemaster()
{
JArray? remasterIdArray = Input.GetOptional<JArray>("remaster_id");
if (remasterIdArray is null)
{
return;
}
if (remasterIdArray.Count > 1)
{
throw new("Несколько отметок ремастер-сущности на устаревшие!");
}
string[] remasterIds = remasterIdArray.ToArray().Select(t => t.ToString()).ToArray();
foreach (string remasterId in remasterIds)
{
LegacyEntityHandler? handler = null;
//LegacyHandlers.FirstOrDefault(h =>
// h.RemasterEntities.Contains(remasterId)
//);
if (handler is null)
{
handler = new(this);
LegacyHandlers.Add(handler);
}
else { }
}
throw new LegacyItemException(Input.GetRequired<string>("name"));
}
/// <summary> /// <summary>
/// Получение списка источников сущности. /// Получение списка источников сущности.
/// </summary> /// </summary>
@@ -308,34 +342,5 @@ internal abstract class AbstractEntityProcessor(
return JsonConvert.SerializeObject(data, new JsonSerializerSettings()); return JsonConvert.SerializeObject(data, new JsonSerializerSettings());
} }
/// <summary> protected static readonly ConcurrentBag<LegacyEntityHandler> LegacyHandlers = [];
/// Создание контрольной суммы объекта.
/// </summary>
/// <param name="input">Строковое представление вычисляемого объекта.</param>
/// <param name="isLowercase">Использовать шестнадцатеричные цифры в нижнем регистре.</param>
/// <returns>Контрольная сумма объекта.</returns>
public static string Md5(string input, bool isLowercase = false)
{
using MD5 md5 = MD5.Create();
byte[] byteHash = md5.ComputeHash(Encoding.UTF8.GetBytes(input));
string hash = BitConverter.ToString(byteHash).Replace("-", "");
return isLowercase ? hash.ToLower() : hash;
}
protected static readonly List<LegacyEntityHandler> LegacyHandlers = [];
} }
/// <summary>
/// Исключение, показывающее, что обработка сущности пропускается как устаревшая.
/// </summary>
/// <param name="name">Название сущности.</param>
public class LegacyItemException(string name)
: Exception($"Item '{name}' is linked with newer version, skipping.") { }
/// <summary>
/// Исключение, показывающее, что обработка сущности пропускается.
/// </summary>
/// <param name="name">Название сущности.</param>
/// <param name="reason">Причина пропуска.</param>
public class SkipItemException(string name, string reason)
: Exception($"Item '{name}' is skipped, reason: {reason}.") { }
@@ -1,4 +1,5 @@
using Pf2eDataScrapper.App; using Pf2eDataScrapper.App;
using Pf2eDataScrapper.Importers.Implementations.Aonprd.Helpers;
using Pf2eDataScrapper.Processors; using Pf2eDataScrapper.Processors;
using Pf2eModel; using Pf2eModel;
using Pf2eModel.Model.Base.Targets; using Pf2eModel.Model.Base.Targets;
@@ -11,9 +12,11 @@ namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Common;
Group = ProcessorGroupEnum.AonprdPreparation Group = ProcessorGroupEnum.AonprdPreparation
)] )]
[ProcessorDependencies(["aonprd:repository"])] [ProcessorDependencies(["aonprd:repository"])]
internal class ExternalProcessor(Pf2eDbContext context) : AbstractProcessor internal class ExternalProcessor(Pf2eDbContext context, AonprdBuilderGlobals globals)
: AbstractProcessor
{ {
public Pf2eDbContext Context { get; } = context; public Pf2eDbContext Context { get; } = context;
public AonprdBuilderGlobals Globals { get; } = globals;
public ExternalTarget AonprdTarget = null!; public ExternalTarget AonprdTarget = null!;
@@ -31,7 +34,6 @@ internal class ExternalProcessor(Pf2eDbContext context) : AbstractProcessor
{ {
Context.ExternalTargets.Add(AonprdTarget); Context.ExternalTargets.Add(AonprdTarget);
// TODO Globals.AonprdTarget = AonprdTarget;
// Globals.AonprdTarget = AonprdTarget;
} }
} }
@@ -0,0 +1,125 @@
using Newtonsoft.Json.Linq;
using Pf2eDataScrapper.App;
using Pf2eDataScrapper.Importers.Implementations.Aonprd.DataBuilder.Helpers;
using Pf2eDataScrapper.Importers.Implementations.Aonprd.Helpers;
using Pf2eDataScrapper.Processors;
using Pf2eModel;
using Pf2eModel.Model.Base.Legacy;
using Pf2eModel.Model.Base.Source;
using Pf2eModel.Model.Common.I18n;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Common;
[ProcessorCollection(
"aonprd:legacy",
InnerType = null,
CompendiumType = ProcessorCompendiumEnum.Aonprd,
Description = "Создание ссылок на устаревшие сущности",
Group = ProcessorGroupEnum.AonprdPreparation
)]
[ProcessorDependencies(["aonprd:source", "aonprd:external"])]
internal class LegacyProcessor(
ProcessorInput input,
Pf2eDbContext context,
AonprdBuilderGlobals globals,
StringHelper stringHelper
) : AbstractInputProcessor(input)
{
public Pf2eDbContext Context { get; } = context;
public AonprdBuilderGlobals Globals { get; } = globals;
public StringHelper StringHelper { get; } = stringHelper;
private LegacyEntity LegacyEntity { get; set; } = null!;
public override void Process()
{
JArray? remasterIdArray = Input.GetOptional<JArray>("remaster_id");
if (Input.GetOptional<JArray>("remaster_id") is null)
{
throw new SilentSkipItemException();
}
LegacyEntity = new()
{
Name = { [LocaleType.English] = Input.GetOptional<string>("name", "sidebar-29") },
};
AddSource();
AddExternal();
}
/// <summary>
/// Генерация источников сущности.
/// </summary>
protected virtual void AddSource()
{
foreach (LegacyEntitySource entitySource in GetSources())
{
LegacyEntity.Sources.Add(entitySource);
}
}
/// <summary>
/// Получение списка источников сущности.
/// </summary>
/// <returns></returns>
public List<LegacyEntitySource> GetSources()
{
string[] sourceNames = Input
.GetRequired<JArray>("source")
.Select(token => token.ToString())
.ToArray();
if (sourceNames.Length == 0)
{
throw new(
$"Source for entity with id \"{LegacyEntity.Name[LocaleType.English]}\" "
+ $"(Aonprd id \"{Input.GetRequired<string>("id")}\") is not found"
);
}
List<LegacyEntitySource> entitySources = [];
foreach (string sourceName in sourceNames)
{
Source source =
Context.Sources.FirstOrDefault(s => s.Name == sourceName)
?? throw new($"Source \"{sourceName}\" did not found");
LegacyEntitySource entitySource = new()
{
LegacyEntity = LegacyEntity,
Source = source,
Suffix = StringHelper.ResolveSource(Input.GetRequired<string>("source_markdown")),
IsRemaster = Input.GetOptional<JArray>("legacy_id") is not null,
};
entitySources.Add(entitySource);
}
return entitySources;
}
/// <summary>
/// Генерация внешних ссылок сущности.
/// </summary>
protected virtual void AddExternal()
{
LegacyEntityExternal external = new()
{
LegacyEntity = LegacyEntity,
Target = Globals.AonprdTarget,
ExternalId = Input.GetRequired<string>("id"),
Suffix = Input.GetRequired<string>("url"),
Md5 = Input.Md5(),
};
Context.LegacyEntityExternals.Add(external);
}
public override void Save()
{
Context.LegacyEntities.Add(LegacyEntity);
}
}
@@ -7,12 +7,14 @@ using Pf2eModel.Model.Enum;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Common; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Common;
[Processor( [ProcessorCollection(
"aonprd:skill", "aonprd:skill",
CompendiumType = ProcessorCompendiumEnum.Aonprd,
InnerType = "skill", InnerType = "skill",
Description = "Навыки", Description = "Навыки",
Group = ProcessorGroupEnum.AonprdPreparation Group = ProcessorGroupEnum.AonprdPreparation
)] )]
[ProcessorDependencies(["aonprd:compendium"])]
internal class SkillProcessor(ProcessorInput input, Pf2eDbContext context) internal class SkillProcessor(ProcessorInput input, Pf2eDbContext context)
: AbstractInputProcessor(input) : AbstractInputProcessor(input)
{ {
@@ -6,13 +6,14 @@ using Pf2eModel.Model.Enum;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Common; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Common;
[Processor( [ProcessorCollection(
"aonprd:source", "aonprd:source",
InnerType = "source", InnerType = "source",
CompendiumType = ProcessorCompendiumEnum.Aonprd,
Description = "Источники", Description = "Источники",
Group = ProcessorGroupEnum.AonprdPreparation Group = ProcessorGroupEnum.AonprdPreparation
)] )]
[ProcessorDependencies(["aonprd:repository"])] [ProcessorDependencies(["aonprd:compendium"])]
internal class SourceProcessor(ProcessorInput input, Pf2eDbContext context) internal class SourceProcessor(ProcessorInput input, Pf2eDbContext context)
: AbstractInputProcessor(input) : AbstractInputProcessor(input)
{ {
@@ -0,0 +1,54 @@
using Newtonsoft.Json.Linq;
using Pf2eDataScrapper.App;
using Pf2eDataScrapper.Helpers;
using Pf2eDataScrapper.Importers.Implementations.Aonprd.Compendium;
using Pf2eDataScrapper.Processors;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Preparation;
[Processor(
"aonprd:compendium",
Description = "Загрузка компендиума",
Group = ProcessorGroupEnum.AonprdPreparation
)]
[ProcessorDependencies(["aonprd:repository"])]
internal class CompendiumLoadProcessor(
AonprdRepository repository,
AonprdCompendium compendium,
ProcessProgress progress
) : IProcessor
{
public AonprdRepository Repository { get; } = repository;
public AonprdCompendium Compendium { get; } = compendium;
public ProcessProgress Progress { get; } = progress;
public void Process()
{
using StreamReader indexFile = new(
Path.Combine(Repository.RepositoryDirPath, "index.json")
);
JObject indexJson = JObject.Parse(indexFile.ReadToEnd());
Progress.Local.Reset(indexJson.Count);
foreach ((string file, JToken? value) in indexJson)
{
Progress.Local.Start(file);
JArray items = (JArray)value!;
foreach (JToken item in items)
{
AonprdCompendiumItem compendiumItem = new(item.ToString(), file);
Compendium.Items.Add(compendiumItem);
}
Progress.Local.End();
}
Progress.Local.Complete();
}
public void Save() { }
}
@@ -1,23 +1,18 @@
using Pf2eDataScrapper.App; using Pf2eDataScrapper.App;
using Pf2eDataScrapper.Helpers; using Pf2eDataScrapper.Helpers;
using Pf2eDataScrapper.Importers.Implementations.Aonprd.Compendium;
using Pf2eDataScrapper.Processors; using Pf2eDataScrapper.Processors;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Common; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Preparation;
[Processor( [Processor(
"aonprd:repository", "aonprd:repository",
Description = "Обновление репозитория", Description = "Обновление репозитория",
Group = ProcessorGroupEnum.AonprdPreparation Group = ProcessorGroupEnum.AonprdPreparation
)] )]
internal class RepositoryUpdateProcessor( internal class RepositoryUpdateProcessor(AonprdRepository repository, ProcessProgress progress)
AonprdRepository repository, : IProcessor
AonprdCompendium compendium,
ProcessProgress progress
) : IProcessor
{ {
public AonprdRepository Repository { get; } = repository; public AonprdRepository Repository { get; } = repository;
public AonprdCompendium Compendium { get; } = compendium;
public ProcessProgress Progress { get; } = progress; public ProcessProgress Progress { get; } = progress;
public void Process() public void Process()
@@ -25,8 +20,5 @@ internal class RepositoryUpdateProcessor(
Repository.Update(Progress); Repository.Update(Progress);
} }
public void Save() public void Save() { }
{
Compendium.Load();
}
} }
@@ -3,12 +3,13 @@ using Pf2eDataScrapper.Helpers;
using Pf2eDataScrapper.Importers.Implementations.Aonprd.Compendium; using Pf2eDataScrapper.Importers.Implementations.Aonprd.Compendium;
using Pf2eDataScrapper.SchemaBuilder; using Pf2eDataScrapper.SchemaBuilder;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Common; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Preparation;
[Processor( [Processor(
"aonprd:schema", "aonprd:schema",
Description = "Построение схемы данных", Description = "Построение схемы данных",
Group = ProcessorGroupEnum.AonprdPreparation Group = ProcessorGroupEnum.AonprdPreparation,
Active = false
)] )]
[ProcessorDependencies(["aonprd:repository"])] [ProcessorDependencies(["aonprd:repository"])]
internal class SchemaBuildProcessor( internal class SchemaBuildProcessor(
@@ -12,9 +12,10 @@ using Pf2eModel.Model.Structure;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Subclasses; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Subclasses;
[Processor( [ProcessorCollection(
"aonprd:apparition", "aonprd:apparition",
InnerType = "apparition", InnerType = "apparition",
CompendiumType = ProcessorCompendiumEnum.Aonprd,
Description = "Apparitions", Description = "Apparitions",
Group = ProcessorGroupEnum.AonprdSubclasses Group = ProcessorGroupEnum.AonprdSubclasses
)] )]
@@ -8,9 +8,10 @@ using Pf2eModel.Model.Enum;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Types; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Types;
[Processor( [ProcessorCollection(
"aonprd:action", "aonprd:action",
InnerType = "action", InnerType = "action",
CompendiumType = ProcessorCompendiumEnum.Aonprd,
Description = "Действия", Description = "Действия",
Group = ProcessorGroupEnum.AonprdEntities Group = ProcessorGroupEnum.AonprdEntities
)] )]
@@ -10,9 +10,10 @@ using Pf2eModel.Model.Structure;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Types; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Types;
[Processor( [ProcessorCollection(
"aonprd:ancestry", "aonprd:ancestry",
InnerType = "ancestry", InnerType = "ancestry",
CompendiumType = ProcessorCompendiumEnum.Aonprd,
Description = "Родословные", Description = "Родословные",
Group = ProcessorGroupEnum.AonprdEntities Group = ProcessorGroupEnum.AonprdEntities
)] )]
@@ -9,9 +9,10 @@ using Pf2eModel.Model.Structure;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Types; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Types;
[Processor( [ProcessorCollection(
"aonprd:archetype", "aonprd:archetype",
InnerType = "archetype", InnerType = "archetype",
CompendiumType = ProcessorCompendiumEnum.Aonprd,
Description = "Архетипы", Description = "Архетипы",
Group = ProcessorGroupEnum.AonprdEntities Group = ProcessorGroupEnum.AonprdEntities
)] )]
@@ -11,9 +11,10 @@ using Pf2eModel.Model.Structure.BackgroundRelation;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Types; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Types;
[Processor( [ProcessorCollection(
"aonprd:background", "aonprd:background",
InnerType = "background", InnerType = "background",
CompendiumType = ProcessorCompendiumEnum.Aonprd,
Description = "Предыстории", Description = "Предыстории",
Group = ProcessorGroupEnum.AonprdEntities Group = ProcessorGroupEnum.AonprdEntities
)] )]
@@ -15,9 +15,10 @@ using static System.Text.RegularExpressions.Regex;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Types; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Types;
[Processor( [ProcessorCollection(
"aonprd:class", "aonprd:class",
InnerType = "class", InnerType = "class",
CompendiumType = ProcessorCompendiumEnum.Aonprd,
Description = "Классы", Description = "Классы",
Group = ProcessorGroupEnum.AonprdEntities Group = ProcessorGroupEnum.AonprdEntities
)] )]
@@ -10,9 +10,10 @@ using Pf2eModel.Model.Structure;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Types; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Types;
[Processor( [ProcessorCollection(
"aonprd:feat", "aonprd:feat",
InnerType = "feat", InnerType = "feat",
CompendiumType = ProcessorCompendiumEnum.Aonprd,
Description = "Черты", Description = "Черты",
Group = ProcessorGroupEnum.AonprdEntities Group = ProcessorGroupEnum.AonprdEntities
)] )]
@@ -9,9 +9,10 @@ using Pf2eModel.Model.Structure;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Types; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Types;
[Processor( [ProcessorCollection(
"aonprd:language", "aonprd:language",
InnerType = "language", InnerType = "language",
CompendiumType = ProcessorCompendiumEnum.Aonprd,
Description = "Языки", Description = "Языки",
Group = ProcessorGroupEnum.AonprdEntities Group = ProcessorGroupEnum.AonprdEntities
)] )]
@@ -13,9 +13,10 @@ using Pf2eModel.Model.Structure.RitualRelation;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Types; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Types;
[Processor( [ProcessorCollection(
"aonprd:ritual", "aonprd:ritual",
InnerType = "ritual", InnerType = "ritual",
CompendiumType = ProcessorCompendiumEnum.Aonprd,
Description = "Ритуалы", Description = "Ритуалы",
Group = ProcessorGroupEnum.AonprdEntities Group = ProcessorGroupEnum.AonprdEntities
)] )]
@@ -5,6 +5,7 @@ using Pf2eDataScrapper.Importers.Implementations.Aonprd.DataBuilder.Helpers;
using Pf2eDataScrapper.Importers.Implementations.Aonprd.Helpers; using Pf2eDataScrapper.Importers.Implementations.Aonprd.Helpers;
using Pf2eDataScrapper.Processors; using Pf2eDataScrapper.Processors;
using Pf2eModel; using Pf2eModel;
using Pf2eModel.Model.Base.Legacy;
using Pf2eModel.Model.Base.Source; using Pf2eModel.Model.Base.Source;
using Pf2eModel.Model.Base.Targets; using Pf2eModel.Model.Base.Targets;
using Pf2eModel.Model.Common.I18n; using Pf2eModel.Model.Common.I18n;
@@ -13,10 +14,11 @@ using Rule = Pf2eModel.Model.Structure.Rule;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Types.Rules; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Types.Rules;
[Processor( [ProcessorCollection(
"aonprd:rules", "aonprd:rules",
InnerType = "rules", InnerType = "rules",
Description = "Правила", CompendiumType = ProcessorCompendiumEnum.Aonprd,
Description = "Создание цели внешних ссылок",
Group = ProcessorGroupEnum.AonprdEntities Group = ProcessorGroupEnum.AonprdEntities
)] )]
[ProcessorDependencies(["aonprd:trait", "aonprd:source", "aonprd:rules-toc"])] [ProcessorDependencies(["aonprd:trait", "aonprd:source", "aonprd:rules-toc"])]
@@ -12,9 +12,10 @@ using Pf2eModel.Model.Structure.SpellRelation;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Types; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Types;
[Processor( [ProcessorCollection(
"aonprd:spell", "aonprd:spell",
InnerType = "spell", InnerType = "spell",
CompendiumType = ProcessorCompendiumEnum.Aonprd,
Description = "Заклинания", Description = "Заклинания",
Group = ProcessorGroupEnum.AonprdEntities Group = ProcessorGroupEnum.AonprdEntities
)] )]
@@ -9,13 +9,14 @@ using Pf2eModel.Model.Structure;
namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Types; namespace Pf2eDataScrapper.Importers.Implementations.Aonprd.Processors.Types;
[Processor( [ProcessorCollection(
"aonprd:trait", "aonprd:trait",
InnerType = "trait", InnerType = "trait",
CompendiumType = ProcessorCompendiumEnum.Aonprd,
Description = "Признаки", Description = "Признаки",
Group = ProcessorGroupEnum.AonprdEntities Group = ProcessorGroupEnum.AonprdEntities
)] )]
[ProcessorDependencies(["aonprd:external"])] [ProcessorDependencies(["aonprd:legacy"])]
internal class TraitProcessor( internal class TraitProcessor(
ProcessorInput input, ProcessorInput input,
Pf2eDbContext context, Pf2eDbContext context,
@@ -1,4 +1,5 @@
using Newtonsoft.Json.Linq; using System.Collections.Concurrent;
using Newtonsoft.Json.Linq;
using Pf2eDataScrapper.Importers.Implementations.FoundryEn.Compendium; using Pf2eDataScrapper.Importers.Implementations.FoundryEn.Compendium;
namespace Pf2eDataScrapper.Importers.Implementations.FoundryEn; namespace Pf2eDataScrapper.Importers.Implementations.FoundryEn;
@@ -25,7 +26,7 @@ internal sealed class FoundryEnCompendium(
/// <summary> /// <summary>
/// Коллекция всех записей компендиума. /// Коллекция всех записей компендиума.
/// </summary> /// </summary>
public List<FoundryEnCompendiumItem> Items { get; } = []; public ConcurrentBag<FoundryEnCompendiumItem> Items { get; } = [];
/// <summary> /// <summary>
/// Возвращает количество элементов в компендиуме. /// Возвращает количество элементов в компендиуме.
@@ -7,11 +7,13 @@ using Pf2eModel.Model.Enum;
namespace Pf2eDataScrapper.Importers.Implementations.FoundryEn.Processors.Entities; namespace Pf2eDataScrapper.Importers.Implementations.FoundryEn.Processors.Entities;
[Processor( [ProcessorCollection(
"foundry-en:action", "foundry-en:action",
InnerType = "action", InnerType = "action",
CompendiumType = ProcessorCompendiumEnum.FoundryEn,
Description = "Действия", Description = "Действия",
Group = ProcessorGroupEnum.FoundryEnEntities Group = ProcessorGroupEnum.FoundryEnEntities,
Active = false
)] )]
[ProcessorDependencies(["foundry-en:compendium"])] [ProcessorDependencies(["foundry-en:compendium"])]
internal class ActionProcessor( internal class ActionProcessor(
@@ -8,11 +8,13 @@ using Pf2eModel.Model.Structure;
namespace Pf2eDataScrapper.Importers.Implementations.FoundryEn.Processors.Entities; namespace Pf2eDataScrapper.Importers.Implementations.FoundryEn.Processors.Entities;
[Processor( [ProcessorCollection(
"foundry-en:condition", "foundry-en:condition",
InnerType = "Item.condition", InnerType = "Item.condition",
CompendiumType = ProcessorCompendiumEnum.FoundryEn,
Description = "Состояния", Description = "Состояния",
Group = ProcessorGroupEnum.FoundryEnEntities Group = ProcessorGroupEnum.FoundryEnEntities,
Active = false
)] )]
[ProcessorDependencies(["foundry-en:compendium"])] [ProcessorDependencies(["foundry-en:compendium"])]
internal class ConditionProcessor( internal class ConditionProcessor(
@@ -7,6 +7,7 @@ using Pf2eDataScrapper.Processors;
using Pf2eModel; using Pf2eModel;
using Pf2eModel.Model; using Pf2eModel.Model;
using Pf2eModel.Model.Base; using Pf2eModel.Model.Base;
using Pf2eModel.Model.Base.Legacy;
using Pf2eModel.Model.Base.Source; using Pf2eModel.Model.Base.Source;
using Pf2eModel.Model.Base.Targets; using Pf2eModel.Model.Base.Targets;
using Pf2eModel.Model.Common.I18n; using Pf2eModel.Model.Common.I18n;
@@ -8,7 +8,8 @@ namespace Pf2eDataScrapper.Importers.Implementations.FoundryEn.Processors.Prepar
[Processor( [Processor(
"foundry-en:compendium-folders", "foundry-en:compendium-folders",
Description = "Загрузка каталогов компендиума", Description = "Загрузка каталогов компендиума",
Group = ProcessorGroupEnum.FoundryEnPreparation Group = ProcessorGroupEnum.FoundryEnPreparation,
Active = false
)] )]
[ProcessorDependencies(["foundry-en:repository"])] [ProcessorDependencies(["foundry-en:repository"])]
internal class CompendiumFolderProcessor( internal class CompendiumFolderProcessor(
@@ -9,7 +9,8 @@ namespace Pf2eDataScrapper.Importers.Implementations.FoundryEn.Processors.Prepar
[Processor( [Processor(
"foundry-en:compendium", "foundry-en:compendium",
Description = "Загрузка компендиума", Description = "Загрузка компендиума",
Group = ProcessorGroupEnum.FoundryEnPreparation Group = ProcessorGroupEnum.FoundryEnPreparation,
Active = false
)] )]
[ProcessorDependencies(["foundry-en:compendium-folders"])] [ProcessorDependencies(["foundry-en:compendium-folders"])]
internal class CompendiumItemsProcessor( internal class CompendiumItemsProcessor(
@@ -7,7 +7,8 @@ namespace Pf2eDataScrapper.Importers.Implementations.FoundryEn.Processors.Prepar
[Processor( [Processor(
"foundry-en:repository", "foundry-en:repository",
Description = "Обновление репозитория", Description = "Обновление репозитория",
Group = ProcessorGroupEnum.FoundryEnPreparation Group = ProcessorGroupEnum.FoundryEnPreparation,
Active = false
)] )]
internal class RepositoryUpdateProcessor(FoundryEnRepository repository, ProcessProgress progress) internal class RepositoryUpdateProcessor(FoundryEnRepository repository, ProcessProgress progress)
: IProcessor : IProcessor
@@ -7,7 +7,8 @@ namespace Pf2eDataScrapper.Importers.Implementations.FoundryEn.Processors.Prepar
[Processor( [Processor(
"foundry-en:schema", "foundry-en:schema",
Description = "Построение схемы данных", Description = "Построение схемы данных",
Group = ProcessorGroupEnum.FoundryEnPreparation Group = ProcessorGroupEnum.FoundryEnPreparation,
Active = false
)] )]
[ProcessorDependencies(["foundry-en:compendium"])] [ProcessorDependencies(["foundry-en:compendium"])]
internal class SchemaBuildProcessor( internal class SchemaBuildProcessor(
+20 -4
View File
@@ -6,14 +6,19 @@
<UseWindowsForms>true</UseWindowsForms> <UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Remove="Importers\Implementations\FoundryEn\DataBuilder\**" />
<EmbeddedResource Remove="Importers\Implementations\FoundryEn\DataBuilder\**" />
<None Remove="Importers\Implementations\FoundryEn\DataBuilder\**" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AngleSharp" Version="1.3.0" /> <PackageReference Include="AngleSharp" Version="1.3.0" />
<PackageReference Include="dotenv.net" Version="3.2.1" /> <PackageReference Include="dotenv.net" Version="3.2.1" />
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" /> <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
<PackageReference Include="Goblinfactory.Konsole" Version="6.2.2" /> <PackageReference Include="FontAwesome.Sharp" Version="6.6.0" />
<PackageReference Include="linq2db" Version="5.4.1" /> <PackageReference Include="linq2db" Version="5.4.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
@@ -34,6 +39,17 @@
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Importers\Implementations\FoundryEn\DataBuilder\" /> <Compile Update="Forms\Controls\CoreBindingNavigator.cs">
<SubType>Component</SubType>
</Compile>
<Compile Update="Forms\Controls\DatabaseViewer\DatabaseViewerControl.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Update="Forms\Controls\EntityTreeViewer\EntityTreeViewerControl.cs">
<SubType>UserControl</SubType>
</Compile>
<Compile Update="Forms\EntityViewForm.cs">
<SubType>Form</SubType>
</Compile>
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -4,21 +4,27 @@ using Newtonsoft.Json.Linq;
using Pf2eDataScrapper.App; using Pf2eDataScrapper.App;
using Pf2eDataScrapper.Helpers; using Pf2eDataScrapper.Helpers;
using Pf2eDataScrapper.Importers; using Pf2eDataScrapper.Importers;
using Pf2eDataScrapper.Importers.Implementations.Aonprd;
using Pf2eDataScrapper.Importers.Implementations.Aonprd.Compendium; using Pf2eDataScrapper.Importers.Implementations.Aonprd.Compendium;
using Pf2eDataScrapper.Importers.Implementations.FoundryEn; using Pf2eDataScrapper.Importers.Implementations.FoundryEn;
using Pf2eModel;
namespace Pf2eDataScrapper.Processors; namespace Pf2eDataScrapper.Processors;
internal sealed class ProcessorExecutor( internal sealed class ProcessorExecutor(
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
ProcessorCollection collection, ProcessorCollection collection,
ProcessProgress progress ProcessProgress progress,
Pf2eDbContext context,
AonprdCompendium aonprdCompendium,
FoundryEnCompendium foundryEnCompendium
) )
{ {
public IServiceProvider ServiceProvider { get; } = serviceProvider; public IServiceProvider ServiceProvider { get; } = serviceProvider;
public ProcessorCollection Collection { get; } = collection; public ProcessorCollection Collection { get; } = collection;
public ProcessProgress Progress { get; } = progress; public ProcessProgress Progress { get; } = progress;
public Pf2eDbContext Context { get; } = context;
public AonprdCompendium AonprdCompendium { get; } = aonprdCompendium;
public FoundryEnCompendium FoundryEnCompendium { get; } = foundryEnCompendium;
public void Start() public void Start()
{ {
@@ -36,6 +42,7 @@ internal sealed class ProcessorExecutor(
ProcessorCollectionItem item = Collection.Items[_currentIndex]; ProcessorCollectionItem item = Collection.Items[_currentIndex];
Progress.Processors.Start(item.Description); Progress.Processors.Start(item.Description);
Progress.Progress(Progress.Processors.ToString());
BackgroundWorker worker = new(); BackgroundWorker worker = new();
@@ -43,9 +50,21 @@ internal sealed class ProcessorExecutor(
{ {
foreach (IProcessor processor in IterateProcessors(item)) foreach (IProcessor processor in IterateProcessors(item))
{ {
processor.Process(); try
processor.Save(); {
processor.Process();
processor.Save();
}
catch (SkipItemException exception)
{
Progress.Information(exception.Message);
}
catch (SilentSkipItemException)
{
//
}
} }
Context.SaveChanges();
}; };
worker.RunWorkerCompleted += Continue; worker.RunWorkerCompleted += Continue;
@@ -70,7 +89,7 @@ internal sealed class ProcessorExecutor(
private IEnumerable<IProcessor> IterateProcessors(ProcessorCollectionItem item) private IEnumerable<IProcessor> IterateProcessors(ProcessorCollectionItem item)
{ {
if (!item.IsCollectionProcessor) if (!item.IsCollection)
{ {
Progress.Local.Reset(1); Progress.Local.Reset(1);
Progress.Local.Start(item.Description); Progress.Local.Start(item.Description);
@@ -86,35 +105,33 @@ internal sealed class ProcessorExecutor(
); );
} }
Type importerType = item.Group switch AbstractCompendium compendium = item.Compendium switch
{ {
ProcessorGroupEnum.AonprdPreparation ProcessorCompendiumEnum.Aonprd => AonprdCompendium,
or ProcessorGroupEnum.AonprdEntities ProcessorCompendiumEnum.FoundryEn => FoundryEnCompendium,
or ProcessorGroupEnum.AonprdSubclasses => typeof(AonprdCompendium),
ProcessorGroupEnum.FoundryEnPreparation or ProcessorGroupEnum.FoundryEnEntities =>
typeof(FoundryEnCompendium),
_ => throw new($"Не могу выбрать импортёр для группы {item.Group}"), _ => throw new($"Не могу выбрать импортёр для группы {item.Group}"),
}; };
AonprdCompendium compendium = (AonprdCompendium) Progress.Local.Reset(
ActivatorUtilities.CreateInstance(ServiceProvider, importerType); item.InnerType is null ? compendium.Count() : compendium.Count(item.InnerType)
);
Progress.Local.Reset(compendium.Count(item.InnerType!));
foreach ( foreach (
(AbstractCompendiumItem compendiumItem, JObject json) in compendium.Iterate( (AbstractCompendiumItem compendiumItem, JObject json) in compendium.Iterate(
item.InnerType! item.InnerType
) )
) )
{ {
Progress.Local.Start(compendiumItem.Id); Progress.Local.Start(compendiumItem.Id);
ProcessorInput processorInput = ActivatorUtilities.CreateInstance<ProcessorInput>(
ServiceProvider,
compendiumItem.Id,
json
);
yield return (IProcessor) yield return (IProcessor)
ActivatorUtilities.CreateInstance( ActivatorUtilities.CreateInstance(ServiceProvider, item.Type, processorInput);
ServiceProvider,
item.Type,
new ProcessorInput(compendiumItem.Id, json)
);
Progress.Local.End(); Progress.Local.End();
} }
@@ -122,3 +139,22 @@ internal sealed class ProcessorExecutor(
Progress.Local.Complete(); Progress.Local.Complete();
} }
} }
/// <summary>
/// Исключение, показывающее, что обработка сущности пропускается как устаревшая.
/// </summary>
/// <param name="name">Название сущности.</param>
public class LegacyItemException(string name) : SkipItemException(name, "Устаревший элемент") { }
/// <summary>
/// Исключение, показывающее, что обработка сущности пропускается.
/// </summary>
/// <param name="name">Название сущности.</param>
/// <param name="reason">Причина пропуска.</param>
public class SkipItemException(string name, string reason)
: Exception($"Пропускаю элемент '{name}', указана причина: {reason}.") { }
/// <summary>
/// Исключение, показывающее, что обработка сущности пропускается без вывода сообщения.
/// </summary>
public class SilentSkipItemException : Exception { }
+48 -2
View File
@@ -1,4 +1,6 @@
using Newtonsoft.Json.Linq; using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json.Linq;
using NpgsqlTypes; using NpgsqlTypes;
using Pf2eModel.Model.Common.I18n; using Pf2eModel.Model.Common.I18n;
@@ -9,7 +11,7 @@ namespace Pf2eDataScrapper.Processors;
/// </summary> /// </summary>
/// <param name="name">Название объекта, ассоциированного с вводом.</param> /// <param name="name">Название объекта, ассоциированного с вводом.</param>
/// <param name="input">JSON-объект, который будет использоваться для доступа к данным.</param> /// <param name="input">JSON-объект, который будет использоваться для доступа к данным.</param>
internal class ProcessorInput(string name, JObject input) internal class ProcessorInput(string name, JObject input, IEnumerable<IEnumSolver> enumSolvers)
{ {
/// <summary> /// <summary>
/// Название объекта, ассоциированного с вводом. /// Название объекта, ассоциированного с вводом.
@@ -21,6 +23,11 @@ internal class ProcessorInput(string name, JObject input)
/// </summary> /// </summary>
public JObject Input { get; } = input; public JObject Input { get; } = input;
/// <summary>
/// Интерфейсы, разрешающие значения перечислений.
/// </summary>
public IEnumerable<IEnumSolver> EnumSolvers { get; } = enumSolvers;
/// <summary> /// <summary>
/// Значение из JSON по заданному пути, которое обязательно должно существовать. /// Значение из JSON по заданному пути, которое обязательно должно существовать.
/// </summary> /// </summary>
@@ -141,6 +148,20 @@ internal class ProcessorInput(string name, JObject input)
public virtual T Enum<T>(string value) public virtual T Enum<T>(string value)
where T : Enum where T : Enum
{ {
IEnumerable<EnumSolver<T>> solvers = EnumSolvers
.Where(solver => solver.GetType().IsAssignableTo(typeof(EnumSolver<T>)))
.Cast<EnumSolver<T>>();
foreach (EnumSolver<T> solver in solvers)
{
T? result = solver.SolveEnum(value);
if (result is not null)
{
return result;
}
}
foreach (System.Reflection.FieldInfo field in typeof(T).GetFields()) foreach (System.Reflection.FieldInfo field in typeof(T).GetFields())
{ {
List<string> variants = [field.Name]; List<string> variants = [field.Name];
@@ -162,6 +183,18 @@ internal class ProcessorInput(string name, JObject input)
throw new InvalidEnumStringException(typeof(T), value); throw new InvalidEnumStringException(typeof(T), value);
} }
/// <summary>
/// Создание контрольной суммы объекта.
/// </summary>
/// <param name="isLowercase">Использовать шестнадцатеричные цифры в нижнем регистре.</param>
/// <returns>Контрольная сумма объекта.</returns>
public string Md5(bool isLowercase = false)
{
byte[] byteHash = MD5.HashData(Encoding.UTF8.GetBytes(Input.ToString()));
string hash = Convert.ToHexString(byteHash).Replace("-", "");
return isLowercase ? hash.ToLower() : hash;
}
/// <summary> /// <summary>
/// Исключение при неверном значении для перечисленния. /// Исключение при неверном значении для перечисленния.
/// </summary> /// </summary>
@@ -170,3 +203,16 @@ internal class ProcessorInput(string name, JObject input)
public class InvalidEnumStringException(Type type, string value) public class InvalidEnumStringException(Type type, string value)
: Exception($"Invalid value '{value}' for enum type {type.FullName}.") { } : Exception($"Invalid value '{value}' for enum type {type.FullName}.") { }
} }
internal interface IEnumSolver { }
public abstract class EnumSolver<T> : IEnumSolver
where T : Enum
{
public abstract Dictionary<string, T> Data { get; }
public T? SolveEnum(string input)
{
return Data.GetValueOrDefault(input);
}
}
@@ -13,7 +13,7 @@ using Pf2eModel.Model.Enum;
namespace Pf2eModel.Migrations namespace Pf2eModel.Migrations
{ {
[DbContext(typeof(Pf2eDbContext))] [DbContext(typeof(Pf2eDbContext))]
[Migration("20250510202833_1")] [Migration("20250514102154_1")]
partial class _1 partial class _1
{ {
/// <inheritdoc /> /// <inheritdoc />
@@ -86,6 +86,25 @@ namespace Pf2eModel.Migrations
b.ToTable("entity_keyword", (string)null); b.ToTable("entity_keyword", (string)null);
}); });
modelBuilder.Entity("EntityLegacyEntity", b =>
{
b.Property<int>("LegacyEntitiesId")
.HasColumnType("integer")
.HasColumnName("legacy_entities_id");
b.Property<int>("RemasterEntitiesId")
.HasColumnType("integer")
.HasColumnName("remaster_entities_id");
b.HasKey("LegacyEntitiesId", "RemasterEntitiesId")
.HasName("pk_entity_legacy_entity");
b.HasIndex("RemasterEntitiesId")
.HasDatabaseName("ix_entity_legacy_entity_remaster_entities_id");
b.ToTable("entity_legacy_entity", (string)null);
});
modelBuilder.Entity("EntityTrait", b => modelBuilder.Entity("EntityTrait", b =>
{ {
b.Property<int>("EntitiesId") b.Property<int>("EntitiesId")
@@ -240,6 +259,123 @@ namespace Pf2eModel.Migrations
b.ToTable("keyword_aliases", (string)null); b.ToTable("keyword_aliases", (string)null);
}); });
modelBuilder.Entity("Pf2eModel.Model.Base.Legacy.LegacyEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id")
.HasColumnOrder(1);
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("name")
.HasColumnOrder(2)
.HasComment("Отображаемое имя");
b.HasKey("Id")
.HasName("pk_legacy_entities");
b.ToTable("legacy_entities", (string)null);
});
modelBuilder.Entity("Pf2eModel.Model.Base.Legacy.LegacyEntityExternal", b =>
{
b.Property<int>("LegacyEntityId")
.HasColumnType("integer")
.HasColumnName("legacy_entity_id")
.HasColumnOrder(1)
.HasComment("Идентификатор сущности");
b.Property<int>("TargetId")
.HasColumnType("integer")
.HasColumnName("target_id")
.HasColumnOrder(2)
.HasComment("Идентификатор цели");
b.Property<string>("ExternalId")
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasColumnName("external_id")
.HasColumnOrder(3);
b.Property<string>("Md5")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("md5")
.HasColumnOrder(5)
.HasComment("Контрольная сумма объекта");
b.Property<string>("Suffix")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasColumnName("suffix")
.HasColumnOrder(4)
.HasComment("Часть URL до объекта. Для источников Foundry - полный UUID объекта");
b.HasKey("LegacyEntityId", "TargetId")
.HasName("pk_legacy_entity_externals");
b.HasIndex("TargetId")
.HasDatabaseName("ix_legacy_entity_externals_target_id");
b.ToTable("legacy_entity_externals", (string)null);
});
modelBuilder.Entity("Pf2eModel.Model.Base.Legacy.LegacyEntitySource", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id")
.HasColumnOrder(0)
.HasComment("Идентификатор");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("IsRemaster")
.HasColumnType("boolean")
.HasColumnName("is_remaster")
.HasColumnOrder(4)
.HasComment("Флаг, указывающий, что объект является частью ремастера");
b.Property<int>("LegacyEntityId")
.HasColumnType("integer")
.HasColumnName("legacy_entity_id")
.HasColumnOrder(1)
.HasComment("Идентификатор устаревшей сущности");
b.Property<int>("SourceId")
.HasColumnType("integer")
.HasColumnName("source_id")
.HasColumnOrder(2)
.HasComment("Идентификатор источника");
b.Property<string>("Suffix")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasColumnName("suffix")
.HasColumnOrder(3)
.HasComment("Номер страницы, эррата и т.д.");
b.HasKey("Id")
.HasName("pk_legacy_entity_sources");
b.HasIndex("LegacyEntityId")
.HasDatabaseName("ix_legacy_entity_sources_legacy_entity_id");
b.HasIndex("SourceId")
.HasDatabaseName("ix_legacy_entity_sources_source_id");
b.ToTable("legacy_entity_sources", (string)null);
});
modelBuilder.Entity("Pf2eModel.Model.Base.Source.EntitySource", b => modelBuilder.Entity("Pf2eModel.Model.Base.Source.EntitySource", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -340,6 +476,12 @@ namespace Pf2eModel.Migrations
.HasColumnOrder(2) .HasColumnOrder(2)
.HasComment("Идентификатор цели"); .HasComment("Идентификатор цели");
b.Property<string>("ExternalId")
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasColumnName("external_id")
.HasColumnOrder(3);
b.Property<string>("Md5") b.Property<string>("Md5")
.IsRequired() .IsRequired()
.HasMaxLength(32) .HasMaxLength(32)
@@ -1842,6 +1984,23 @@ namespace Pf2eModel.Migrations
.HasConstraintName("fk_entity_keyword_keywords_keywords_id"); .HasConstraintName("fk_entity_keyword_keywords_keywords_id");
}); });
modelBuilder.Entity("EntityLegacyEntity", b =>
{
b.HasOne("Pf2eModel.Model.Base.Legacy.LegacyEntity", null)
.WithMany()
.HasForeignKey("LegacyEntitiesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_entity_legacy_entity_legacy_entities_legacy_entities_id");
b.HasOne("Pf2eModel.Model.Base.Entity", null)
.WithMany()
.HasForeignKey("RemasterEntitiesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_entity_legacy_entity_entities_remaster_entities_id");
});
modelBuilder.Entity("EntityTrait", b => modelBuilder.Entity("EntityTrait", b =>
{ {
b.HasOne("Pf2eModel.Model.Base.Entity", null) b.HasOne("Pf2eModel.Model.Base.Entity", null)
@@ -1871,6 +2030,48 @@ namespace Pf2eModel.Migrations
b.Navigation("Keyword"); b.Navigation("Keyword");
}); });
modelBuilder.Entity("Pf2eModel.Model.Base.Legacy.LegacyEntityExternal", b =>
{
b.HasOne("Pf2eModel.Model.Base.Legacy.LegacyEntity", "LegacyEntity")
.WithMany("Externals")
.HasForeignKey("LegacyEntityId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_legacy_entity_externals_legacy_entities_legacy_entity_id");
b.HasOne("Pf2eModel.Model.Base.Targets.ExternalTarget", "Target")
.WithMany("LegacyEntityExternals")
.HasForeignKey("TargetId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_legacy_entity_externals_external_targets_target_id");
b.Navigation("LegacyEntity");
b.Navigation("Target");
});
modelBuilder.Entity("Pf2eModel.Model.Base.Legacy.LegacyEntitySource", b =>
{
b.HasOne("Pf2eModel.Model.Base.Legacy.LegacyEntity", "LegacyEntity")
.WithMany("Sources")
.HasForeignKey("LegacyEntityId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_legacy_entity_sources_legacy_entities_legacy_entity_id");
b.HasOne("Pf2eModel.Model.Base.Source.Source", "Source")
.WithMany("LegacyEntitySources")
.HasForeignKey("SourceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_legacy_entity_sources_sources_source_id");
b.Navigation("LegacyEntity");
b.Navigation("Source");
});
modelBuilder.Entity("Pf2eModel.Model.Base.Source.EntitySource", b => modelBuilder.Entity("Pf2eModel.Model.Base.Source.EntitySource", b =>
{ {
b.HasOne("Pf2eModel.Model.Base.Entity", "Entity") b.HasOne("Pf2eModel.Model.Base.Entity", "Entity")
@@ -2509,14 +2710,25 @@ namespace Pf2eModel.Migrations
b.Navigation("Aliases"); b.Navigation("Aliases");
}); });
modelBuilder.Entity("Pf2eModel.Model.Base.Legacy.LegacyEntity", b =>
{
b.Navigation("Externals");
b.Navigation("Sources");
});
modelBuilder.Entity("Pf2eModel.Model.Base.Source.Source", b => modelBuilder.Entity("Pf2eModel.Model.Base.Source.Source", b =>
{ {
b.Navigation("EntitySources"); b.Navigation("EntitySources");
b.Navigation("LegacyEntitySources");
}); });
modelBuilder.Entity("Pf2eModel.Model.Base.Targets.ExternalTarget", b => modelBuilder.Entity("Pf2eModel.Model.Base.Targets.ExternalTarget", b =>
{ {
b.Navigation("EntityExternals"); b.Navigation("EntityExternals");
b.Navigation("LegacyEntityExternals");
}); });
modelBuilder.Entity("Pf2eModel.Model.Structure.Ancestry", b => modelBuilder.Entity("Pf2eModel.Model.Structure.Ancestry", b =>
@@ -101,6 +101,19 @@ namespace Pf2eModel.Migrations
table.PrimaryKey("pk_keywords", x => x.id); table.PrimaryKey("pk_keywords", x => x.id);
}); });
migrationBuilder.CreateTable(
name: "legacy_entities",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
name = table.Column<string>(type: "jsonb", nullable: false, comment: "Отображаемое имя")
},
constraints: table =>
{
table.PrimaryKey("pk_legacy_entities", x => x.id);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "sources", name: "sources",
columns: table => new columns: table => new
@@ -399,6 +412,7 @@ namespace Pf2eModel.Migrations
{ {
entity_id = table.Column<int>(type: "integer", nullable: false, comment: "Идентификатор сущности"), entity_id = table.Column<int>(type: "integer", nullable: false, comment: "Идентификатор сущности"),
target_id = table.Column<int>(type: "integer", nullable: false, comment: "Идентификатор цели"), target_id = table.Column<int>(type: "integer", nullable: false, comment: "Идентификатор цели"),
external_id = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
suffix = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false, comment: "Часть URL до объекта. Для источников Foundry - полный UUID объекта"), suffix = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false, comment: "Часть URL до объекта. Для источников Foundry - полный UUID объекта"),
md5 = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false, comment: "Контрольная сумма объекта") md5 = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false, comment: "Контрольная сумма объекта")
}, },
@@ -463,6 +477,57 @@ namespace Pf2eModel.Migrations
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable(
name: "entity_legacy_entity",
columns: table => new
{
legacy_entities_id = table.Column<int>(type: "integer", nullable: false),
remaster_entities_id = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_entity_legacy_entity", x => new { x.legacy_entities_id, x.remaster_entities_id });
table.ForeignKey(
name: "fk_entity_legacy_entity_entities_remaster_entities_id",
column: x => x.remaster_entities_id,
principalTable: "entities",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_entity_legacy_entity_legacy_entities_legacy_entities_id",
column: x => x.legacy_entities_id,
principalTable: "legacy_entities",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "legacy_entity_externals",
columns: table => new
{
legacy_entity_id = table.Column<int>(type: "integer", nullable: false, comment: "Идентификатор сущности"),
target_id = table.Column<int>(type: "integer", nullable: false, comment: "Идентификатор цели"),
external_id = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
suffix = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false, comment: "Часть URL до объекта. Для источников Foundry - полный UUID объекта"),
md5 = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false, comment: "Контрольная сумма объекта")
},
constraints: table =>
{
table.PrimaryKey("pk_legacy_entity_externals", x => new { x.legacy_entity_id, x.target_id });
table.ForeignKey(
name: "fk_legacy_entity_externals_external_targets_target_id",
column: x => x.target_id,
principalTable: "external_targets",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_legacy_entity_externals_legacy_entities_legacy_entity_id",
column: x => x.legacy_entity_id,
principalTable: "legacy_entities",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "entity_sources", name: "entity_sources",
columns: table => new columns: table => new
@@ -491,6 +556,34 @@ namespace Pf2eModel.Migrations
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable(
name: "legacy_entity_sources",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false, comment: "Идентификатор")
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
legacy_entity_id = table.Column<int>(type: "integer", nullable: false, comment: "Идентификатор устаревшей сущности"),
source_id = table.Column<int>(type: "integer", nullable: false, comment: "Идентификатор источника"),
suffix = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false, comment: "Номер страницы, эррата и т.д."),
is_remaster = table.Column<bool>(type: "boolean", nullable: false, comment: "Флаг, указывающий, что объект является частью ремастера")
},
constraints: table =>
{
table.PrimaryKey("pk_legacy_entity_sources", x => x.id);
table.ForeignKey(
name: "fk_legacy_entity_sources_legacy_entities_legacy_entity_id",
column: x => x.legacy_entity_id,
principalTable: "legacy_entities",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "fk_legacy_entity_sources_sources_source_id",
column: x => x.source_id,
principalTable: "sources",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "ancestry_boosts", name: "ancestry_boosts",
columns: table => new columns: table => new
@@ -1260,6 +1353,11 @@ namespace Pf2eModel.Migrations
table: "entity_keyword", table: "entity_keyword",
column: "keywords_id"); column: "keywords_id");
migrationBuilder.CreateIndex(
name: "ix_entity_legacy_entity_remaster_entities_id",
table: "entity_legacy_entity",
column: "remaster_entities_id");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_entity_sources_entity_id", name: "ix_entity_sources_entity_id",
table: "entity_sources", table: "entity_sources",
@@ -1295,6 +1393,21 @@ namespace Pf2eModel.Migrations
table: "languages", table: "languages",
column: "entity_id"); column: "entity_id");
migrationBuilder.CreateIndex(
name: "ix_legacy_entity_externals_target_id",
table: "legacy_entity_externals",
column: "target_id");
migrationBuilder.CreateIndex(
name: "ix_legacy_entity_sources_legacy_entity_id",
table: "legacy_entity_sources",
column: "legacy_entity_id");
migrationBuilder.CreateIndex(
name: "ix_legacy_entity_sources_source_id",
table: "legacy_entity_sources",
column: "source_id");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_ritual_checks_ritual_id", name: "ix_ritual_checks_ritual_id",
table: "ritual_checks", table: "ritual_checks",
@@ -1411,6 +1524,9 @@ namespace Pf2eModel.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "entity_keyword"); name: "entity_keyword");
migrationBuilder.DropTable(
name: "entity_legacy_entity");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "entity_sources"); name: "entity_sources");
@@ -1429,6 +1545,12 @@ namespace Pf2eModel.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "keyword_aliases"); name: "keyword_aliases");
migrationBuilder.DropTable(
name: "legacy_entity_externals");
migrationBuilder.DropTable(
name: "legacy_entity_sources");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "ritual_areas"); name: "ritual_areas");
@@ -1465,12 +1587,6 @@ namespace Pf2eModel.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "deities"); name: "deities");
migrationBuilder.DropTable(
name: "external_targets");
migrationBuilder.DropTable(
name: "sources");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "traits"); name: "traits");
@@ -1480,6 +1596,15 @@ namespace Pf2eModel.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "keywords"); name: "keywords");
migrationBuilder.DropTable(
name: "external_targets");
migrationBuilder.DropTable(
name: "legacy_entities");
migrationBuilder.DropTable(
name: "sources");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "rituals"); name: "rituals");
@@ -83,6 +83,25 @@ namespace Pf2eModel.Migrations
b.ToTable("entity_keyword", (string)null); b.ToTable("entity_keyword", (string)null);
}); });
modelBuilder.Entity("EntityLegacyEntity", b =>
{
b.Property<int>("LegacyEntitiesId")
.HasColumnType("integer")
.HasColumnName("legacy_entities_id");
b.Property<int>("RemasterEntitiesId")
.HasColumnType("integer")
.HasColumnName("remaster_entities_id");
b.HasKey("LegacyEntitiesId", "RemasterEntitiesId")
.HasName("pk_entity_legacy_entity");
b.HasIndex("RemasterEntitiesId")
.HasDatabaseName("ix_entity_legacy_entity_remaster_entities_id");
b.ToTable("entity_legacy_entity", (string)null);
});
modelBuilder.Entity("EntityTrait", b => modelBuilder.Entity("EntityTrait", b =>
{ {
b.Property<int>("EntitiesId") b.Property<int>("EntitiesId")
@@ -237,6 +256,123 @@ namespace Pf2eModel.Migrations
b.ToTable("keyword_aliases", (string)null); b.ToTable("keyword_aliases", (string)null);
}); });
modelBuilder.Entity("Pf2eModel.Model.Base.Legacy.LegacyEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id")
.HasColumnOrder(1);
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("name")
.HasColumnOrder(2)
.HasComment("Отображаемое имя");
b.HasKey("Id")
.HasName("pk_legacy_entities");
b.ToTable("legacy_entities", (string)null);
});
modelBuilder.Entity("Pf2eModel.Model.Base.Legacy.LegacyEntityExternal", b =>
{
b.Property<int>("LegacyEntityId")
.HasColumnType("integer")
.HasColumnName("legacy_entity_id")
.HasColumnOrder(1)
.HasComment("Идентификатор сущности");
b.Property<int>("TargetId")
.HasColumnType("integer")
.HasColumnName("target_id")
.HasColumnOrder(2)
.HasComment("Идентификатор цели");
b.Property<string>("ExternalId")
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasColumnName("external_id")
.HasColumnOrder(3);
b.Property<string>("Md5")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)")
.HasColumnName("md5")
.HasColumnOrder(5)
.HasComment("Контрольная сумма объекта");
b.Property<string>("Suffix")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasColumnName("suffix")
.HasColumnOrder(4)
.HasComment("Часть URL до объекта. Для источников Foundry - полный UUID объекта");
b.HasKey("LegacyEntityId", "TargetId")
.HasName("pk_legacy_entity_externals");
b.HasIndex("TargetId")
.HasDatabaseName("ix_legacy_entity_externals_target_id");
b.ToTable("legacy_entity_externals", (string)null);
});
modelBuilder.Entity("Pf2eModel.Model.Base.Legacy.LegacyEntitySource", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id")
.HasColumnOrder(0)
.HasComment("Идентификатор");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("IsRemaster")
.HasColumnType("boolean")
.HasColumnName("is_remaster")
.HasColumnOrder(4)
.HasComment("Флаг, указывающий, что объект является частью ремастера");
b.Property<int>("LegacyEntityId")
.HasColumnType("integer")
.HasColumnName("legacy_entity_id")
.HasColumnOrder(1)
.HasComment("Идентификатор устаревшей сущности");
b.Property<int>("SourceId")
.HasColumnType("integer")
.HasColumnName("source_id")
.HasColumnOrder(2)
.HasComment("Идентификатор источника");
b.Property<string>("Suffix")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasColumnName("suffix")
.HasColumnOrder(3)
.HasComment("Номер страницы, эррата и т.д.");
b.HasKey("Id")
.HasName("pk_legacy_entity_sources");
b.HasIndex("LegacyEntityId")
.HasDatabaseName("ix_legacy_entity_sources_legacy_entity_id");
b.HasIndex("SourceId")
.HasDatabaseName("ix_legacy_entity_sources_source_id");
b.ToTable("legacy_entity_sources", (string)null);
});
modelBuilder.Entity("Pf2eModel.Model.Base.Source.EntitySource", b => modelBuilder.Entity("Pf2eModel.Model.Base.Source.EntitySource", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -337,6 +473,12 @@ namespace Pf2eModel.Migrations
.HasColumnOrder(2) .HasColumnOrder(2)
.HasComment("Идентификатор цели"); .HasComment("Идентификатор цели");
b.Property<string>("ExternalId")
.HasMaxLength(255)
.HasColumnType("character varying(255)")
.HasColumnName("external_id")
.HasColumnOrder(3);
b.Property<string>("Md5") b.Property<string>("Md5")
.IsRequired() .IsRequired()
.HasMaxLength(32) .HasMaxLength(32)
@@ -1839,6 +1981,23 @@ namespace Pf2eModel.Migrations
.HasConstraintName("fk_entity_keyword_keywords_keywords_id"); .HasConstraintName("fk_entity_keyword_keywords_keywords_id");
}); });
modelBuilder.Entity("EntityLegacyEntity", b =>
{
b.HasOne("Pf2eModel.Model.Base.Legacy.LegacyEntity", null)
.WithMany()
.HasForeignKey("LegacyEntitiesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_entity_legacy_entity_legacy_entities_legacy_entities_id");
b.HasOne("Pf2eModel.Model.Base.Entity", null)
.WithMany()
.HasForeignKey("RemasterEntitiesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_entity_legacy_entity_entities_remaster_entities_id");
});
modelBuilder.Entity("EntityTrait", b => modelBuilder.Entity("EntityTrait", b =>
{ {
b.HasOne("Pf2eModel.Model.Base.Entity", null) b.HasOne("Pf2eModel.Model.Base.Entity", null)
@@ -1868,6 +2027,48 @@ namespace Pf2eModel.Migrations
b.Navigation("Keyword"); b.Navigation("Keyword");
}); });
modelBuilder.Entity("Pf2eModel.Model.Base.Legacy.LegacyEntityExternal", b =>
{
b.HasOne("Pf2eModel.Model.Base.Legacy.LegacyEntity", "LegacyEntity")
.WithMany("Externals")
.HasForeignKey("LegacyEntityId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_legacy_entity_externals_legacy_entities_legacy_entity_id");
b.HasOne("Pf2eModel.Model.Base.Targets.ExternalTarget", "Target")
.WithMany("LegacyEntityExternals")
.HasForeignKey("TargetId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_legacy_entity_externals_external_targets_target_id");
b.Navigation("LegacyEntity");
b.Navigation("Target");
});
modelBuilder.Entity("Pf2eModel.Model.Base.Legacy.LegacyEntitySource", b =>
{
b.HasOne("Pf2eModel.Model.Base.Legacy.LegacyEntity", "LegacyEntity")
.WithMany("Sources")
.HasForeignKey("LegacyEntityId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_legacy_entity_sources_legacy_entities_legacy_entity_id");
b.HasOne("Pf2eModel.Model.Base.Source.Source", "Source")
.WithMany("LegacyEntitySources")
.HasForeignKey("SourceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_legacy_entity_sources_sources_source_id");
b.Navigation("LegacyEntity");
b.Navigation("Source");
});
modelBuilder.Entity("Pf2eModel.Model.Base.Source.EntitySource", b => modelBuilder.Entity("Pf2eModel.Model.Base.Source.EntitySource", b =>
{ {
b.HasOne("Pf2eModel.Model.Base.Entity", "Entity") b.HasOne("Pf2eModel.Model.Base.Entity", "Entity")
@@ -2506,14 +2707,25 @@ namespace Pf2eModel.Migrations
b.Navigation("Aliases"); b.Navigation("Aliases");
}); });
modelBuilder.Entity("Pf2eModel.Model.Base.Legacy.LegacyEntity", b =>
{
b.Navigation("Externals");
b.Navigation("Sources");
});
modelBuilder.Entity("Pf2eModel.Model.Base.Source.Source", b => modelBuilder.Entity("Pf2eModel.Model.Base.Source.Source", b =>
{ {
b.Navigation("EntitySources"); b.Navigation("EntitySources");
b.Navigation("LegacyEntitySources");
}); });
modelBuilder.Entity("Pf2eModel.Model.Base.Targets.ExternalTarget", b => modelBuilder.Entity("Pf2eModel.Model.Base.Targets.ExternalTarget", b =>
{ {
b.Navigation("EntityExternals"); b.Navigation("EntityExternals");
b.Navigation("LegacyEntityExternals");
}); });
modelBuilder.Entity("Pf2eModel.Model.Structure.Ancestry", b => modelBuilder.Entity("Pf2eModel.Model.Structure.Ancestry", b =>
+6
View File
@@ -2,6 +2,7 @@
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Pf2eModel.Model.Base.Keywords; using Pf2eModel.Model.Base.Keywords;
using Pf2eModel.Model.Base.Legacy;
using Pf2eModel.Model.Base.Source; using Pf2eModel.Model.Base.Source;
using Pf2eModel.Model.Base.Targets; using Pf2eModel.Model.Base.Targets;
using Pf2eModel.Model.Common.I18n; using Pf2eModel.Model.Common.I18n;
@@ -99,4 +100,9 @@ public class Entity
/// Коллекция внешних ссылок, связанных с сущностью. /// Коллекция внешних ссылок, связанных с сущностью.
/// </summary> /// </summary>
public ICollection<EntityExternal> Externals { get; set; } = []; public ICollection<EntityExternal> Externals { get; set; } = [];
/// <summary>
/// Коллекция устаревших сущностей.
/// </summary>
public IEnumerable<LegacyEntity> LegacyEntities { get; set; } = [];
} }
@@ -0,0 +1,42 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using Pf2eModel.Model.Common.I18n;
namespace Pf2eModel.Model.Base.Legacy;
/// <summary>
/// Информация об устаревшей сущности
/// </summary>
public class LegacyEntity
{
/// <summary>
/// Идентификатор сущности.
/// </summary>
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Column(Order = 1)]
public int Id { get; set; }
/// <summary>
/// Отображаемое имя сущности.
/// </summary>
[Column(Order = 2)]
[Comment("Отображаемое имя")]
public LocaleString Name { get; set; } = [];
/// <summary>
/// Коллекция обновлённых сущностей.
/// </summary>
public ICollection<Entity> RemasterEntities { get; set; } = [];
/// <summary>
/// Коллекция источников, связанных с сущностью.
/// </summary>
public ICollection<LegacyEntitySource> Sources { get; set; } = [];
/// <summary>
/// Коллекция внешних ссылок, связанных с сущностью.
/// </summary>
public ICollection<LegacyEntityExternal> Externals { get; set; } = [];
}
@@ -0,0 +1,58 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using Pf2eModel.Model.Base.Targets;
namespace Pf2eModel.Model.Base.Legacy;
/// <summary>
/// Внешняя ссылка сущности.
/// </summary>
[PrimaryKey(nameof(LegacyEntityId), nameof(TargetId))]
public class LegacyEntityExternal
{
/// <summary>
/// Идентификатор сущности.
/// </summary>
[Column(Order = 1)]
[Comment("Идентификатор сущности")]
public int LegacyEntityId { get; set; }
/// <summary>
/// Идентификатор цели.
/// </summary>
[Column(Order = 2)]
[Comment("Идентификатор цели")]
public int TargetId { get; set; }
[Column(Order = 3)]
[MaxLength(255)]
public string? ExternalId { get; set; } = null;
/// <summary>
/// Конечная часть URL внешней ссылки.
/// Для источников Foundry - полный UUID объекта.
/// </summary>
[Column(Order = 4)]
[MaxLength(255)]
[Comment("Часть URL до объекта. Для источников Foundry - полный UUID объекта")]
public string Suffix { get; set; } = "";
/// <summary>
/// Контрольная сумма объекта.
/// </summary>
[Column(Order = 5)]
[MaxLength(32)]
[Comment("Контрольная сумма объекта")]
public string Md5 { get; set; } = null!;
/// <summary>
/// Сущность, которая ссылается на внешнюю ссылку.
/// </summary>
public LegacyEntity LegacyEntity { get; set; } = null!;
/// <summary>
/// Цель внешней ссылки.
/// </summary>
public ExternalTarget Target { get; set; } = null!;
}
@@ -0,0 +1,59 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace Pf2eModel.Model.Base.Legacy;
/// <summary>
/// Связь между сущностью и источником.
/// </summary>
public class LegacyEntitySource
{
/// <summary>
/// Идентификатор связи.
/// </summary>
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Column(Order = 0)]
[Comment("Идентификатор")]
public int Id { get; set; }
/// <summary>
/// Идентификатор устаревшей сущности.
/// </summary>
[Column(Order = 1)]
[Comment("Идентификатор устаревшей сущности")]
public int LegacyEntityId { get; set; }
/// <summary>
/// Идентификатор источника.
/// </summary>
[Column(Order = 2)]
[Comment("Идентификатор источника")]
public int SourceId { get; set; }
/// <summary>
/// Дополнительная информация о источнике, например, номер страницы, номер тома и т.д.
/// </summary>
[Column(Order = 3)]
[MaxLength(255)]
[Comment("Номер страницы, эррата и т.д.")]
public string Suffix { get; set; } = null!;
/// <summary>
/// Флаг, указывающий, что объект является частью ремастера.
/// </summary>
[Column(Order = 4)]
[Comment("Флаг, указывающий, что объект является частью ремастера")]
public bool IsRemaster { get; set; }
/// <summary>
/// Сущность, к которой относится источник.
/// </summary>
public LegacyEntity LegacyEntity { get; set; } = null!;
/// <summary>
/// Источник, к которому относится сущность.
/// </summary>
public Source.Source Source { get; set; } = null!;
}
+6
View File
@@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Pf2eModel.Model.Base.Legacy;
using Pf2eModel.Model.Enum; using Pf2eModel.Model.Enum;
namespace Pf2eModel.Model.Base.Source; namespace Pf2eModel.Model.Base.Source;
@@ -45,4 +46,9 @@ public class Source
/// Связанные сущности. /// Связанные сущности.
/// </summary> /// </summary>
public ICollection<EntitySource> EntitySources { get; set; } = []; public ICollection<EntitySource> EntitySources { get; set; } = [];
/// <summary>
/// Связанные сущности.
/// </summary>
public ICollection<LegacyEntitySource> LegacyEntitySources { get; set; } = [];
} }
@@ -24,11 +24,9 @@ public class EntityExternal
[Comment("Идентификатор цели")] [Comment("Идентификатор цели")]
public int TargetId { get; set; } public int TargetId { get; set; }
/*
[Column(Order = 3)] [Column(Order = 3)]
[MaxLength(255)] [MaxLength(255)]
public string ExternalId { get; set; } = ""; public string? ExternalId { get; set; } = null;
*/
/// <summary> /// <summary>
/// Конечная часть URL внешней ссылки. /// Конечная часть URL внешней ссылки.
@@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Pf2eModel.Model.Base.Legacy;
namespace Pf2eModel.Model.Base.Targets; namespace Pf2eModel.Model.Base.Targets;
@@ -46,4 +47,9 @@ public class ExternalTarget
/// Коллекция сущностей, которые ссылаются на эту цель. /// Коллекция сущностей, которые ссылаются на эту цель.
/// </summary> /// </summary>
public ICollection<EntityExternal> EntityExternals { get; set; } = []; public ICollection<EntityExternal> EntityExternals { get; set; } = [];
/// <summary>
/// Коллекция устаревших сущностей, которые ссылаются на эту цель.
/// </summary>
public ICollection<LegacyEntityExternal> LegacyEntityExternals { get; set; } = [];
} }
+7
View File
@@ -3,6 +3,7 @@ using dotenv.net.Utilities;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Pf2eModel.Model.Base; using Pf2eModel.Model.Base;
using Pf2eModel.Model.Base.Keywords; using Pf2eModel.Model.Base.Keywords;
using Pf2eModel.Model.Base.Legacy;
using Pf2eModel.Model.Base.Source; using Pf2eModel.Model.Base.Source;
using Pf2eModel.Model.Base.Targets; using Pf2eModel.Model.Base.Targets;
using Pf2eModel.Model.Common; using Pf2eModel.Model.Common;
@@ -97,6 +98,12 @@ public partial class Pf2eDbContext : DbContext
public virtual DbSet<Language> Languages { get; set; } public virtual DbSet<Language> Languages { get; set; }
public virtual DbSet<LegacyEntity> LegacyEntities { get; set; }
public virtual DbSet<LegacyEntityExternal> LegacyEntityExternals { get; set; }
public virtual DbSet<LegacyEntitySource> LegacyEntitySources { get; set; }
public virtual DbSet<Ritual> Rituals { get; set; } public virtual DbSet<Ritual> Rituals { get; set; }
public virtual DbSet<RitualArea> RitualAreas { get; set; } public virtual DbSet<RitualArea> RitualAreas { get; set; }
+35
View File
@@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Pf2eModel.Model; using Pf2eModel.Model;
using Pf2eModel.Model.Base; using Pf2eModel.Model.Base;
using Pf2eModel.Model.Base.Keywords; using Pf2eModel.Model.Base.Keywords;
using Pf2eModel.Model.Base.Legacy;
using Pf2eModel.Model.Base.Source; using Pf2eModel.Model.Base.Source;
using Pf2eModel.Model.Base.Targets; using Pf2eModel.Model.Base.Targets;
using Pf2eModel.Model.Common; using Pf2eModel.Model.Common;
@@ -144,6 +145,28 @@ public partial class Pf2eDbContext
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
}); });
// LegacyEntity
modelBuilder.Entity<LegacyEntity>(entity =>
{
BindLocaleString(entity, l => l.Name);
entity.HasMany(l => l.RemasterEntities).WithMany(e => e.LegacyEntities);
// LegacyEntitySource
entity
.HasMany(e => e.Sources)
.WithOne(e => e.LegacyEntity)
.HasForeignKey(e => e.LegacyEntityId)
.OnDelete(DeleteBehavior.Cascade);
// LegacyEntityExternal
entity
.HasMany(e => e.Externals)
.WithOne(e => e.LegacyEntity)
.HasForeignKey(e => e.LegacyEntityId)
.OnDelete(DeleteBehavior.Cascade);
});
// Source // Source
modelBuilder.Entity<Source>(entity => modelBuilder.Entity<Source>(entity =>
{ {
@@ -155,6 +178,12 @@ public partial class Pf2eDbContext
.HasForeignKey(e => e.SourceId) .HasForeignKey(e => e.SourceId)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
entity
.HasMany(s => s.LegacyEntitySources)
.WithOne(e => e.Source)
.HasForeignKey(e => e.SourceId)
.OnDelete(DeleteBehavior.Cascade);
// + EntitySource // + EntitySource
}); });
modelBuilder.Entity<EntitySource>(entity => { }); modelBuilder.Entity<EntitySource>(entity => { });
@@ -167,6 +196,12 @@ public partial class Pf2eDbContext
.WithOne(e => e.Target) .WithOne(e => e.Target)
.HasForeignKey(e => e.TargetId) .HasForeignKey(e => e.TargetId)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
entity
.HasMany(t => t.LegacyEntityExternals)
.WithOne(e => e.Target)
.HasForeignKey(e => e.TargetId)
.OnDelete(DeleteBehavior.Cascade);
}); });
modelBuilder.Entity<EntityExternal>(entity => { }); modelBuilder.Entity<EntityExternal>(entity => { });