Db: Migrate ImportHistory to sqlite

This commit is contained in:
Liza 2023-02-16 19:51:54 +01:00
parent 7e2ccd3b42
commit c7d5aa1eaa
18 changed files with 415 additions and 71 deletions

View File

@ -7,6 +7,8 @@ using System.Text.Json;
using Dalamud.Logging; using Dalamud.Logging;
using Dalamud.Plugin; using Dalamud.Plugin;
using ImGuiNET; using ImGuiNET;
using Microsoft.Extensions.DependencyInjection;
using Pal.Client.Database;
using NJson = Newtonsoft.Json; using NJson = Newtonsoft.Json;
namespace Pal.Client.Configuration namespace Pal.Client.Configuration
@ -14,12 +16,14 @@ namespace Pal.Client.Configuration
internal sealed class ConfigurationManager internal sealed class ConfigurationManager
{ {
private readonly DalamudPluginInterface _pluginInterface; private readonly DalamudPluginInterface _pluginInterface;
private readonly IServiceProvider _serviceProvider;
public event EventHandler<IPalacePalConfiguration>? Saved; public event EventHandler<IPalacePalConfiguration>? Saved;
public ConfigurationManager(DalamudPluginInterface pluginInterface) public ConfigurationManager(DalamudPluginInterface pluginInterface, IServiceProvider serviceProvider)
{ {
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_serviceProvider = serviceProvider;
Migrate(); Migrate();
} }
@ -61,6 +65,26 @@ namespace Pal.Client.Configuration
var v7 = MigrateToV7(configurationV1); var v7 = MigrateToV7(configurationV1);
Save(v7, queue: false); Save(v7, queue: false);
using (var scope = _serviceProvider.CreateScope())
{
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
dbContext.Imports.RemoveRange(dbContext.Imports);
foreach (var importHistory in configurationV1.ImportHistory)
{
PluginLog.Information($"Migrating import {importHistory.Id}");
dbContext.Imports.Add(new ImportHistory
{
Id = importHistory.Id,
RemoteUrl = importHistory.RemoteUrl?.Replace(".μ.tv", ".liza.sh"),
ExportedAt = importHistory.ExportedAt,
ImportedAt = importHistory.ImportedAt
});
}
dbContext.SaveChanges();
}
File.Move(_pluginInterface.ConfigFile.FullName, _pluginInterface.ConfigFile.FullName + ".old", true); File.Move(_pluginInterface.ConfigFile.FullName, _pluginInterface.ConfigFile.FullName + ".old", true);
} }
} }

View File

@ -23,9 +23,6 @@ namespace Pal.Client.Configuration
DeepDungeonConfiguration DeepDungeons { get; set; } DeepDungeonConfiguration DeepDungeons { get; set; }
RendererConfiguration Renderer { get; set; } RendererConfiguration Renderer { get; set; }
[Obsolete]
List<ConfigurationV1.ImportHistoryEntry> ImportHistory { get; }
IAccountConfiguration CreateAccount(string server, Guid accountId); IAccountConfiguration CreateAccount(string server, Guid accountId);
IAccountConfiguration? FindAccount(string server); IAccountConfiguration? FindAccount(string server);
void RemoveAccount(string server); void RemoveAccount(string server);

View File

@ -0,0 +1,12 @@
using System;
namespace Pal.Client.Database
{
internal sealed class ImportHistory
{
public Guid Id { get; set; }
public string? RemoteUrl { get; set; }
public DateTime ExportedAt { get; set; }
public DateTime ImportedAt { get; set; }
}
}

View File

@ -0,0 +1,45 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Pal.Client.Database;
#nullable disable
namespace Pal.Client.Database.Migrations
{
[DbContext(typeof(PalClientContext))]
[Migration("20230216154417_AddImportHistory")]
partial class AddImportHistory
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.3");
modelBuilder.Entity("Pal.Client.Database.ImportHistory", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("ExportedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("ImportedAt")
.HasColumnType("TEXT");
b.Property<string>("RemoteUrl")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Imports");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,36 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Pal.Client.Database.Migrations
{
/// <inheritdoc />
public partial class AddImportHistory : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Imports",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
RemoteUrl = table.Column<string>(type: "TEXT", nullable: true),
ExportedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
ImportedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Imports", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Imports");
}
}
}

View File

@ -0,0 +1,42 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Pal.Client.Database;
#nullable disable
namespace Pal.Client.Database.Migrations
{
[DbContext(typeof(PalClientContext))]
partial class PalClientContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.3");
modelBuilder.Entity("Pal.Client.Database.ImportHistory", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("ExportedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("ImportedAt")
.HasColumnType("TEXT");
b.Property<string>("RemoteUrl")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Imports");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,14 @@
using Microsoft.EntityFrameworkCore;
namespace Pal.Client.Database
{
internal class PalClientContext : DbContext
{
public DbSet<ImportHistory> Imports { get; set; } = null!;
public PalClientContext(DbContextOptions<PalClientContext> options)
: base(options)
{
}
}
}

View File

@ -0,0 +1,20 @@
#if EF
using System;
using System.IO;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace Pal.Client.Database
{
internal sealed class PalClientContextFactory : IDesignTimeDbContextFactory<PalClientContext>
{
public PalClientContext CreateDbContext(string[] args)
{
var optionsBuilder =
new DbContextOptionsBuilder<PalClientContext>().UseSqlite(
$"Data Source={Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "XIVLauncher", "pluginConfigs", "Palace Pal", "palace-pal.data.sqlite3")}");
return new PalClientContext(optionsBuilder.Options);
}
}
}
#endif

View File

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Pal.Client.Database;
namespace Pal.Client.DependencyInjection
{
internal sealed class ImportService
{
private readonly IServiceProvider _serviceProvider;
public ImportService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void Add(ImportHistory history)
{
using var scope = _serviceProvider.CreateScope();
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
dbContext.Imports.Add(history);
dbContext.SaveChanges();
}
public ImportHistory? FindLast()
{
using var scope = _serviceProvider.CreateScope();
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
return dbContext.Imports.OrderByDescending(x => x.ImportedAt).ThenBy(x => x.Id).FirstOrDefault();
}
public List<ImportHistory> FindForServer(string server)
{
if (string.IsNullOrEmpty(server))
return new();
using var scope = _serviceProvider.CreateScope();
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
return dbContext.Imports.Where(x => x.RemoteUrl == server).ToList();
}
public void RemoveAllByIds(List<Guid> ids)
{
using var scope = _serviceProvider.CreateScope();
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
dbContext.RemoveRange(dbContext.Imports.Where(x => ids.Contains(x.Id)));
dbContext.SaveChanges();
}
public void RemoveById(Guid id)
=> RemoveAllByIds(new List<Guid> { id });
}
}

View File

@ -1,4 +1,8 @@
using System.Globalization; using System;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game; using Dalamud.Game;
using Dalamud.Game.ClientState; using Dalamud.Game.ClientState;
@ -7,17 +11,22 @@ using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.Command; using Dalamud.Game.Command;
using Dalamud.Game.Gui; using Dalamud.Game.Gui;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Logging;
using Dalamud.Plugin; using Dalamud.Plugin;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Pal.Client.Commands; using Pal.Client.Commands;
using Pal.Client.Configuration; using Pal.Client.Configuration;
using Pal.Client.Database;
using Pal.Client.DependencyInjection;
using Pal.Client.Net; using Pal.Client.Net;
using Pal.Client.Properties; using Pal.Client.Properties;
using Pal.Client.Rendering; using Pal.Client.Rendering;
using Pal.Client.Scheduled; using Pal.Client.Scheduled;
using Pal.Client.Windows; using Pal.Client.Windows;
namespace Pal.Client.DependencyInjection namespace Pal.Client
{ {
/// <summary> /// <summary>
/// DI-aware Plugin. /// DI-aware Plugin.
@ -25,6 +34,8 @@ namespace Pal.Client.DependencyInjection
// ReSharper disable once UnusedType.Global // ReSharper disable once UnusedType.Global
internal sealed class DependencyInjectionContext : IDalamudPlugin internal sealed class DependencyInjectionContext : IDalamudPlugin
{ {
private readonly string _sqliteConnectionString;
private readonly CancellationTokenSource _initCts = new();
private ServiceProvider? _serviceProvider; private ServiceProvider? _serviceProvider;
public string Name => Localization.Palace_Pal; public string Name => Localization.Palace_Pal;
@ -39,6 +50,9 @@ namespace Pal.Client.DependencyInjection
CommandManager commandManager, CommandManager commandManager,
DataManager dataManager) DataManager dataManager)
{ {
PluginLog.Information("Building service container");
CancellationToken token = _initCts.Token;
IServiceCollection services = new ServiceCollection(); IServiceCollection services = new ServiceCollection();
// dalamud // dalamud
@ -54,6 +68,11 @@ namespace Pal.Client.DependencyInjection
services.AddSingleton(dataManager); services.AddSingleton(dataManager);
services.AddSingleton(new WindowSystem(typeof(DependencyInjectionContext).AssemblyQualifiedName)); services.AddSingleton(new WindowSystem(typeof(DependencyInjectionContext).AssemblyQualifiedName));
// EF core
_sqliteConnectionString =
$"Data Source={Path.Join(pluginInterface.GetPluginConfigDirectory(), "palace-pal.data.sqlite3")}";
services.AddDbContext<PalClientContext>(o => o.UseSqlite(_sqliteConnectionString));
// plugin-specific // plugin-specific
services.AddSingleton<Plugin>(); services.AddSingleton<Plugin>();
services.AddSingleton<DebugState>(); services.AddSingleton<DebugState>();
@ -64,11 +83,12 @@ namespace Pal.Client.DependencyInjection
services.AddTransient<RepoVerification>(); services.AddTransient<RepoVerification>();
services.AddSingleton<PalCommand>(); services.AddSingleton<PalCommand>();
// territory handling // territory & marker related services
services.AddSingleton<TerritoryState>(); services.AddSingleton<TerritoryState>();
services.AddSingleton<FrameworkService>(); services.AddSingleton<FrameworkService>();
services.AddSingleton<ChatService>(); services.AddSingleton<ChatService>();
services.AddSingleton<FloorService>(); services.AddSingleton<FloorService>();
services.AddSingleton<ImportService>();
// windows & related services // windows & related services
services.AddSingleton<AgreementWindow>(); services.AddSingleton<AgreementWindow>();
@ -97,7 +117,7 @@ namespace Pal.Client.DependencyInjection
ValidateScopes = true, ValidateScopes = true,
}); });
// initialize plugin
#if RELEASE #if RELEASE
// You're welcome to remove this code in your fork, but please make sure that: // You're welcome to remove this code in your fork, but please make sure that:
// - none of the links accessible within FFXIV open the original repo (e.g. in the plugin installer), and // - none of the links accessible within FFXIV open the original repo (e.g. in the plugin installer), and
@ -108,24 +128,61 @@ namespace Pal.Client.DependencyInjection
_serviceProvider.GetService<RepoVerification>(); _serviceProvider.GetService<RepoVerification>();
#endif #endif
// set up legacy services // This is not ideal as far as loading the plugin goes, because there's no way to check for errors and
LocalState.PluginInterface = pluginInterface; // tell Dalamud that no, the plugin isn't ready -- so the plugin will count as properly initialized,
LocalState.Mode = _serviceProvider.GetRequiredService<IPalacePalConfiguration>().Mode; // even if it's not.
//
// There's 2-3 seconds of slowdown primarily caused by the sqlite init, but that needs to happen for
// config stuff.
PluginLog.Information("Service container built, triggering async init");
Task.Run(async () =>
{
try
{
PluginLog.Information("Starting async init");
// windows that have logic to open on startup // initialize database
_serviceProvider.GetRequiredService<AgreementWindow>(); await using (var scope = _serviceProvider.CreateAsyncScope())
{
PluginLog.Log("Loading database & running migrations");
await using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
await dbContext.Database.MigrateAsync();
// initialize components that are mostly self-contained/self-registered PluginLog.Log("Completed database migrations");
_serviceProvider.GetRequiredService<Hooks>(); }
_serviceProvider.GetRequiredService<PalCommand>();
_serviceProvider.GetRequiredService<FrameworkService>();
_serviceProvider.GetRequiredService<ChatService>();
_serviceProvider.GetRequiredService<Plugin>(); token.ThrowIfCancellationRequested();
// set up legacy services
LocalState.PluginConfigDirectory = pluginInterface.GetPluginConfigDirectory();
LocalState.Mode = _serviceProvider.GetRequiredService<IPalacePalConfiguration>().Mode;
// windows that have logic to open on startup
_serviceProvider.GetRequiredService<AgreementWindow>();
// initialize components that are mostly self-contained/self-registered
_serviceProvider.GetRequiredService<Hooks>();
_serviceProvider.GetRequiredService<PalCommand>();
_serviceProvider.GetRequiredService<FrameworkService>();
_serviceProvider.GetRequiredService<ChatService>();
token.ThrowIfCancellationRequested();
_serviceProvider.GetRequiredService<Plugin>();
PluginLog.Information("Async init complete");
}
catch (Exception e)
{
PluginLog.Error(e, "Async load failed");
chatGui.PrintError($"Async loading failed: {e}");
}
});
} }
public void Dispose() public void Dispose()
{ {
_initCts.Cancel();
// ensure we're not calling dispose recursively on ourselves // ensure we're not calling dispose recursively on ourselves
if (_serviceProvider != null) if (_serviceProvider != null)
{ {
@ -133,6 +190,10 @@ namespace Pal.Client.DependencyInjection
_serviceProvider = null; _serviceProvider = null;
serviceProvider.Dispose(); serviceProvider.Dispose();
// ensure we're not keeping the file open longer than the plugin is loaded
using (SqliteConnection sqliteConnection = new(_sqliteConnectionString))
SqliteConnection.ClearPool(sqliteConnection);
} }
} }
} }

View File

@ -5,7 +5,6 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.Json; using System.Text.Json;
using Dalamud.Plugin;
using Pal.Client.Configuration; using Pal.Client.Configuration;
using Pal.Client.Extensions; using Pal.Client.Extensions;
@ -19,7 +18,7 @@ namespace Pal.Client
private static readonly JsonSerializerOptions JsonSerializerOptions = new() { IncludeFields = true }; private static readonly JsonSerializerOptions JsonSerializerOptions = new() { IncludeFields = true };
private const int CurrentVersion = 4; private const int CurrentVersion = 4;
internal static DalamudPluginInterface PluginInterface { get; set; } internal static string PluginConfigDirectory { get; set; } = null!;
internal static EMode Mode { get; set; } internal static EMode Mode { get; set; }
public uint TerritoryType { get; set; } public uint TerritoryType { get; set; }
@ -126,7 +125,7 @@ namespace Pal.Client
public string GetSaveLocation() => GetSaveLocation(TerritoryType); public string GetSaveLocation() => GetSaveLocation(TerritoryType);
private static string GetSaveLocation(uint territoryType) => Path.Join(PluginInterface.GetPluginConfigDirectory(), $"{territoryType}.json"); private static string GetSaveLocation(uint territoryType) => Path.Join(PluginConfigDirectory, $"{territoryType}.json");
public static void ForEach(Action<LocalState> action) public static void ForEach(Action<LocalState> action)
{ {

View File

@ -25,15 +25,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup Condition="'$(Configuration)' == 'Release' And Exists('Certificate.pfx')"> <ItemGroup Condition="'$(Configuration)' == 'Release' And Exists('Certificate.pfx')">
<None Remove="Certificate.pfx" /> <None Remove="Certificate.pfx" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(Configuration)' == 'Release' And Exists('Certificate.pfx')"> <ItemGroup Condition="'$(Configuration)' == 'Release' And Exists('Certificate.pfx')">
<EmbeddedResource Include="Certificate.pfx" /> <EmbeddedResource Include="Certificate.pfx" />
<EmbeddedResource Update="Properties\Localization.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Localization.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -68,44 +64,51 @@
<!--You may need to adjust these paths yourself. These point to a Dalamud assembly in AppData.--> <!--You may need to adjust these paths yourself. These point to a Dalamud assembly in AppData.-->
<Reference Include="Dalamud"> <Reference Include="Dalamud">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath> <HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
<Private>false</Private> <Private Condition="'$(Configuration)' != 'EF'">false</Private>
</Reference> </Reference>
<Reference Include="ImGui.NET"> <Reference Include="ImGui.NET">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\ImGui.NET.dll</HintPath> <HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\ImGui.NET.dll</HintPath>
<Private>false</Private> <Private Condition="'$(Configuration)' != 'EF'">false</Private>
</Reference> </Reference>
<Reference Include="ImGuiScene"> <Reference Include="ImGuiScene">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll</HintPath> <HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll</HintPath>
<Private>false</Private> <Private Condition="'$(Configuration)' != 'EF'">false</Private>
</Reference> </Reference>
<Reference Include="Lumina"> <Reference Include="Lumina">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll</HintPath> <HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll</HintPath>
<Private>false</Private> <Private Condition="'$(Configuration)' != 'EF'">false</Private>
</Reference> </Reference>
<Reference Include="Lumina.Excel"> <Reference Include="Lumina.Excel">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll</HintPath> <HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll</HintPath>
<Private>false</Private> <Private Condition="'$(Configuration)' != 'EF'">false</Private>
</Reference> </Reference>
<Reference Include="Newtonsoft.Json"> <Reference Include="Newtonsoft.Json">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Newtonsoft.Json.dll</HintPath> <HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Newtonsoft.Json.dll</HintPath>
<Private>false</Private> <Private Condition="'$(Configuration)' != 'EF'">false</Private>
</Reference> </Reference>
<Reference Include="FFXIVClientStructs"> <Reference Include="FFXIVClientStructs">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\FFXIVClientStructs.dll</HintPath> <HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\FFXIVClientStructs.dll</HintPath>
<Private>false</Private> <Private Condition="'$(Configuration)' != 'EF'">false</Private>
</Reference> </Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Update="Properties\Localization.Designer.cs"> <EmbeddedResource Update="Properties\Localization.resx">
<DesignTime>True</DesignTime> <Generator>ResXFileCodeGenerator</Generator>
<AutoGen>True</AutoGen> <LastGenOutput>Localization.Designer.cs</LastGenOutput>
<DependentUpon>Localization.resx</DependentUpon> </EmbeddedResource>
</Compile> <Compile Update="Properties\Localization.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Localization.resx</DependentUpon>
</Compile>
</ItemGroup> </ItemGroup>
<Target Name="RenameLatestZip" AfterTargets="PackagePlugin"> <Target Name="RenameLatestZip" AfterTargets="PackagePlugin" Condition="'$(Configuration)' == 'Release'">
<Exec Command="rename &quot;$(OutDir)$(AssemblyName)\latest.zip&quot; &quot;$(AssemblyName)-$(Version).zip&quot;" /> <Exec Command="rename &quot;$(OutDir)$(AssemblyName)\latest.zip&quot; &quot;$(AssemblyName)-$(Version).zip&quot;" />
</Target> </Target>
<Target Name="Clean">
<RemoveDir Directories="dist" />
</Target>
</Project> </Project>

View File

@ -5,7 +5,6 @@ using Pal.Client.Windows;
using System; using System;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using Dalamud.Logging;
using Pal.Client.Properties; using Pal.Client.Properties;
using ECommons; using ECommons;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -31,8 +30,6 @@ namespace Pal.Client
IPalacePalConfiguration configuration, IPalacePalConfiguration configuration,
RenderAdapter renderAdapter) RenderAdapter renderAdapter)
{ {
PluginLog.Information("Initializing Palace Pal");
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_configuration = configuration; _configuration = configuration;

15
Pal.Client/README.md Normal file
View File

@ -0,0 +1,15 @@
# Palace Pal
## Client Build Notes
### Database Migrations
Since EF core needs all dll files to be present, including Dalamud ones,
there's a special `EF` configuration that exempts them from setting
`<Private>false</Private>` during the build.
To use with `dotnet ef` commands, specify it as `-c EF`, for example:
```shell
dotnet ef migrations add MigrationName --configuration EF
```

View File

@ -7,10 +7,11 @@ using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Game.Gui; using Dalamud.Game.Gui;
using Dalamud.Logging; using Dalamud.Logging;
using Pal.Client.Configuration; using Pal.Client.Database;
using Pal.Client.DependencyInjection; using Pal.Client.DependencyInjection;
using Pal.Client.Extensions; using Pal.Client.Extensions;
using Pal.Client.Properties; using Pal.Client.Properties;
using Pal.Client.Windows;
namespace Pal.Client.Scheduled namespace Pal.Client.Scheduled
{ {
@ -30,17 +31,20 @@ namespace Pal.Client.Scheduled
internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedImport> internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedImport>
{ {
private readonly ChatGui _chatGui; private readonly ChatGui _chatGui;
private readonly IPalacePalConfiguration _configuration;
private readonly ConfigurationManager _configurationManager;
private readonly FloorService _floorService; private readonly FloorService _floorService;
private readonly ImportService _importService;
private readonly ConfigWindow _configWindow;
public Handler(ChatGui chatGui, IPalacePalConfiguration configuration, public Handler(
ConfigurationManager configurationManager, FloorService floorService) ChatGui chatGui,
FloorService floorService,
ImportService importService,
ConfigWindow configWindow)
{ {
_chatGui = chatGui; _chatGui = chatGui;
_configuration = configuration;
_configurationManager = configurationManager;
_floorService = floorService; _floorService = floorService;
_importService = importService;
_configWindow = configWindow;
} }
protected override void Run(QueuedImport import, ref bool recreateLayout, ref bool saveMarkers) protected override void Run(QueuedImport import, ref bool recreateLayout, ref bool saveMarkers)
@ -53,11 +57,9 @@ namespace Pal.Client.Scheduled
if (!Validate(import)) if (!Validate(import))
return; return;
var oldExportIds = string.IsNullOrEmpty(import.Export.ServerUrl) List<Guid> oldExportIds = _importService.FindForServer(import.Export.ServerUrl)
? _configuration.ImportHistory.Where(x => x.RemoteUrl == import.Export.ServerUrl) .Select(x => x.Id)
.Select(x => x.Id) .ToList();
.Where(x => x != Guid.Empty).ToList()
: new List<Guid>();
foreach (var remoteFloor in import.Export.Floors) foreach (var remoteFloor in import.Export.Floors)
{ {
@ -70,17 +72,19 @@ namespace Pal.Client.Scheduled
localState.Save(); localState.Save();
} }
_configuration.ImportHistory.RemoveAll(hist => _importService.RemoveAllByIds(oldExportIds);
oldExportIds.Contains(hist.Id) || hist.Id == import.ExportId); _importService.RemoveById(import.ExportId);
_configuration.ImportHistory.Add(new ConfigurationV1.ImportHistoryEntry _importService.Add(new ImportHistory
{ {
Id = import.ExportId, Id = import.ExportId,
RemoteUrl = import.Export.ServerUrl, RemoteUrl = import.Export.ServerUrl,
ExportedAt = import.Export.CreatedAt.ToDateTime(), ExportedAt = import.Export.CreatedAt.ToDateTime(),
ImportedAt = DateTime.UtcNow, ImportedAt = DateTime.UtcNow,
}); });
_configurationManager.Save(_configuration); _configWindow.UpdateLastImport();
PluginLog.Information(
$"Imported {import.ExportId} for {import.ImportedTraps} traps, {import.ImportedHoardCoffers} hoard coffers");
_chatGui.PalMessage(string.Format(Localization.ImportCompleteStatistics, import.ImportedTraps, _chatGui.PalMessage(string.Format(Localization.ImportCompleteStatistics, import.ImportedTraps,
import.ImportedHoardCoffers)); import.ImportedHoardCoffers));
} }
@ -101,9 +105,9 @@ namespace Pal.Client.Scheduled
return false; return false;
} }
if (!Guid.TryParse(import.Export.ExportId, out Guid exportId) || import.ExportId == Guid.Empty) if (!Guid.TryParse(import.Export.ExportId, out Guid exportId) || exportId == Guid.Empty)
{ {
PluginLog.Error("Import: Invalid export id"); PluginLog.Error($"Import: Invalid export id ({import.Export.ExportId})");
_chatGui.PalError(Localization.Error_ImportFailed_InvalidFile); _chatGui.PalError(Localization.Error_ImportFailed_InvalidFile);
return false; return false;
} }

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using Pal.Client.Configuration; using Pal.Client.Configuration;
using Pal.Client.DependencyInjection; using Pal.Client.DependencyInjection;
using Pal.Client.Windows;
using Pal.Common; using Pal.Common;
namespace Pal.Client.Scheduled namespace Pal.Client.Scheduled
@ -17,13 +18,15 @@ namespace Pal.Client.Scheduled
internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedUndoImport> internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedUndoImport>
{ {
private readonly IPalacePalConfiguration _configuration; private readonly ImportService _importService;
private readonly FloorService _floorService; private readonly FloorService _floorService;
private readonly ConfigWindow _configWindow;
public Handler(IPalacePalConfiguration configuration, FloorService floorService) public Handler(ImportService importService, FloorService floorService, ConfigWindow configWindow)
{ {
_configuration = configuration; _importService = importService;
_floorService = floorService; _floorService = floorService;
_configWindow = configWindow;
} }
protected override void Run(QueuedUndoImport queued, ref bool recreateLayout, ref bool saveMarkers) protected override void Run(QueuedUndoImport queued, ref bool recreateLayout, ref bool saveMarkers)
@ -38,7 +41,8 @@ namespace Pal.Client.Scheduled
localState.Save(); localState.Save();
} }
_configuration.ImportHistory.RemoveAll(hist => hist.Id == queued.ExportId); _importService.RemoveById(queued.ExportId);
_configWindow.UpdateLastImport();
} }
} }
} }

View File

@ -21,6 +21,7 @@ using System.Threading.Tasks;
using Dalamud.Game.Gui; using Dalamud.Game.Gui;
using Pal.Client.Properties; using Pal.Client.Properties;
using Pal.Client.Configuration; using Pal.Client.Configuration;
using Pal.Client.Database;
using Pal.Client.DependencyInjection; using Pal.Client.DependencyInjection;
using Pal.Client.Extensions; using Pal.Client.Extensions;
@ -40,6 +41,7 @@ namespace Pal.Client.Windows
private readonly DebugState _debugState; private readonly DebugState _debugState;
private readonly ChatGui _chatGui; private readonly ChatGui _chatGui;
private readonly RemoteApi _remoteApi; private readonly RemoteApi _remoteApi;
private readonly ImportService _importService;
private int _mode; private int _mode;
private int _renderer; private int _renderer;
@ -55,6 +57,7 @@ namespace Pal.Client.Windows
private string? _saveExportDialogStartPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); private string? _saveExportDialogStartPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
private readonly FileDialogManager _importDialog; private readonly FileDialogManager _importDialog;
private readonly FileDialogManager _exportDialog; private readonly FileDialogManager _exportDialog;
private ImportHistory? _lastImport;
private CancellationTokenSource? _testConnectionCts; private CancellationTokenSource? _testConnectionCts;
@ -68,7 +71,8 @@ namespace Pal.Client.Windows
FloorService floorService, FloorService floorService,
DebugState debugState, DebugState debugState,
ChatGui chatGui, ChatGui chatGui,
RemoteApi remoteApi) RemoteApi remoteApi,
ImportService importService)
: base(WindowId) : base(WindowId)
{ {
_windowSystem = windowSystem; _windowSystem = windowSystem;
@ -81,6 +85,7 @@ namespace Pal.Client.Windows
_debugState = debugState; _debugState = debugState;
_chatGui = chatGui; _chatGui = chatGui;
_remoteApi = remoteApi; _remoteApi = remoteApi;
_importService = importService;
LanguageChanged(); LanguageChanged();
@ -117,6 +122,8 @@ namespace Pal.Client.Windows
_hoardConfig = new ConfigurableMarker(_configuration.DeepDungeons.HoardCoffers); _hoardConfig = new ConfigurableMarker(_configuration.DeepDungeons.HoardCoffers);
_silverConfig = new ConfigurableMarker(_configuration.DeepDungeons.SilverCoffers); _silverConfig = new ConfigurableMarker(_configuration.DeepDungeons.SilverCoffers);
_connectionText = null; _connectionText = null;
UpdateLastImport();
} }
public override void OnClose() public override void OnClose()
@ -278,13 +285,14 @@ namespace Pal.Client.Windows
DoImport(_openImportPath); DoImport(_openImportPath);
ImGui.EndDisabled(); ImGui.EndDisabled();
var importHistory = _configuration.ImportHistory.OrderByDescending(x => x.ImportedAt) ImportHistory? importHistory = _lastImport;
.ThenBy(x => x.Id).FirstOrDefault();
if (importHistory != null) if (importHistory != null)
{ {
ImGui.Separator(); ImGui.Separator();
ImGui.TextWrapped(string.Format(Localization.Config_UndoImportExplanation1, ImGui.TextWrapped(string.Format(Localization.Config_UndoImportExplanation1,
importHistory.ImportedAt, importHistory.RemoteUrl, importHistory.ExportedAt)); importHistory.ImportedAt.ToLocalTime(),
importHistory.RemoteUrl,
importHistory.ExportedAt.ToUniversalTime()));
ImGui.TextWrapped(Localization.Config_UndoImportExplanation2); ImGui.TextWrapped(Localization.Config_UndoImportExplanation2);
if (ImGui.Button(Localization.Config_UndoImport)) if (ImGui.Button(Localization.Config_UndoImport))
UndoImport(importHistory.Id); UndoImport(importHistory.Id);
@ -466,6 +474,11 @@ namespace Pal.Client.Windows
_frameworkService.EarlyEventQueue.Enqueue(new QueuedUndoImport(importId)); _frameworkService.EarlyEventQueue.Enqueue(new QueuedUndoImport(importId));
} }
internal void UpdateLastImport()
{
_lastImport = _importService.FindLast();
}
private void DoExport(string destinationPath) private void DoExport(string destinationPath)
{ {
Task.Run(async () => Task.Run(async () =>