Compare commits

..

No commits in common. "master" and "v4.4" have entirely different histories.
master ... v4.4

99 changed files with 5360 additions and 5107 deletions

View File

@ -4,6 +4,4 @@
.gitignore .gitignore
Dockerfile Dockerfile
.dockerignore .dockerignore
docker-build.sh
.vs/ .vs/
.idea/

View File

@ -21,7 +21,7 @@ resharper_indent_text = ZeroIndent
csharp_style_expression_bodied_methods = true csharp_style_expression_bodied_methods = true
# namespaces # namespaces
csharp_style_namespace_declarations = file_scoped csharp_style_namespace_declarations = block_scoped
# braces # braces
csharp_prefer_braces = when_multiline csharp_prefer_braces = when_multiline

45
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,45 @@
name: dotnet build
on:
push:
paths-ignore:
- '**.md'
- 'Dockerfile'
jobs:
build:
runs-on: windows-latest
env:
DOTNET_CLI_TELEMETRY_OPTOUT: 'true'
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
steps:
- uses: actions/checkout@v3
with:
submodules: true
- name: Setup .NET SDK
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7.0
- name: Download Dalamud
run: |
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip
Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev\"
- id: cache-dependencies
uses: actions/cache@v3
with:
path: ${{ github.workspace }}/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
restore-keys: |
${{ runner.os }}-nuget-
- name: Install dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --no-restore --verbosity normal
timeout-minutes: 10

31
.github/workflows/server.yml vendored Normal file
View File

@ -0,0 +1,31 @@
name: docker build
on:
push:
branches:
- master
paths:
- '.github/workflows/server.yml'
- 'Pal.Common/**'
- 'Pal.Server/**'
- 'Dockerfile'
workflow_dispatch: { }
permissions:
packages: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Login to GitHub Package Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
push: true
tags: ghcr.io/${{ github.repository_owner }}/palace-pal:latest

24
.github/workflows/upload-crowdin.yml vendored Normal file
View File

@ -0,0 +1,24 @@
name: Upload to Crowdin
on:
push:
branches:
- master
paths:
- 'Pal.Client/Properties/*.resx'
- 'crowdin.yml'
workflow_dispatch: { }
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Crowdin Action
uses: crowdin/github-action@1.4.9
with:
upload_sources: true
upload_translations: true
download_translations: false
crowdin_branch_name: ${{ github.ref_name }}
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

6
.gitmodules vendored
View File

@ -1,9 +1,3 @@
[submodule "vendor/ECommons"] [submodule "vendor/ECommons"]
path = vendor/ECommons path = vendor/ECommons
url = https://github.com/NightmareXIV/ECommons url = https://github.com/NightmareXIV/ECommons
[submodule "vendor/LLib"]
path = vendor/LLib
url = https://git.carvel.li/liza/LLib.git
[submodule "Server"]
path = Server
url = https://git.carvel.li/liza/PalacePal.Server.git

View File

@ -1,19 +1,13 @@
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build-env
ARG TARGETARCH
WORKDIR /build WORKDIR /build
COPY Pal.Common/Pal.Common.csproj Pal.Common/ COPY Pal.Common/Pal.Common.csproj Pal.Common/
COPY Server/Server/Pal.Server.csproj Server/Server/ COPY Pal.Server/Pal.Server.csproj Pal.Server/
RUN dotnet restore Server/Server/Pal.Server.csproj -a $TARGETARCH RUN dotnet restore Pal.Server/Pal.Server.csproj
COPY . ./ COPY . ./
RUN dotnet publish Server/Server/Pal.Server.csproj -a $TARGETARCH --no-restore -o /dist RUN dotnet publish Pal.Server/Pal.Server.csproj --configuration Release --no-restore -o /dist
FROM mcr.microsoft.com/dotnet/aspnet:8.0
# fix later
ENV DOTNET_ROLL_FORWARD=Major
ENV DOTNET_ROLL_FORWARD_PRE_RELEASE=1
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS runtime
EXPOSE 5415 EXPOSE 5415
ENV DOTNET_ENVIRONMENT=Production ENV DOTNET_ENVIRONMENT=Production
ENV ASPNETCORE_URLS= ENV ASPNETCORE_URLS=
@ -27,4 +21,4 @@ WORKDIR /app
COPY --from=build-env /dist . COPY --from=build-env /dist .
USER pal USER pal
ENTRYPOINT ["dotnet", "Pal.Server.dll"] ENTRYPOINT ["dotnet", "Pal.Server.dll"]

View File

@ -1,9 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace Pal.Client.Commands; namespace Pal.Client.Commands
public interface ISubCommand
{ {
IReadOnlyDictionary<string, Action<string>> GetHandlers(); public interface ISubCommand
{
IReadOnlyDictionary<string, Action<string>> GetHandlers();
}
} }

View File

@ -3,37 +3,38 @@ using System.Collections.Generic;
using Pal.Client.Configuration; using Pal.Client.Configuration;
using Pal.Client.Windows; using Pal.Client.Windows;
namespace Pal.Client.Commands; namespace Pal.Client.Commands
internal class PalConfigCommand : ISubCommand
{ {
private readonly IPalacePalConfiguration _configuration; internal class PalConfigCommand : ISubCommand
private readonly AgreementWindow _agreementWindow;
private readonly ConfigWindow _configWindow;
public PalConfigCommand(
IPalacePalConfiguration configuration,
AgreementWindow agreementWindow,
ConfigWindow configWindow)
{ {
_configuration = configuration; private readonly IPalacePalConfiguration _configuration;
_agreementWindow = agreementWindow; private readonly AgreementWindow _agreementWindow;
_configWindow = configWindow; private readonly ConfigWindow _configWindow;
}
public PalConfigCommand(
public IReadOnlyDictionary<string, Action<string>> GetHandlers() IPalacePalConfiguration configuration,
=> new Dictionary<string, Action<string>> AgreementWindow agreementWindow,
ConfigWindow configWindow)
{ {
{ "config", _ => Execute() }, _configuration = configuration;
{ "", _ => Execute() } _agreementWindow = agreementWindow;
}; _configWindow = configWindow;
}
public void Execute()
{ public IReadOnlyDictionary<string, Action<string>> GetHandlers()
if (_configuration.FirstUse) => new Dictionary<string, Action<string>>
_agreementWindow.IsOpen = true; {
else { "config", _ => Execute() },
_configWindow.Toggle(); { "", _ => Execute() }
};
public void Execute()
{
if (_configuration.FirstUse)
_agreementWindow.IsOpen = true;
else
_configWindow.Toggle();
}
} }
} }

View File

@ -1,62 +1,63 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Dalamud.Plugin.Services; using Dalamud.Game.ClientState;
using Pal.Client.DependencyInjection; using Pal.Client.DependencyInjection;
using Pal.Client.Extensions; using Pal.Client.Extensions;
using Pal.Client.Floors; using Pal.Client.Floors;
using Pal.Client.Rendering; using Pal.Client.Rendering;
namespace Pal.Client.Commands; namespace Pal.Client.Commands
internal sealed class PalNearCommand : ISubCommand
{ {
private readonly Chat _chat; internal sealed class PalNearCommand : ISubCommand
private readonly IClientState _clientState;
private readonly TerritoryState _territoryState;
private readonly FloorService _floorService;
public PalNearCommand(Chat chat, IClientState clientState, TerritoryState territoryState,
FloorService floorService)
{ {
_chat = chat; private readonly Chat _chat;
_clientState = clientState; private readonly ClientState _clientState;
_territoryState = territoryState; private readonly TerritoryState _territoryState;
_floorService = floorService; private readonly FloorService _floorService;
}
public PalNearCommand(Chat chat, ClientState clientState, TerritoryState territoryState,
public IReadOnlyDictionary<string, Action<string>> GetHandlers() FloorService floorService)
=> new Dictionary<string, Action<string>>
{ {
{ "near", _ => DebugNearest(_ => true) }, _chat = chat;
{ "tnear", _ => DebugNearest(m => m.Type == MemoryLocation.EType.Trap) }, _clientState = clientState;
{ "hnear", _ => DebugNearest(m => m.Type == MemoryLocation.EType.Hoard) }, _territoryState = territoryState;
}; _floorService = floorService;
}
private void DebugNearest(Predicate<PersistentLocation> predicate)
{
if (!_territoryState.IsInDeepDungeon())
return;
var state = _floorService.GetTerritoryIfReady(_clientState.TerritoryType); public IReadOnlyDictionary<string, Action<string>> GetHandlers()
if (state == null) => new Dictionary<string, Action<string>>
return; {
{ "near", _ => DebugNearest(_ => true) },
{ "tnear", _ => DebugNearest(m => m.Type == MemoryLocation.EType.Trap) },
{ "hnear", _ => DebugNearest(m => m.Type == MemoryLocation.EType.Hoard) },
};
var playerPosition = _clientState.LocalPlayer?.Position; private void DebugNearest(Predicate<PersistentLocation> predicate)
if (playerPosition == null) {
return; if (!_territoryState.IsInDeepDungeon())
_chat.Message($"Your position: {playerPosition}"); return;
var nearbyMarkers = state.Locations var state = _floorService.GetTerritoryIfReady(_clientState.TerritoryType);
.Where(m => predicate(m)) if (state == null)
.Where(m => m.RenderElement != null && m.RenderElement.Enabled) return;
.Select(m => new { m, distance = (playerPosition.Value - m.Position).Length() })
.OrderBy(m => m.distance) var playerPosition = _clientState.LocalPlayer?.Position;
.Take(5) if (playerPosition == null)
.ToList(); return;
foreach (var nearbyMarker in nearbyMarkers) _chat.Message($"Your position: {playerPosition}");
_chat.UnformattedMessage(
$"{nearbyMarker.distance:F2} - {nearbyMarker.m.Type} {nearbyMarker.m.NetworkId?.ToPartialId(length: 8)} - {nearbyMarker.m.Position}"); var nearbyMarkers = state.Locations
.Where(m => predicate(m))
.Where(m => m.RenderElement != null && m.RenderElement.Color != RenderData.ColorInvisible)
.Select(m => new { m, distance = (playerPosition.Value - m.Position).Length() })
.OrderBy(m => m.distance)
.Take(5)
.ToList();
foreach (var nearbyMarker in nearbyMarkers)
_chat.UnformattedMessage(
$"{nearbyMarker.distance:F2} - {nearbyMarker.m.Type} {nearbyMarker.m.NetworkId?.ToPartialId(length: 8)} - {nearbyMarker.m.Position}");
}
} }
} }

View File

@ -2,23 +2,24 @@
using System.Collections.Generic; using System.Collections.Generic;
using Pal.Client.DependencyInjection; using Pal.Client.DependencyInjection;
namespace Pal.Client.Commands; namespace Pal.Client.Commands
internal sealed class PalStatsCommand : ISubCommand
{ {
private readonly StatisticsService _statisticsService; internal sealed class PalStatsCommand : ISubCommand
public PalStatsCommand(StatisticsService statisticsService)
{ {
_statisticsService = statisticsService; private readonly StatisticsService _statisticsService;
}
public IReadOnlyDictionary<string, Action<string>> GetHandlers() public PalStatsCommand(StatisticsService statisticsService)
=> new Dictionary<string, Action<string>>
{ {
{ "stats", _ => Execute() }, _statisticsService = statisticsService;
}; }
private void Execute() public IReadOnlyDictionary<string, Action<string>> GetHandlers()
=> _statisticsService.ShowGlobalStatistics(); => new Dictionary<string, Action<string>>
{
{ "stats", _ => Execute() },
};
private void Execute()
=> _statisticsService.ShowGlobalStatistics();
}
} }

View File

@ -3,27 +3,28 @@ using System.Collections.Generic;
using ECommons.Schedulers; using ECommons.Schedulers;
using Pal.Client.Windows; using Pal.Client.Windows;
namespace Pal.Client.Commands; namespace Pal.Client.Commands
internal sealed class PalTestConnectionCommand : ISubCommand
{ {
private readonly ConfigWindow _configWindow; internal sealed class PalTestConnectionCommand : ISubCommand
public PalTestConnectionCommand(ConfigWindow configWindow)
{ {
_configWindow = configWindow; private readonly ConfigWindow _configWindow;
}
public IReadOnlyDictionary<string, Action<string>> GetHandlers() public PalTestConnectionCommand(ConfigWindow configWindow)
=> new Dictionary<string, Action<string>>
{ {
{ "test-connection", _ => Execute() }, _configWindow = configWindow;
{ "tc", _ => Execute() }, }
};
private void Execute() public IReadOnlyDictionary<string, Action<string>> GetHandlers()
{ => new Dictionary<string, Action<string>>
_configWindow.IsOpen = true; {
var _ = new TickScheduler(() => _configWindow.TestConnection()); { "test-connection", _ => Execute() },
{ "tc", _ => Execute() },
};
private void Execute()
{
_configWindow.IsOpen = true;
var _ = new TickScheduler(() => _configWindow.TestConnection());
}
} }
} }

View File

@ -4,141 +4,146 @@ using System.Security.Cryptography;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Pal.Client.Configuration; namespace Pal.Client.Configuration
public sealed class AccountConfigurationV7 : IAccountConfiguration
{ {
private const int DefaultEntropyLength = 16; public sealed class AccountConfigurationV7 : IAccountConfiguration
[JsonConstructor]
public AccountConfigurationV7()
{ {
} private const int DefaultEntropyLength = 16;
public AccountConfigurationV7(string server, Guid accountId) private static readonly ILogger _logger =
{ DependencyInjectionContext.LoggerProvider.CreateLogger<AccountConfigurationV7>();
Server = server;
(EncryptedId, Entropy, Format) = EncryptAccountId(accountId);
}
[Obsolete("for V1 import")] [JsonConstructor]
public AccountConfigurationV7(string server, string accountId) public AccountConfigurationV7()
{
Server = server;
if (accountId.StartsWith("s:"))
{ {
EncryptedId = accountId.Substring(2);
Entropy = ConfigurationData.FixedV1Entropy;
Format = EFormat.UseProtectedData;
EncryptIfNeeded();
}
else if (Guid.TryParse(accountId, out Guid guid))
(EncryptedId, Entropy, Format) = EncryptAccountId(guid);
else
throw new InvalidOperationException($"Invalid account id format, can't migrate account for server {server}");
}
[JsonInclude]
[JsonRequired]
public EFormat Format { get; private set; } = EFormat.Unencrypted;
/// <summary>
/// Depending on <see cref="Format"/>, this is either a Guid as string or a base64 encoded byte array.
/// </summary>
[JsonPropertyName("Id")]
[JsonInclude]
[JsonRequired]
public string EncryptedId { get; private set; } = null!;
[JsonInclude]
public byte[]? Entropy { get; private set; }
[JsonRequired]
public string Server { get; init; } = null!;
[JsonIgnore] public bool IsUsable => DecryptAccountId() != null;
[JsonIgnore] public Guid AccountId => DecryptAccountId() ?? throw new InvalidOperationException("Account id can't be read");
public List<string> CachedRoles { get; set; } = new();
private Guid? DecryptAccountId()
{
if (Format == EFormat.UseProtectedData && ConfigurationData.SupportsDpapi)
{
try
{
byte[] guidBytes = ProtectedData.Unprotect(Convert.FromBase64String(EncryptedId), Entropy, DataProtectionScope.CurrentUser);
return new Guid(guidBytes);
}
catch (Exception)
{
return null;
}
}
else if (Format == EFormat.Unencrypted)
return Guid.Parse(EncryptedId);
else if (Format == EFormat.ProtectedDataUnsupported && !ConfigurationData.SupportsDpapi)
return Guid.Parse(EncryptedId);
else
return null;
}
private (string encryptedId, byte[]? entropy, EFormat format) EncryptAccountId(Guid g)
{
if (!ConfigurationData.SupportsDpapi)
return (g.ToString(), null, EFormat.ProtectedDataUnsupported);
else
{
try
{
byte[] entropy = RandomNumberGenerator.GetBytes(DefaultEntropyLength);
byte[] guidBytes = ProtectedData.Protect(g.ToByteArray(), entropy, DataProtectionScope.CurrentUser);
return (Convert.ToBase64String(guidBytes), entropy, EFormat.UseProtectedData);
}
catch (Exception)
{
return (g.ToString(), null, EFormat.Unencrypted);
}
}
}
public bool EncryptIfNeeded()
{
if (Format == EFormat.Unencrypted)
{
var (newId, newEntropy, newFormat) = EncryptAccountId(Guid.Parse(EncryptedId));
if (newFormat != EFormat.Unencrypted)
{
EncryptedId = newId;
Entropy = newEntropy;
Format = newFormat;
return true;
}
}
else if (Format == EFormat.UseProtectedData && Entropy is { Length: < DefaultEntropyLength })
{
Guid? g = DecryptAccountId();
if (g != null)
{
(EncryptedId, Entropy, Format) = EncryptAccountId(g.Value);
return true;
}
} }
return false; public AccountConfigurationV7(string server, Guid accountId)
} {
Server = server;
(EncryptedId, Entropy, Format) = EncryptAccountId(accountId);
}
public enum EFormat [Obsolete("for V1 import")]
{ public AccountConfigurationV7(string server, string accountId)
Unencrypted = 1, {
UseProtectedData = 2, Server = server;
if (accountId.StartsWith("s:"))
{
EncryptedId = accountId.Substring(2);
Entropy = ConfigurationData.FixedV1Entropy;
Format = EFormat.UseProtectedData;
EncryptIfNeeded();
}
else if (Guid.TryParse(accountId, out Guid guid))
(EncryptedId, Entropy, Format) = EncryptAccountId(guid);
else
throw new InvalidOperationException($"Invalid account id format, can't migrate account for server {server}");
}
[JsonInclude]
[JsonRequired]
public EFormat Format { get; private set; } = EFormat.Unencrypted;
/// <summary> /// <summary>
/// Used for filtering: We don't want to overwrite any entries of this value using DPAPI, ever. /// Depending on <see cref="Format"/>, this is either a Guid as string or a base64 encoded byte array.
/// This is mostly a wine fallback.
/// </summary> /// </summary>
ProtectedDataUnsupported = 3, [JsonPropertyName("Id")]
[JsonInclude]
[JsonRequired]
public string EncryptedId { get; private set; } = null!;
[JsonInclude]
public byte[]? Entropy { get; private set; }
[JsonRequired]
public string Server { get; init; } = null!;
[JsonIgnore] public bool IsUsable => DecryptAccountId() != null;
[JsonIgnore] public Guid AccountId => DecryptAccountId() ?? throw new InvalidOperationException("Account id can't be read");
public List<string> CachedRoles { get; set; } = new();
private Guid? DecryptAccountId()
{
if (Format == EFormat.UseProtectedData && ConfigurationData.SupportsDpapi)
{
try
{
byte[] guidBytes = ProtectedData.Unprotect(Convert.FromBase64String(EncryptedId), Entropy, DataProtectionScope.CurrentUser);
return new Guid(guidBytes);
}
catch (Exception e)
{
_logger.LogTrace(e, "Could not load account id {Id}", EncryptedId);
return null;
}
}
else if (Format == EFormat.Unencrypted)
return Guid.Parse(EncryptedId);
else if (Format == EFormat.ProtectedDataUnsupported && !ConfigurationData.SupportsDpapi)
return Guid.Parse(EncryptedId);
else
return null;
}
private (string encryptedId, byte[]? entropy, EFormat format) EncryptAccountId(Guid g)
{
if (!ConfigurationData.SupportsDpapi)
return (g.ToString(), null, EFormat.ProtectedDataUnsupported);
else
{
try
{
byte[] entropy = RandomNumberGenerator.GetBytes(DefaultEntropyLength);
byte[] guidBytes = ProtectedData.Protect(g.ToByteArray(), entropy, DataProtectionScope.CurrentUser);
return (Convert.ToBase64String(guidBytes), entropy, EFormat.UseProtectedData);
}
catch (Exception)
{
return (g.ToString(), null, EFormat.Unencrypted);
}
}
}
public bool EncryptIfNeeded()
{
if (Format == EFormat.Unencrypted)
{
var (newId, newEntropy, newFormat) = EncryptAccountId(Guid.Parse(EncryptedId));
if (newFormat != EFormat.Unencrypted)
{
EncryptedId = newId;
Entropy = newEntropy;
Format = newFormat;
return true;
}
}
else if (Format == EFormat.UseProtectedData && Entropy is { Length: < DefaultEntropyLength })
{
Guid? g = DecryptAccountId();
if (g != null)
{
(EncryptedId, Entropy, Format) = EncryptAccountId(g.Value);
return true;
}
}
return false;
}
public enum EFormat
{
Unencrypted = 1,
UseProtectedData = 2,
/// <summary>
/// Used for filtering: We don't want to overwrite any entries of this value using DPAPI, ever.
/// This is mostly a wine fallback.
/// </summary>
ProtectedDataUnsupported = 3,
}
} }
} }

View File

@ -3,38 +3,42 @@ using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Pal.Client.Configuration; namespace Pal.Client.Configuration
internal static class ConfigurationData
{ {
[Obsolete("for V1 import")] internal static class ConfigurationData
internal static readonly byte[] FixedV1Entropy = { 0x22, 0x4b, 0xe7, 0x21, 0x44, 0x83, 0x69, 0x55, 0x80, 0x38 };
public const string ConfigFileName = "palace-pal.config.json";
private static bool? _supportsDpapi;
public static bool SupportsDpapi
{ {
get private static readonly ILogger _logger =
{ DependencyInjectionContext.LoggerProvider.CreateLogger(typeof(ConfigurationData));
if (_supportsDpapi == null)
{
try
{
byte[] input = RandomNumberGenerator.GetBytes(32);
byte[] entropy = RandomNumberGenerator.GetBytes(16);
byte[] temp = ProtectedData.Protect(input, entropy, DataProtectionScope.CurrentUser);
byte[] output = ProtectedData.Unprotect(temp, entropy, DataProtectionScope.CurrentUser);
_supportsDpapi = input.SequenceEqual(output);
}
catch (Exception)
{
_supportsDpapi = false;
}
}
return _supportsDpapi.Value; [Obsolete("for V1 import")]
internal static readonly byte[] FixedV1Entropy = { 0x22, 0x4b, 0xe7, 0x21, 0x44, 0x83, 0x69, 0x55, 0x80, 0x38 };
public const string ConfigFileName = "palace-pal.config.json";
private static bool? _supportsDpapi = null;
public static bool SupportsDpapi
{
get
{
if (_supportsDpapi == null)
{
try
{
byte[] input = RandomNumberGenerator.GetBytes(32);
byte[] entropy = RandomNumberGenerator.GetBytes(16);
byte[] temp = ProtectedData.Protect(input, entropy, DataProtectionScope.CurrentUser);
byte[] output = ProtectedData.Unprotect(temp, entropy, DataProtectionScope.CurrentUser);
_supportsDpapi = input.SequenceEqual(output);
}
catch (Exception)
{
_supportsDpapi = false;
}
_logger.LogTrace("DPAPI support: {Supported}", _supportsDpapi);
}
return _supportsDpapi.Value;
}
} }
} }
} }

View File

@ -13,158 +13,144 @@ using Pal.Client.Configuration.Legacy;
using Pal.Client.Database; using Pal.Client.Database;
using NJson = Newtonsoft.Json; using NJson = Newtonsoft.Json;
namespace Pal.Client.Configuration; namespace Pal.Client.Configuration
internal sealed class ConfigurationManager
{ {
private readonly ILogger<ConfigurationManager> _logger; internal sealed class ConfigurationManager
private readonly IDalamudPluginInterface _pluginInterface;
private readonly IServiceProvider _serviceProvider;
public event EventHandler<IPalacePalConfiguration>? Saved;
public ConfigurationManager(ILogger<ConfigurationManager> logger, IDalamudPluginInterface pluginInterface,
IServiceProvider serviceProvider)
{ {
_logger = logger; private readonly ILogger<ConfigurationManager> _logger;
_pluginInterface = pluginInterface; private readonly DalamudPluginInterface _pluginInterface;
_serviceProvider = serviceProvider; private readonly IServiceProvider _serviceProvider;
}
private string ConfigPath => public event EventHandler<IPalacePalConfiguration>? Saved;
Path.Join(_pluginInterface.GetPluginConfigDirectory(), ConfigurationData.ConfigFileName);
public IPalacePalConfiguration Load() public ConfigurationManager(ILogger<ConfigurationManager> logger, DalamudPluginInterface pluginInterface,
{ IServiceProvider serviceProvider)
if (!File.Exists(ConfigPath))
{ {
_logger.LogInformation("No config file exists, creating one"); _logger = logger;
Save(new ConfigurationV7(), false); _pluginInterface = pluginInterface;
_serviceProvider = serviceProvider;
} }
return JsonSerializer.Deserialize<ConfigurationV7>(File.ReadAllText(ConfigPath, Encoding.UTF8)) ?? private string ConfigPath =>
new ConfigurationV7(); Path.Join(_pluginInterface.GetPluginConfigDirectory(), ConfigurationData.ConfigFileName);
}
public void Save(IConfigurationInConfigDirectory config, bool queue = true) public IPalacePalConfiguration Load()
{ {
File.WriteAllText(ConfigPath, if (!File.Exists(ConfigPath))
JsonSerializer.Serialize(config, config.GetType(), {
new JsonSerializerOptions _logger.LogInformation("No config file exists, creating one");
{ WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }), Save(new ConfigurationV7(), false);
Encoding.UTF8); }
if (queue && config is ConfigurationV7 v7) return JsonSerializer.Deserialize<ConfigurationV7>(File.ReadAllText(ConfigPath, Encoding.UTF8)) ??
Saved?.Invoke(this, v7); new ConfigurationV7();
} }
public void Save(IConfigurationInConfigDirectory config, bool queue = true)
{
File.WriteAllText(ConfigPath,
JsonSerializer.Serialize(config, config.GetType(),
new JsonSerializerOptions
{ WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }),
Encoding.UTF8);
if (queue && config is ConfigurationV7 v7)
Saved?.Invoke(this, v7);
}
#pragma warning disable CS0612 #pragma warning disable CS0612
#pragma warning disable CS0618 #pragma warning disable CS0618
public void Migrate() public void Migrate()
{
if (_pluginInterface.ConfigFile.Exists)
{ {
_logger.LogInformation("Migrating config file from v1-v6 format"); if (_pluginInterface.ConfigFile.Exists)
ConfigurationV1 configurationV1 =
NJson.JsonConvert.DeserializeObject<ConfigurationV1>(
File.ReadAllText(_pluginInterface.ConfigFile.FullName)) ?? new ConfigurationV1();
configurationV1.Migrate(_pluginInterface,
_serviceProvider.GetRequiredService<ILogger<ConfigurationV1>>());
configurationV1.Save(_pluginInterface);
var v7 = MigrateToV7(configurationV1);
Save(v7, queue: false);
using (var scope = _serviceProvider.CreateScope())
{ {
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>(); _logger.LogInformation("Migrating config file from v1-v6 format");
dbContext.Imports.RemoveRange(dbContext.Imports);
foreach (var importHistory in configurationV1.ImportHistory) ConfigurationV1 configurationV1 =
NJson.JsonConvert.DeserializeObject<ConfigurationV1>(
File.ReadAllText(_pluginInterface.ConfigFile.FullName)) ?? new ConfigurationV1();
configurationV1.Migrate(_pluginInterface,
_serviceProvider.GetRequiredService<ILogger<ConfigurationV1>>());
configurationV1.Save(_pluginInterface);
var v7 = MigrateToV7(configurationV1);
Save(v7, queue: false);
using (var scope = _serviceProvider.CreateScope())
{ {
_logger.LogInformation("Migrating import {Id}", importHistory.Id); using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
dbContext.Imports.Add(new ImportHistory dbContext.Imports.RemoveRange(dbContext.Imports);
foreach (var importHistory in configurationV1.ImportHistory)
{ {
Id = importHistory.Id, _logger.LogInformation("Migrating import {Id}", importHistory.Id);
RemoteUrl = importHistory.RemoteUrl dbContext.Imports.Add(new ImportHistory
?.Replace(".μ.tv", ".liza.sh") {
.Replace("pal.liza.sh", "connect.palacepal.com"), Id = importHistory.Id,
ExportedAt = importHistory.ExportedAt, RemoteUrl = importHistory.RemoteUrl?.Replace(".μ.tv", ".liza.sh"),
ImportedAt = importHistory.ImportedAt ExportedAt = importHistory.ExportedAt,
}); ImportedAt = importHistory.ImportedAt
});
}
dbContext.SaveChanges();
} }
dbContext.SaveChanges(); File.Move(_pluginInterface.ConfigFile.FullName, _pluginInterface.ConfigFile.FullName + ".old", true);
} }
File.Move(_pluginInterface.ConfigFile.FullName, _pluginInterface.ConfigFile.FullName + ".old", true);
} }
IPalacePalConfiguration? currentConfig = Load(); private ConfigurationV7 MigrateToV7(ConfigurationV1 v1)
IAccountConfiguration? legacyAccount = currentConfig?.FindAccount("https://pal.liza.sh");
if (currentConfig != null && legacyAccount != null)
{ {
IAccountConfiguration newAccount = currentConfig.CreateAccount("https://connect.palacepal.com", legacyAccount.AccountId); ConfigurationV7 v7 = new()
newAccount.CachedRoles = legacyAccount.CachedRoles;
currentConfig.RemoveAccount(legacyAccount.Server);
Save(currentConfig, false);
}
}
private ConfigurationV7 MigrateToV7(ConfigurationV1 v1)
{
ConfigurationV7 v7 = new()
{
Version = 7,
FirstUse = v1.FirstUse,
Mode = v1.Mode,
BetaKey = v1.BetaKey,
DeepDungeons = new DeepDungeonConfiguration
{ {
Traps = new MarkerConfiguration Version = 7,
FirstUse = v1.FirstUse,
Mode = v1.Mode,
BetaKey = v1.BetaKey,
DeepDungeons = new DeepDungeonConfiguration
{ {
Show = v1.ShowTraps, Traps = new MarkerConfiguration
Color = ImGui.ColorConvertFloat4ToU32(v1.TrapColor), {
OnlyVisibleAfterPomander = v1.OnlyVisibleTrapsAfterPomander, Show = v1.ShowTraps,
Fill = false Color = ImGui.ColorConvertFloat4ToU32(v1.TrapColor),
}, OnlyVisibleAfterPomander = v1.OnlyVisibleTrapsAfterPomander,
HoardCoffers = new MarkerConfiguration Fill = false
{ },
Show = v1.ShowHoard, HoardCoffers = new MarkerConfiguration
Color = ImGui.ColorConvertFloat4ToU32(v1.HoardColor), {
OnlyVisibleAfterPomander = v1.OnlyVisibleHoardAfterPomander, Show = v1.ShowHoard,
Fill = false Color = ImGui.ColorConvertFloat4ToU32(v1.HoardColor),
}, OnlyVisibleAfterPomander = v1.OnlyVisibleHoardAfterPomander,
SilverCoffers = new MarkerConfiguration Fill = false
{ },
Show = v1.ShowSilverCoffers, SilverCoffers = new MarkerConfiguration
Color = ImGui.ColorConvertFloat4ToU32(v1.SilverCofferColor), {
OnlyVisibleAfterPomander = false, Show = v1.ShowSilverCoffers,
Fill = v1.FillSilverCoffers Color = ImGui.ColorConvertFloat4ToU32(v1.SilverCofferColor),
OnlyVisibleAfterPomander = false,
Fill = v1.FillSilverCoffers
}
} }
};
foreach (var (server, oldAccount) in v1.Accounts)
{
string? accountId = oldAccount.Id;
if (string.IsNullOrEmpty(accountId))
continue;
string serverName = server.Replace(".μ.tv", ".liza.sh");
IAccountConfiguration newAccount = v7.CreateAccount(serverName, accountId);
newAccount.CachedRoles = oldAccount.CachedRoles.ToList();
} }
};
foreach (var (server, oldAccount) in v1.Accounts) // TODO Migrate ImportHistory
{
string? accountId = oldAccount.Id;
if (string.IsNullOrEmpty(accountId))
continue;
string serverName = server return v7;
.Replace(".μ.tv", ".liza.sh")
.Replace("pal.liza.sh", "connect.palacepal.com");
IAccountConfiguration newAccount = v7.CreateAccount(serverName, accountId);
newAccount.CachedRoles = oldAccount.CachedRoles.ToList();
} }
// TODO Migrate ImportHistory
return v7;
}
#pragma warning restore CS0618 #pragma warning restore CS0618
#pragma warning restore CS0612 #pragma warning restore CS0612
}
} }

View File

@ -2,52 +2,53 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace Pal.Client.Configuration; namespace Pal.Client.Configuration
public sealed class ConfigurationV7 : IPalacePalConfiguration, IConfigurationInConfigDirectory
{ {
public int Version { get; set; } = 7; public sealed class ConfigurationV7 : IPalacePalConfiguration, IConfigurationInConfigDirectory
public bool FirstUse { get; set; } = true;
public EMode Mode { get; set; }
public string BetaKey { get; init; } = "";
public DeepDungeonConfiguration DeepDungeons { get; set; } = new();
public RendererConfiguration Renderer { get; set; } = new();
public List<AccountConfigurationV7> Accounts { get; set; } = new();
public BackupConfiguration Backups { get; set; } = new();
public IAccountConfiguration CreateAccount(string server, Guid accountId)
{ {
var account = new AccountConfigurationV7(server, accountId); public int Version { get; set; } = 7;
Accounts.Add(account);
return account;
}
[Obsolete("for V1 import")] public bool FirstUse { get; set; } = true;
internal IAccountConfiguration CreateAccount(string server, string accountId) public EMode Mode { get; set; }
{ public string BetaKey { get; init; } = "";
var account = new AccountConfigurationV7(server, accountId);
Accounts.Add(account);
return account;
}
public IAccountConfiguration? FindAccount(string server) public DeepDungeonConfiguration DeepDungeons { get; set; } = new();
{ public RendererConfiguration Renderer { get; set; } = new();
return Accounts.FirstOrDefault(a => a.Server == server && a.IsUsable); public List<AccountConfigurationV7> Accounts { get; set; } = new();
} public BackupConfiguration Backups { get; set; } = new();
public void RemoveAccount(string server) public IAccountConfiguration CreateAccount(string server, Guid accountId)
{ {
Accounts.RemoveAll(a => a.Server == server && a.IsUsable); var account = new AccountConfigurationV7(server, accountId);
} Accounts.Add(account);
return account;
}
public bool HasRoleOnCurrentServer(string server, string role) [Obsolete("for V1 import")]
{ internal IAccountConfiguration CreateAccount(string server, string accountId)
if (Mode != EMode.Online) {
return false; var account = new AccountConfigurationV7(server, accountId);
Accounts.Add(account);
return account;
}
var account = FindAccount(server); public IAccountConfiguration? FindAccount(string server)
return account == null || account.CachedRoles.Contains(role); {
return Accounts.FirstOrDefault(a => a.Server == server && a.IsUsable);
}
public void RemoveAccount(string server)
{
Accounts.RemoveAll(a => a.Server == server && a.IsUsable);
}
public bool HasRoleOnCurrentServer(string server, string role)
{
if (Mode != EMode.Online)
return false;
var account = FindAccount(server);
return account == null || account.CachedRoles.Contains(role);
}
} }
} }

View File

@ -1,14 +1,15 @@
namespace Pal.Client.Configuration; namespace Pal.Client.Configuration
public enum EMode
{ {
/// <summary> public enum EMode
/// Fetches trap locations from remote server. {
/// </summary> /// <summary>
Online = 1, /// Fetches trap locations from remote server.
/// </summary>
Online = 1,
/// <summary> /// <summary>
/// Only shows traps found by yourself using a pomander of sight. /// Only shows traps found by yourself using a pomander of sight.
/// </summary> /// </summary>
Offline = 2, Offline = 2,
}
} }

View File

@ -1,10 +1,11 @@
namespace Pal.Client.Configuration; namespace Pal.Client.Configuration
public enum ERenderer
{ {
/// <see cref="Rendering.SimpleRenderer"/> public enum ERenderer
Simple = 0, {
/// <see cref="Rendering.SimpleRenderer"/>
Simple = 0,
/// <see cref="Rendering.SplatoonRenderer"/> /// <see cref="Rendering.SplatoonRenderer"/>
Splatoon = 1, Splatoon = 1,
}
} }

View File

@ -4,107 +4,108 @@ using System.Numerics;
using ImGuiNET; using ImGuiNET;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Pal.Client.Configuration; namespace Pal.Client.Configuration
public interface IVersioned
{ {
int Version { get; set; } public interface IVersioned
}
public interface IConfigurationInConfigDirectory : IVersioned
{
}
public interface IPalacePalConfiguration : IConfigurationInConfigDirectory
{
bool FirstUse { get; set; }
EMode Mode { get; set; }
string BetaKey { get; }
bool HasBetaFeature(string feature) => BetaKey.Contains(feature);
DeepDungeonConfiguration DeepDungeons { get; set; }
RendererConfiguration Renderer { get; set; }
BackupConfiguration Backups { get; set; }
IAccountConfiguration CreateAccount(string server, Guid accountId);
IAccountConfiguration? FindAccount(string server);
void RemoveAccount(string server);
bool HasRoleOnCurrentServer(string server, string role);
}
public class DeepDungeonConfiguration
{
public MarkerConfiguration Traps { get; set; } = new()
{ {
Show = true, int Version { get; set; }
Color = ImGui.ColorConvertFloat4ToU32(new Vector4(1, 0, 0, 0.4f)), }
OnlyVisibleAfterPomander = true, public interface IConfigurationInConfigDirectory : IVersioned
Fill = false
};
public MarkerConfiguration HoardCoffers { get; set; } = new()
{ {
Show = true, }
Color = ImGui.ColorConvertFloat4ToU32(new Vector4(0, 1, 1, 0.4f)),
OnlyVisibleAfterPomander = true,
Fill = false
};
public MarkerConfiguration SilverCoffers { get; set; } = new() public interface IPalacePalConfiguration : IConfigurationInConfigDirectory
{ {
Show = false, bool FirstUse { get; set; }
Color = ImGui.ColorConvertFloat4ToU32(new Vector4(1, 1, 1, 0.4f)), EMode Mode { get; set; }
OnlyVisibleAfterPomander = false, string BetaKey { get; }
Fill = true bool HasBetaFeature(string feature) => BetaKey.Contains(feature);
};
public MarkerConfiguration GoldCoffers { get; set; } = new() DeepDungeonConfiguration DeepDungeons { get; set; }
RendererConfiguration Renderer { get; set; }
BackupConfiguration Backups { get; set; }
IAccountConfiguration CreateAccount(string server, Guid accountId);
IAccountConfiguration? FindAccount(string server);
void RemoveAccount(string server);
bool HasRoleOnCurrentServer(string server, string role);
}
public class DeepDungeonConfiguration
{ {
Show = false, public MarkerConfiguration Traps { get; set; } = new()
Color = ImGui.ColorConvertFloat4ToU32(new Vector4(1, 1, 0, 0.4f)), {
OnlyVisibleAfterPomander = false, Show = true,
Fill = true Color = ImGui.ColorConvertFloat4ToU32(new Vector4(1, 0, 0, 0.4f)),
}; OnlyVisibleAfterPomander = true,
} Fill = false
};
public class MarkerConfiguration
{ public MarkerConfiguration HoardCoffers { get; set; } = new()
[JsonRequired] {
public bool Show { get; set; } Show = true,
Color = ImGui.ColorConvertFloat4ToU32(new Vector4(0, 1, 1, 0.4f)),
[JsonRequired] OnlyVisibleAfterPomander = true,
public uint Color { get; set; } Fill = false
};
public bool OnlyVisibleAfterPomander { get; set; }
public bool Fill { get; set; } public MarkerConfiguration SilverCoffers { get; set; } = new()
} {
Show = false,
public class RendererConfiguration Color = ImGui.ColorConvertFloat4ToU32(new Vector4(1, 1, 1, 0.4f)),
{ OnlyVisibleAfterPomander = false,
public ERenderer SelectedRenderer { get; set; } = ERenderer.Splatoon; Fill = true
} };
public interface IAccountConfiguration public MarkerConfiguration GoldCoffers { get; set; } = new()
{ {
bool IsUsable { get; } Show = false,
string Server { get; } Color = ImGui.ColorConvertFloat4ToU32(new Vector4(1, 1, 0, 0.4f)),
Guid AccountId { get; } OnlyVisibleAfterPomander = false,
Fill = true
/// <summary> };
/// This is taken from the JWT, and is only refreshed on a successful login. }
///
/// If you simply reload the plugin without any server interaction, this doesn't change. public class MarkerConfiguration
/// {
/// This has no impact on what roles the JWT actually contains, but is just to make it [JsonRequired]
/// easier to draw a consistent UI. The server will still reject unauthorized calls. public bool Show { get; set; }
/// </summary>
List<string> CachedRoles { get; set; } [JsonRequired]
public uint Color { get; set; }
bool EncryptIfNeeded();
} public bool OnlyVisibleAfterPomander { get; set; }
public bool Fill { get; set; }
public class BackupConfiguration }
{
public int MinimumBackupsToKeep { get; set; } = 3; public class RendererConfiguration
public int DaysToDeleteAfter { get; set; } = 21; {
public ERenderer SelectedRenderer { get; set; } = ERenderer.Splatoon;
}
public interface IAccountConfiguration
{
bool IsUsable { get; }
string Server { get; }
Guid AccountId { get; }
/// <summary>
/// This is taken from the JWT, and is only refreshed on a successful login.
///
/// If you simply reload the plugin without any server interaction, this doesn't change.
///
/// This has no impact on what roles the JWT actually contains, but is just to make it
/// easier to draw a consistent UI. The server will still reject unauthorized calls.
/// </summary>
List<string> CachedRoles { get; set; }
bool EncryptIfNeeded();
}
public class BackupConfiguration
{
public int MinimumBackupsToKeep { get; set; } = 3;
public int DaysToDeleteAfter { get; set; } = 21;
}
} }

View File

@ -8,159 +8,160 @@ using Dalamud.Plugin;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Pal.Client.Configuration.Legacy; namespace Pal.Client.Configuration.Legacy
[Obsolete]
public sealed class ConfigurationV1
{ {
public int Version { get; set; } = 6;
#region Saved configuration values
public bool FirstUse { get; set; } = true;
public EMode Mode { get; set; } = EMode.Offline;
public ERenderer Renderer { get; set; } = ERenderer.Splatoon;
[Obsolete] [Obsolete]
public string? DebugAccountId { private get; set; } public sealed class ConfigurationV1
[Obsolete]
public string? AccountId { private get; set; }
[Obsolete]
public Dictionary<string, Guid> AccountIds { private get; set; } = new();
public Dictionary<string, AccountInfo> Accounts { get; set; } = new();
public List<ImportHistoryEntry> ImportHistory { get; set; } = new();
public bool ShowTraps { get; set; } = true;
public Vector4 TrapColor { get; set; } = new(1, 0, 0, 0.4f);
public bool OnlyVisibleTrapsAfterPomander { get; set; } = true;
public bool ShowHoard { get; set; } = true;
public Vector4 HoardColor { get; set; } = new(0, 1, 1, 0.4f);
public bool OnlyVisibleHoardAfterPomander { get; set; } = true;
public bool ShowSilverCoffers { get; set; }
public Vector4 SilverCofferColor { get; set; } = new(1, 1, 1, 0.4f);
public bool FillSilverCoffers { get; set; } = true;
/// <summary>
/// Needs to be manually set.
/// </summary>
public string BetaKey { get; set; } = "";
#endregion
public void Migrate(IDalamudPluginInterface pluginInterface, ILogger<ConfigurationV1> logger)
{ {
if (Version == 1) public int Version { get; set; } = 6;
{
logger.LogInformation("Updating config to version 2");
if (DebugAccountId != null && Guid.TryParse(DebugAccountId, out Guid debugAccountId)) #region Saved configuration values
AccountIds["http://localhost:5145"] = debugAccountId; public bool FirstUse { get; set; } = true;
public EMode Mode { get; set; } = EMode.Offline;
public ERenderer Renderer { get; set; } = ERenderer.Splatoon;
if (AccountId != null && Guid.TryParse(AccountId, out Guid accountId)) [Obsolete]
AccountIds["https://pal.μ.tv"] = accountId; public string? DebugAccountId { private get; set; }
Version = 2; [Obsolete]
Save(pluginInterface); public string? AccountId { private get; set; }
}
if (Version == 2) [Obsolete]
{ public Dictionary<string, Guid> AccountIds { private get; set; } = new();
logger.LogInformation("Updating config to version 3"); public Dictionary<string, AccountInfo> Accounts { get; set; } = new();
Accounts = AccountIds.ToDictionary(x => x.Key, x => new AccountInfo public List<ImportHistoryEntry> ImportHistory { get; set; } = new();
{
Id = x.Value.ToString() // encryption happens in V7 migration at latest
});
Version = 3;
Save(pluginInterface);
}
if (Version == 3) public bool ShowTraps { get; set; } = true;
{ public Vector4 TrapColor { get; set; } = new(1, 0, 0, 0.4f);
Version = 4; public bool OnlyVisibleTrapsAfterPomander { get; set; } = true;
Save(pluginInterface);
}
if (Version == 4) public bool ShowHoard { get; set; } = true;
{ public Vector4 HoardColor { get; set; } = new(0, 1, 1, 0.4f);
// 2.2 had a bug that would mark chests as traps, there's no easy way to detect this -- or clean this up. public bool OnlyVisibleHoardAfterPomander { get; set; } = true;
// Not a problem for online players, but offline players might be fucked.
//bool changedAnyFile = false;
JsonFloorState.ForEach(s =>
{
foreach (var marker in s.Markers)
marker.SinceVersion = "0.0";
var lastModified = File.GetLastWriteTimeUtc(s.GetSaveLocation()); public bool ShowSilverCoffers { get; set; }
if (lastModified >= new DateTime(2023, 2, 3, 0, 0, 0, DateTimeKind.Utc)) public Vector4 SilverCofferColor { get; set; } = new(1, 1, 1, 0.4f);
{ public bool FillSilverCoffers { get; set; } = true;
s.Backup(suffix: "bak");
s.Markers = new ConcurrentBag<JsonMarker>(s.Markers.Where(m => m.SinceVersion != "0.0" || m.Type == JsonMarker.EType.Hoard || m.WasImported));
s.Save();
//changedAnyFile = true;
}
else
{
// just add version information, nothing else
s.Save();
}
});
/*
// Only notify offline users - we can just re-download the backup markers from the server seamlessly.
if (Mode == EMode.Offline && changedAnyFile)
{
_ = new TickScheduler(delegate
{
Service.Chat.PalError("Due to a bug, some coffers were accidentally saved as traps. To fix the related display issue, locally cached data was cleaned up.");
Service.Chat.PrintError($"If you have any backup tools installed, please restore the contents of '{Service.PluginInterface.GetPluginConfigDirectory()}' to any backup from February 2, 2023 or before.");
Service.Chat.PrintError("You can also manually restore .json.bak files (by removing the '.bak') if you have not been in any deep dungeon since February 2, 2023.");
}, 2500);
}
*/
Version = 5;
Save(pluginInterface);
}
if (Version == 5)
{
JsonFloorState.UpdateAll();
Version = 6;
Save(pluginInterface);
}
}
public void Save(IDalamudPluginInterface pluginInterface)
{
File.WriteAllText(pluginInterface.ConfigFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented, new JsonSerializerSettings
{
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
TypeNameHandling = TypeNameHandling.Objects
}));
}
public sealed class AccountInfo
{
public string? Id { get; set; }
public List<string> CachedRoles { get; set; } = new();
}
public sealed class ImportHistoryEntry
{
public Guid Id { get; set; }
public string? RemoteUrl { get; set; }
public DateTime ExportedAt { get; set; }
/// <summary> /// <summary>
/// Set when the file is imported locally. /// Needs to be manually set.
/// </summary> /// </summary>
public DateTime ImportedAt { get; set; } public string BetaKey { get; set; } = "";
#endregion
public void Migrate(DalamudPluginInterface pluginInterface, ILogger<ConfigurationV1> logger)
{
if (Version == 1)
{
logger.LogInformation("Updating config to version 2");
if (DebugAccountId != null && Guid.TryParse(DebugAccountId, out Guid debugAccountId))
AccountIds["http://localhost:5145"] = debugAccountId;
if (AccountId != null && Guid.TryParse(AccountId, out Guid accountId))
AccountIds["https://pal.μ.tv"] = accountId;
Version = 2;
Save(pluginInterface);
}
if (Version == 2)
{
logger.LogInformation("Updating config to version 3");
Accounts = AccountIds.ToDictionary(x => x.Key, x => new AccountInfo
{
Id = x.Value.ToString() // encryption happens in V7 migration at latest
});
Version = 3;
Save(pluginInterface);
}
if (Version == 3)
{
Version = 4;
Save(pluginInterface);
}
if (Version == 4)
{
// 2.2 had a bug that would mark chests as traps, there's no easy way to detect this -- or clean this up.
// Not a problem for online players, but offline players might be fucked.
//bool changedAnyFile = false;
JsonFloorState.ForEach(s =>
{
foreach (var marker in s.Markers)
marker.SinceVersion = "0.0";
var lastModified = File.GetLastWriteTimeUtc(s.GetSaveLocation());
if (lastModified >= new DateTime(2023, 2, 3, 0, 0, 0, DateTimeKind.Utc))
{
s.Backup(suffix: "bak");
s.Markers = new ConcurrentBag<JsonMarker>(s.Markers.Where(m => m.SinceVersion != "0.0" || m.Type == JsonMarker.EType.Hoard || m.WasImported));
s.Save();
//changedAnyFile = true;
}
else
{
// just add version information, nothing else
s.Save();
}
});
/*
// Only notify offline users - we can just re-download the backup markers from the server seamlessly.
if (Mode == EMode.Offline && changedAnyFile)
{
_ = new TickScheduler(delegate
{
Service.Chat.PalError("Due to a bug, some coffers were accidentally saved as traps. To fix the related display issue, locally cached data was cleaned up.");
Service.Chat.PrintError($"If you have any backup tools installed, please restore the contents of '{Service.PluginInterface.GetPluginConfigDirectory()}' to any backup from February 2, 2023 or before.");
Service.Chat.PrintError("You can also manually restore .json.bak files (by removing the '.bak') if you have not been in any deep dungeon since February 2, 2023.");
}, 2500);
}
*/
Version = 5;
Save(pluginInterface);
}
if (Version == 5)
{
JsonFloorState.UpdateAll();
Version = 6;
Save(pluginInterface);
}
}
public void Save(DalamudPluginInterface pluginInterface)
{
File.WriteAllText(pluginInterface.ConfigFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented, new JsonSerializerSettings
{
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
TypeNameHandling = TypeNameHandling.Objects
}));
}
public sealed class AccountInfo
{
public string? Id { get; set; }
public List<string> CachedRoles { get; set; } = new();
}
public sealed class ImportHistoryEntry
{
public Guid Id { get; set; }
public string? RemoteUrl { get; set; }
public DateTime ExportedAt { get; set; }
/// <summary>
/// Set when the file is imported locally.
/// </summary>
public DateTime ImportedAt { get; set; }
}
} }
} }

View File

@ -7,155 +7,164 @@ using System.Text.Json;
using Pal.Client.Extensions; using Pal.Client.Extensions;
using Pal.Common; using Pal.Common;
namespace Pal.Client.Configuration.Legacy; namespace Pal.Client.Configuration.Legacy
/// <summary>
/// Legacy JSON file for marker locations.
/// </summary>
[Obsolete]
public sealed class JsonFloorState
{ {
private static readonly JsonSerializerOptions JsonSerializerOptions = new() { IncludeFields = true }; /// <summary>
private const int CurrentVersion = 4; /// Legacy JSON file for marker locations.
/// </summary>
private static string _pluginConfigDirectory = null!; [Obsolete]
public sealed class JsonFloorState
internal static void SetContextProperties(string pluginConfigDirectory)
{ {
_pluginConfigDirectory = pluginConfigDirectory; private static readonly JsonSerializerOptions JsonSerializerOptions = new() { IncludeFields = true };
} private const int CurrentVersion = 4;
public ushort TerritoryType { get; set; } private static string _pluginConfigDirectory = null!;
public ConcurrentBag<JsonMarker> Markers { get; set; } = new();
public JsonFloorState(ushort territoryType) // might not be true, but this is 'less strict filtering' for migrations
{ private static readonly EMode _mode = EMode.Online;
TerritoryType = territoryType;
}
private void ApplyFilters() internal static void SetContextProperties(string pluginConfigDirectory)
{
Markers = new ConcurrentBag<JsonMarker>(Markers.Where(x => x.Seen || !x.WasImported || x.Imports.Count > 0));
}
public static JsonFloorState? Load(ushort territoryType)
{
string path = GetSaveLocation(territoryType);
if (!File.Exists(path))
return null;
string content = File.ReadAllText(path);
if (content.Length == 0)
return null;
JsonFloorState localState;
int version = 1;
if (content[0] == '[')
{ {
// v1 only had a list of markers, not a JSON object as root _pluginConfigDirectory = pluginConfigDirectory;
localState = new JsonFloorState(territoryType)
{
Markers = new ConcurrentBag<JsonMarker>(JsonSerializer.Deserialize<HashSet<JsonMarker>>(content, JsonSerializerOptions) ?? new()),
};
} }
else
public ushort TerritoryType { get; set; }
public ConcurrentBag<JsonMarker> Markers { get; set; } = new();
public JsonFloorState(ushort territoryType)
{ {
var save = JsonSerializer.Deserialize<SaveFile>(content, JsonSerializerOptions); TerritoryType = territoryType;
if (save == null) }
private void ApplyFilters()
{
if (_mode == EMode.Offline)
Markers = new ConcurrentBag<JsonMarker>(Markers.Where(x => x.Seen || (x.WasImported && x.Imports.Count > 0)));
else
// ensure old import markers are removed if they are no longer part of a "current" import
// this MAY remove markers the server sent you (and that you haven't seen), but this should be fixed the next time you enter the zone
Markers = new ConcurrentBag<JsonMarker>(Markers.Where(x => x.Seen || !x.WasImported || x.Imports.Count > 0));
}
public static JsonFloorState? Load(ushort territoryType)
{
string path = GetSaveLocation(territoryType);
if (!File.Exists(path))
return null; return null;
localState = new JsonFloorState(territoryType) string content = File.ReadAllText(path);
if (content.Length == 0)
return null;
JsonFloorState localState;
int version = 1;
if (content[0] == '[')
{ {
Markers = new ConcurrentBag<JsonMarker>(save.Markers.Where(o => o.Type == JsonMarker.EType.Trap || o.Type == JsonMarker.EType.Hoard)), // v1 only had a list of markers, not a JSON object as root
}; localState = new JsonFloorState(territoryType)
version = save.Version; {
Markers = new ConcurrentBag<JsonMarker>(JsonSerializer.Deserialize<HashSet<JsonMarker>>(content, JsonSerializerOptions) ?? new()),
};
}
else
{
var save = JsonSerializer.Deserialize<SaveFile>(content, JsonSerializerOptions);
if (save == null)
return null;
localState = new JsonFloorState(territoryType)
{
Markers = new ConcurrentBag<JsonMarker>(save.Markers.Where(o => o.Type == JsonMarker.EType.Trap || o.Type == JsonMarker.EType.Hoard)),
};
version = save.Version;
}
localState.ApplyFilters();
if (version <= 3)
{
foreach (var marker in localState.Markers)
marker.RemoteSeenOn = marker.RemoteSeenOn.Select(x => x.ToPartialId()).ToList();
}
if (version < CurrentVersion)
localState.Save();
return localState;
} }
localState.ApplyFilters(); public void Save()
if (version <= 3)
{ {
foreach (var marker in localState.Markers) string path = GetSaveLocation(TerritoryType);
marker.RemoteSeenOn = marker.RemoteSeenOn.Select(x => x.ToPartialId()).ToList();
}
if (version < CurrentVersion) ApplyFilters();
localState.Save();
return localState;
}
public void Save()
{
string path = GetSaveLocation(TerritoryType);
ApplyFilters();
SaveImpl(path);
}
public void Backup(string suffix)
{
string path = $"{GetSaveLocation(TerritoryType)}.{suffix}";
if (!File.Exists(path))
{
SaveImpl(path); SaveImpl(path);
} }
}
private void SaveImpl(string path) public void Backup(string suffix)
{
foreach (var marker in Markers)
{ {
if (string.IsNullOrEmpty(marker.SinceVersion)) string path = $"{GetSaveLocation(TerritoryType)}.{suffix}";
marker.SinceVersion = typeof(Plugin).Assembly.GetName().Version!.ToString(2); if (!File.Exists(path))
}
if (Markers.Count == 0)
File.Delete(path);
else
{
File.WriteAllText(path, JsonSerializer.Serialize(new SaveFile
{ {
Version = CurrentVersion, SaveImpl(path);
Markers = new HashSet<JsonMarker>(Markers) }
}, JsonSerializerOptions));
} }
}
public string GetSaveLocation() => GetSaveLocation(TerritoryType); private void SaveImpl(string path)
private static string GetSaveLocation(uint territoryType) => Path.Join(_pluginConfigDirectory, $"{territoryType}.json");
public static void ForEach(Action<JsonFloorState> action)
{
foreach (ETerritoryType territory in typeof(ETerritoryType).GetEnumValues())
{ {
// we never had markers for eureka orthos, so don't bother foreach (var marker in Markers)
if (territory > ETerritoryType.HeavenOnHigh_91_100) {
break; if (string.IsNullOrEmpty(marker.SinceVersion))
marker.SinceVersion = typeof(Plugin).Assembly.GetName().Version!.ToString(2);
}
JsonFloorState? localState = Load((ushort)territory); if (Markers.Count == 0)
if (localState != null) File.Delete(path);
action(localState); else
{
File.WriteAllText(path, JsonSerializer.Serialize(new SaveFile
{
Version = CurrentVersion,
Markers = new HashSet<JsonMarker>(Markers)
}, JsonSerializerOptions));
}
} }
}
public static void UpdateAll() public string GetSaveLocation() => GetSaveLocation(TerritoryType);
{
ForEach(s => s.Save());
}
public void UndoImport(List<Guid> importIds) private static string GetSaveLocation(uint territoryType) => Path.Join(_pluginConfigDirectory, $"{territoryType}.json");
{
// When saving a floor state, any markers not seen, not remote seen, and not having an import id are removed;
// so it is possible to remove "wrong" markers by not having them be in the current import.
foreach (var marker in Markers)
marker.Imports.RemoveAll(importIds.Contains);
}
public sealed class SaveFile public static void ForEach(Action<JsonFloorState> action)
{ {
public int Version { get; set; } foreach (ETerritoryType territory in typeof(ETerritoryType).GetEnumValues())
public HashSet<JsonMarker> Markers { get; set; } = new(); {
// we never had markers for eureka orthos, so don't bother
if (territory > ETerritoryType.HeavenOnHigh_91_100)
break;
JsonFloorState? localState = Load((ushort)territory);
if (localState != null)
action(localState);
}
}
public static void UpdateAll()
{
ForEach(s => s.Save());
}
public void UndoImport(List<Guid> importIds)
{
// When saving a floor state, any markers not seen, not remote seen, and not having an import id are removed;
// so it is possible to remove "wrong" markers by not having them be in the current import.
foreach (var marker in Markers)
marker.Imports.RemoveAll(importIds.Contains);
}
public sealed class SaveFile
{
public int Version { get; set; }
public HashSet<JsonMarker> Markers { get; set; } = new();
}
} }
} }

View File

@ -2,24 +2,25 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
namespace Pal.Client.Configuration.Legacy; namespace Pal.Client.Configuration.Legacy
[Obsolete]
public class JsonMarker
{ {
public EType Type { get; set; } = EType.Unknown; [Obsolete]
public Vector3 Position { get; set; } public class JsonMarker
public bool Seen { get; set; }
public List<string> RemoteSeenOn { get; set; } = new();
public List<Guid> Imports { get; set; } = new();
public bool WasImported { get; set; }
public string? SinceVersion { get; set; }
public enum EType
{ {
Unknown = 0, public EType Type { get; set; } = EType.Unknown;
Trap = 1, public Vector3 Position { get; set; }
Hoard = 2, public bool Seen { get; set; }
Debug = 3, public List<string> RemoteSeenOn { get; set; } = new();
public List<Guid> Imports { get; set; } = new();
public bool WasImported { get; set; }
public string? SinceVersion { get; set; }
public enum EType
{
Unknown = 0,
Trap = 1,
Hoard = 2,
Debug = 3,
}
} }
} }

View File

@ -12,135 +12,136 @@ using Microsoft.Extensions.Logging;
using Pal.Client.Database; using Pal.Client.Database;
using Pal.Common; using Pal.Common;
namespace Pal.Client.Configuration.Legacy; namespace Pal.Client.Configuration.Legacy
/// <summary>
/// Imports legacy territoryType.json files into the database if it exists, and no markers for that territory exist.
/// </summary>
internal sealed class JsonMigration
{ {
private readonly ILogger<JsonMigration> _logger; /// <summary>
private readonly IServiceScopeFactory _serviceScopeFactory; /// Imports legacy territoryType.json files into the database if it exists, and no markers for that territory exist.
private readonly IDalamudPluginInterface _pluginInterface; /// </summary>
internal sealed class JsonMigration
public JsonMigration(ILogger<JsonMigration> logger, IServiceScopeFactory serviceScopeFactory,
IDalamudPluginInterface pluginInterface)
{ {
_logger = logger; private readonly ILogger<JsonMigration> _logger;
_serviceScopeFactory = serviceScopeFactory; private readonly IServiceScopeFactory _serviceScopeFactory;
_pluginInterface = pluginInterface; private readonly DalamudPluginInterface _pluginInterface;
}
public JsonMigration(ILogger<JsonMigration> logger, IServiceScopeFactory serviceScopeFactory,
DalamudPluginInterface pluginInterface)
{
_logger = logger;
_serviceScopeFactory = serviceScopeFactory;
_pluginInterface = pluginInterface;
}
#pragma warning disable CS0612 #pragma warning disable CS0612
public async Task MigrateAsync(CancellationToken cancellationToken) public async Task MigrateAsync(CancellationToken cancellationToken)
{
List<JsonFloorState> floorsToMigrate = new();
JsonFloorState.ForEach(floorsToMigrate.Add);
if (floorsToMigrate.Count == 0)
{ {
_logger.LogInformation("Found no floors to migrate"); List<JsonFloorState> floorsToMigrate = new();
return; JsonFloorState.ForEach(floorsToMigrate.Add);
}
cancellationToken.ThrowIfCancellationRequested(); if (floorsToMigrate.Count == 0)
await using var scope = _serviceScopeFactory.CreateAsyncScope();
await using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
var fileStream = new FileStream(
Path.Join(_pluginInterface.GetPluginConfigDirectory(),
$"territory-backup-{DateTime.Now:yyyyMMdd-HHmmss}.zip"),
FileMode.CreateNew);
using (var backup = new ZipArchive(fileStream, ZipArchiveMode.Create, false))
{
IReadOnlyDictionary<Guid, ImportHistory> imports =
await dbContext.Imports.ToDictionaryAsync(import => import.Id, cancellationToken);
foreach (var floorToMigrate in floorsToMigrate)
{ {
backup.CreateEntryFromFile(floorToMigrate.GetSaveLocation(), _logger.LogInformation("Found no floors to migrate");
Path.GetFileName(floorToMigrate.GetSaveLocation()), CompressionLevel.SmallestSize); return;
await MigrateFloor(dbContext, floorToMigrate, imports, cancellationToken);
} }
await dbContext.SaveChangesAsync(cancellationToken); cancellationToken.ThrowIfCancellationRequested();
}
_logger.LogInformation("Removing {Count} old json files", floorsToMigrate.Count); await using var scope = _serviceScopeFactory.CreateAsyncScope();
foreach (var floorToMigrate in floorsToMigrate) await using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
File.Delete(floorToMigrate.GetSaveLocation());
}
/// <returns>Whether to archive this file once complete</returns> var fileStream = new FileStream(
private async Task MigrateFloor( Path.Join(_pluginInterface.GetPluginConfigDirectory(),
PalClientContext dbContext, $"territory-backup-{DateTime.Now:yyyyMMdd-HHmmss}.zip"),
JsonFloorState floorToMigrate, FileMode.CreateNew);
IReadOnlyDictionary<Guid, ImportHistory> imports, using (var backup = new ZipArchive(fileStream, ZipArchiveMode.Create, false))
CancellationToken cancellationToken)
{
using var logScope = _logger.BeginScope($"Import {(ETerritoryType)floorToMigrate.TerritoryType}");
if (floorToMigrate.Markers.Count == 0)
{
_logger.LogInformation("Skipping migration, floor has no markers");
}
if (await dbContext.Locations.AnyAsync(o => o.TerritoryType == floorToMigrate.TerritoryType,
cancellationToken))
{
_logger.LogInformation("Skipping migration, floor already has locations in the database");
return;
}
_logger.LogInformation("Starting migration of {Count} locations", floorToMigrate.Markers.Count);
List<ClientLocation> clientLocations = floorToMigrate.Markers
.Where(o => o.Type == JsonMarker.EType.Trap || o.Type == JsonMarker.EType.Hoard)
.Select(o =>
{ {
var clientLocation = new ClientLocation IReadOnlyDictionary<Guid, ImportHistory> imports =
await dbContext.Imports.ToDictionaryAsync(import => import.Id, cancellationToken);
foreach (var floorToMigrate in floorsToMigrate)
{ {
TerritoryType = floorToMigrate.TerritoryType, backup.CreateEntryFromFile(floorToMigrate.GetSaveLocation(),
Type = MapJsonType(o.Type), Path.GetFileName(floorToMigrate.GetSaveLocation()), CompressionLevel.SmallestSize);
X = o.Position.X, await MigrateFloor(dbContext, floorToMigrate, imports, cancellationToken);
Y = o.Position.Y, }
Z = o.Position.Z,
Seen = o.Seen,
// the SelectMany is misleading here, each import has either 0 or 1 associated db entry with that id await dbContext.SaveChangesAsync(cancellationToken);
ImportedBy = o.Imports }
.Select(importId =>
imports.TryGetValue(importId, out ImportHistory? import) ? import : null)
.Where(import => import != null)
.Cast<ImportHistory>()
.Distinct()
.ToList(),
// if we have a location not encountered locally, which also wasn't imported, _logger.LogInformation("Removing {Count} old json files", floorsToMigrate.Count);
// it very likely is a download (but we have no information to track this). foreach (var floorToMigrate in floorsToMigrate)
Source = o.Seen ? ClientLocation.ESource.SeenLocally : File.Delete(floorToMigrate.GetSaveLocation());
o.Imports.Count > 0 ? ClientLocation.ESource.Import : ClientLocation.ESource.Download, }
SinceVersion = o.SinceVersion ?? "0.0",
};
clientLocation.RemoteEncounters = o.RemoteSeenOn /// <returns>Whether to archive this file once complete</returns>
.Select(accountId => new RemoteEncounter(clientLocation, accountId)) private async Task MigrateFloor(
.ToList(); PalClientContext dbContext,
JsonFloorState floorToMigrate,
return clientLocation; IReadOnlyDictionary<Guid, ImportHistory> imports,
}).ToList(); CancellationToken cancellationToken)
await dbContext.Locations.AddRangeAsync(clientLocations, cancellationToken);
_logger.LogInformation("Migrated {Count} locations", clientLocations.Count);
}
private ClientLocation.EType MapJsonType(JsonMarker.EType type)
{
return type switch
{ {
JsonMarker.EType.Trap => ClientLocation.EType.Trap, using var logScope = _logger.BeginScope($"Import {(ETerritoryType)floorToMigrate.TerritoryType}");
JsonMarker.EType.Hoard => ClientLocation.EType.Hoard, if (floorToMigrate.Markers.Count == 0)
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null) {
}; _logger.LogInformation("Skipping migration, floor has no markers");
} }
if (await dbContext.Locations.AnyAsync(o => o.TerritoryType == floorToMigrate.TerritoryType,
cancellationToken))
{
_logger.LogInformation("Skipping migration, floor already has locations in the database");
return;
}
_logger.LogInformation("Starting migration of {Count} locations", floorToMigrate.Markers.Count);
List<ClientLocation> clientLocations = floorToMigrate.Markers
.Where(o => o.Type == JsonMarker.EType.Trap || o.Type == JsonMarker.EType.Hoard)
.Select(o =>
{
var clientLocation = new ClientLocation
{
TerritoryType = floorToMigrate.TerritoryType,
Type = MapJsonType(o.Type),
X = o.Position.X,
Y = o.Position.Y,
Z = o.Position.Z,
Seen = o.Seen,
// the SelectMany is misleading here, each import has either 0 or 1 associated db entry with that id
ImportedBy = o.Imports
.Select(importId =>
imports.TryGetValue(importId, out ImportHistory? import) ? import : null)
.Where(import => import != null)
.Cast<ImportHistory>()
.Distinct()
.ToList(),
// if we have a location not encountered locally, which also wasn't imported,
// it very likely is a download (but we have no information to track this).
Source = o.Seen ? ClientLocation.ESource.SeenLocally :
o.Imports.Count > 0 ? ClientLocation.ESource.Import : ClientLocation.ESource.Download,
SinceVersion = o.SinceVersion ?? "0.0",
};
clientLocation.RemoteEncounters = o.RemoteSeenOn
.Select(accountId => new RemoteEncounter(clientLocation, accountId))
.ToList();
return clientLocation;
}).ToList();
await dbContext.Locations.AddRangeAsync(clientLocations, cancellationToken);
_logger.LogInformation("Migrated {Count} locations", clientLocations.Count);
}
private ClientLocation.EType MapJsonType(JsonMarker.EType type)
{
return type switch
{
JsonMarker.EType.Trap => ClientLocation.EType.Trap,
JsonMarker.EType.Hoard => ClientLocation.EType.Hoard,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
}
#pragma warning restore CS0612 #pragma warning restore CS0612
}
} }

View File

@ -6,61 +6,62 @@ using Microsoft.Extensions.Logging;
using Pal.Client.Configuration; using Pal.Client.Configuration;
using Pal.Common; using Pal.Common;
namespace Pal.Client.Database; namespace Pal.Client.Database
internal sealed class Cleanup
{ {
private readonly ILogger<Cleanup> _logger; internal sealed class Cleanup
private readonly IPalacePalConfiguration _configuration;
public Cleanup(ILogger<Cleanup> logger, IPalacePalConfiguration configuration)
{ {
_logger = logger; private readonly ILogger<Cleanup> _logger;
_configuration = configuration; private readonly IPalacePalConfiguration _configuration;
}
public void Purge(PalClientContext dbContext) public Cleanup(ILogger<Cleanup> logger, IPalacePalConfiguration configuration)
{ {
var toDelete = dbContext.Locations _logger = logger;
.Include(o => o.ImportedBy) _configuration = configuration;
.Include(o => o.RemoteEncounters) }
.AsSplitQuery()
.Where(DefaultPredicate())
.Where(AnyRemoteEncounter())
.ToList();
_logger.LogInformation("Cleaning up {Count} outdated locations", toDelete.Count);
dbContext.Locations.RemoveRange(toDelete);
}
public void Purge(PalClientContext dbContext, ETerritoryType territoryType) public void Purge(PalClientContext dbContext)
{ {
var toDelete = dbContext.Locations var toDelete = dbContext.Locations
.Include(o => o.ImportedBy) .Include(o => o.ImportedBy)
.Include(o => o.RemoteEncounters) .Include(o => o.RemoteEncounters)
.AsSplitQuery() .AsSplitQuery()
.Where(o => o.TerritoryType == (ushort)territoryType) .Where(DefaultPredicate())
.Where(DefaultPredicate()) .Where(AnyRemoteEncounter())
.Where(AnyRemoteEncounter()) .ToList();
.ToList(); _logger.LogInformation("Cleaning up {Count} outdated locations", toDelete.Count);
_logger.LogInformation("Cleaning up {Count} outdated locations for territory {Territory}", toDelete.Count, dbContext.Locations.RemoveRange(toDelete);
territoryType); }
dbContext.Locations.RemoveRange(toDelete);
}
private Expression<Func<ClientLocation, bool>> DefaultPredicate() public void Purge(PalClientContext dbContext, ETerritoryType territoryType)
{ {
return o => !o.Seen && var toDelete = dbContext.Locations
o.ImportedBy.Count == 0 && .Include(o => o.ImportedBy)
o.Source != ClientLocation.ESource.SeenLocally && .Include(o => o.RemoteEncounters)
o.Source != ClientLocation.ESource.ExplodedLocally; .AsSplitQuery()
} .Where(o => o.TerritoryType == (ushort)territoryType)
.Where(DefaultPredicate())
.Where(AnyRemoteEncounter())
.ToList();
_logger.LogInformation("Cleaning up {Count} outdated locations for territory {Territory}", toDelete.Count,
territoryType);
dbContext.Locations.RemoveRange(toDelete);
}
private Expression<Func<ClientLocation, bool>> AnyRemoteEncounter() private Expression<Func<ClientLocation, bool>> DefaultPredicate()
{ {
if (_configuration.Mode == EMode.Offline) return o => !o.Seen &&
return o => true; o.ImportedBy.Count == 0 &&
else o.Source != ClientLocation.ESource.SeenLocally &&
// keep downloaded markers o.Source != ClientLocation.ESource.ExplodedLocally;
return o => o.Source != ClientLocation.ESource.Download; }
private Expression<Func<ClientLocation, bool>> AnyRemoteEncounter()
{
if (_configuration.Mode == EMode.Offline)
return o => true;
else
// keep downloaded markers
return o => o.Source != ClientLocation.ESource.Download;
}
} }
} }

View File

@ -1,58 +1,59 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Pal.Client.Database; namespace Pal.Client.Database
internal sealed class ClientLocation
{ {
[Key] public int LocalId { get; set; } internal sealed class ClientLocation
public ushort TerritoryType { get; set; }
public EType Type { get; set; }
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }
/// <summary>
/// Whether we have encountered the trap/coffer at this location in-game.
/// </summary>
public bool Seen { get; set; }
/// <summary>
/// Which account ids this marker was seen. This is a list merely to support different remote endpoints
/// (where each server would assign you a different id).
/// </summary>
public List<RemoteEncounter> RemoteEncounters { get; set; } = new();
/// <summary>
/// To keep track of which markers were imported through a downloaded file, we save the associated import-id.
///
/// Importing another file for the same remote server will remove the old import-id, and add the new import-id here.
/// </summary>
public List<ImportHistory> ImportedBy { get; set; } = new();
/// <summary>
/// Determines where this location is originally from.
/// </summary>
public ESource Source { get; set; }
/// <summary>
/// To make rollbacks of local data easier, keep track of the plugin version which was used to create this location initially.
/// </summary>
public string SinceVersion { get; set; } = "0.0";
public enum EType
{ {
Trap = 1, [Key] public int LocalId { get; set; }
Hoard = 2, public ushort TerritoryType { get; set; }
} public EType Type { get; set; }
public float X { get; set; }
public float Y { get; set; }
public float Z { get; set; }
public enum ESource /// <summary>
{ /// Whether we have encountered the trap/coffer at this location in-game.
Unknown = 0, /// </summary>
SeenLocally = 1, public bool Seen { get; set; }
ExplodedLocally = 2,
Import = 3, /// <summary>
Download = 4, /// Which account ids this marker was seen. This is a list merely to support different remote endpoints
/// (where each server would assign you a different id).
/// </summary>
public List<RemoteEncounter> RemoteEncounters { get; set; } = new();
/// <summary>
/// To keep track of which markers were imported through a downloaded file, we save the associated import-id.
///
/// Importing another file for the same remote server will remove the old import-id, and add the new import-id here.
/// </summary>
public List<ImportHistory> ImportedBy { get; set; } = new();
/// <summary>
/// Determines where this location is originally from.
/// </summary>
public ESource Source { get; set; }
/// <summary>
/// To make rollbacks of local data easier, keep track of the plugin version which was used to create this location initially.
/// </summary>
public string SinceVersion { get; set; } = "0.0";
public enum EType
{
Trap = 1,
Hoard = 2,
}
public enum ESource
{
Unknown = 0,
SeenLocally = 1,
ExplodedLocally = 2,
Import = 3,
Download = 4,
}
} }
} }

View File

@ -1,14 +1,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace Pal.Client.Database; namespace Pal.Client.Database
internal sealed class ImportHistory
{ {
public Guid Id { get; set; } internal sealed class ImportHistory
public string? RemoteUrl { get; set; } {
public DateTime ExportedAt { get; set; } public Guid Id { get; set; }
public DateTime ImportedAt { get; set; } public string? RemoteUrl { get; set; }
public DateTime ExportedAt { get; set; }
public DateTime ImportedAt { get; set; }
public List<ClientLocation> ImportedLocations { get; set; } = new(); public List<ClientLocation> ImportedLocations { get; set; } = new();
}
} }

View File

@ -1,23 +1,24 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace Pal.Client.Database; namespace Pal.Client.Database
internal class PalClientContext : DbContext
{ {
public DbSet<ClientLocation> Locations { get; set; } = null!; internal class PalClientContext : DbContext
public DbSet<ImportHistory> Imports { get; set; } = null!;
public DbSet<RemoteEncounter> RemoteEncounters { get; set; } = null!;
public PalClientContext(DbContextOptions<PalClientContext> options)
: base(options)
{ {
} public DbSet<ClientLocation> Locations { get; set; } = null!;
public DbSet<ImportHistory> Imports { get; set; } = null!;
public DbSet<RemoteEncounter> RemoteEncounters { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder) public PalClientContext(DbContextOptions<PalClientContext> options)
{ : base(options)
modelBuilder.Entity<ClientLocation>() {
.HasMany(o => o.ImportedBy) }
.WithMany(o => o.ImportedLocations)
.UsingEntity(o => o.ToTable("LocationImports")); protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ClientLocation>()
.HasMany(o => o.ImportedBy)
.WithMany(o => o.ImportedLocations)
.UsingEntity(o => o.ToTable("LocationImports"));
}
} }
} }

View File

@ -2,39 +2,40 @@
using Pal.Client.Extensions; using Pal.Client.Extensions;
using Pal.Client.Net; using Pal.Client.Net;
namespace Pal.Client.Database; namespace Pal.Client.Database
/// <summary>
/// To avoid sending too many requests to the server, we cache which locations have been seen
/// locally. These never expire, and locations which have been seen with a specific account
/// are never sent to the server again.
///
/// To be marked as seen, it needs to be essentially processed by <see cref="RemoteApi.MarkAsSeen"/>.
/// </summary>
internal sealed class RemoteEncounter
{ {
[Key]
public int Id { get; private set; }
public int ClientLocationId { get; private set; }
public ClientLocation ClientLocation { get; private set; } = null!;
/// <summary> /// <summary>
/// Partial account id. This is partially unique - however problems would (in theory) /// To avoid sending too many requests to the server, we cache which locations have been seen
/// only occur once you have two account-ids where the first 13 characters are equal. /// locally. These never expire, and locations which have been seen with a specific account
/// are never sent to the server again.
///
/// To be marked as seen, it needs to be essentially processed by <see cref="RemoteApi.MarkAsSeen"/>.
/// </summary> /// </summary>
[MaxLength(13)] internal sealed class RemoteEncounter
public string AccountId { get; private set; }
private RemoteEncounter(int clientLocationId, string accountId)
{ {
ClientLocationId = clientLocationId; [Key]
AccountId = accountId; public int Id { get; private set; }
}
public RemoteEncounter(ClientLocation clientLocation, string accountId) public int ClientLocationId { get; private set; }
{ public ClientLocation ClientLocation { get; private set; } = null!;
ClientLocation = clientLocation;
AccountId = accountId.ToPartialId(); /// <summary>
/// Partial account id. This is partially unique - however problems would (in theory)
/// only occur once you have two account-ids where the first 13 characters are equal.
/// </summary>
[MaxLength(13)]
public string AccountId { get; private set; }
private RemoteEncounter(int clientLocationId, string accountId)
{
ClientLocationId = clientLocationId;
AccountId = accountId;
}
public RemoteEncounter(ClientLocation clientLocation, string accountId)
{
ClientLocation = clientLocation;
AccountId = accountId.ToPartialId();
}
} }
} }

View File

@ -19,177 +19,178 @@ using Pal.Client.DependencyInjection;
using Pal.Client.Floors; using Pal.Client.Floors;
using Pal.Client.Windows; using Pal.Client.Windows;
namespace Pal.Client; namespace Pal.Client
/// <summary>
/// Takes care of async plugin init - this is mostly everything that requires either the config or the database to
/// be available.
/// </summary>
internal sealed class DependencyContextInitializer
{ {
private readonly ILogger<DependencyContextInitializer> _logger; /// <summary>
private readonly IServiceProvider _serviceProvider; /// Takes care of async plugin init - this is mostly everything that requires either the config or the database to
/// be available.
public DependencyContextInitializer(ILogger<DependencyContextInitializer> logger, /// </summary>
IServiceProvider serviceProvider) internal sealed class DependencyContextInitializer
{ {
_logger = logger; private readonly ILogger<DependencyContextInitializer> _logger;
_serviceProvider = serviceProvider; private readonly IServiceProvider _serviceProvider;
}
public async Task InitializeAsync(CancellationToken cancellationToken) public DependencyContextInitializer(ILogger<DependencyContextInitializer> logger,
{ IServiceProvider serviceProvider)
using IDisposable? logScope = _logger.BeginScope("AsyncInit");
_logger.LogInformation("Starting async init");
await CreateBackup();
cancellationToken.ThrowIfCancellationRequested();
await RunMigrations(cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
// v1 migration: config migration for import history, json migration for markers
_serviceProvider.GetRequiredService<ConfigurationManager>().Migrate();
await _serviceProvider.GetRequiredService<JsonMigration>().MigrateAsync(cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
await RunCleanup();
cancellationToken.ThrowIfCancellationRequested();
await RemoveOldBackups();
cancellationToken.ThrowIfCancellationRequested();
// windows that have logic to open on startup
_serviceProvider.GetRequiredService<AgreementWindow>();
// initialize components that are mostly self-contained/self-registered
_serviceProvider.GetRequiredService<GameHooks>();
_serviceProvider.GetRequiredService<FrameworkService>();
_serviceProvider.GetRequiredService<ChatService>();
// eager load any commands to find errors now, not when running them
_serviceProvider.GetRequiredService<IEnumerable<ISubCommand>>();
cancellationToken.ThrowIfCancellationRequested();
if (_serviceProvider.GetRequiredService<IPalacePalConfiguration>().HasBetaFeature(ObjectTableDebug.FeatureName))
_serviceProvider.GetRequiredService<ObjectTableDebug>();
cancellationToken.ThrowIfCancellationRequested();
_logger.LogInformation("Async init complete");
}
private async Task RemoveOldBackups()
{
await using var scope = _serviceProvider.CreateAsyncScope();
var pluginInterface = scope.ServiceProvider.GetRequiredService<IDalamudPluginInterface>();
var configuration = scope.ServiceProvider.GetRequiredService<IPalacePalConfiguration>();
var paths = Directory.GetFiles(pluginInterface.GetPluginConfigDirectory(), "backup-*.data.sqlite3",
new EnumerationOptions
{
IgnoreInaccessible = true,
RecurseSubdirectories = false,
MatchCasing = MatchCasing.CaseSensitive,
AttributesToSkip = FileAttributes.ReadOnly | FileAttributes.Hidden | FileAttributes.System,
ReturnSpecialDirectories = false,
});
if (paths.Length == 0)
return;
Regex backupRegex = new Regex(@"backup-([\d\-]{10})\.data\.sqlite3", RegexOptions.Compiled);
List<(DateTime Date, string Path)> backupFiles = new();
foreach (string path in paths)
{ {
var match = backupRegex.Match(Path.GetFileName(path)); _logger = logger;
if (!match.Success) _serviceProvider = serviceProvider;
continue;
if (DateTime.TryParseExact(match.Groups[1].Value, "yyyy-MM-dd", CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal, out DateTime backupDate))
{
backupFiles.Add((backupDate, path));
}
} }
var toDelete = backupFiles.OrderByDescending(x => x.Date) public async Task InitializeAsync(CancellationToken cancellationToken)
.Skip(configuration.Backups.MinimumBackupsToKeep)
.Where(x => (DateTime.Now.ToUniversalTime() - x.Date).Days > configuration.Backups.DaysToDeleteAfter)
.Select(x => x.Path);
foreach (var path in toDelete)
{ {
try using IDisposable? logScope = _logger.BeginScope("AsyncInit");
{
File.Delete(path); _logger.LogInformation("Starting async init");
_logger.LogInformation("Deleted old backup file '{Path}'", path);
} await CreateBackup();
catch (Exception e) cancellationToken.ThrowIfCancellationRequested();
{
_logger.LogWarning(e, "Could not delete backup file '{Path}'", path); await RunMigrations(cancellationToken);
} cancellationToken.ThrowIfCancellationRequested();
// v1 migration: config migration for import history, json migration for markers
_serviceProvider.GetRequiredService<ConfigurationManager>().Migrate();
await _serviceProvider.GetRequiredService<JsonMigration>().MigrateAsync(cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
await RunCleanup();
cancellationToken.ThrowIfCancellationRequested();
await RemoveOldBackups();
cancellationToken.ThrowIfCancellationRequested();
// windows that have logic to open on startup
_serviceProvider.GetRequiredService<AgreementWindow>();
// initialize components that are mostly self-contained/self-registered
_serviceProvider.GetRequiredService<GameHooks>();
_serviceProvider.GetRequiredService<FrameworkService>();
_serviceProvider.GetRequiredService<ChatService>();
// eager load any commands to find errors now, not when running them
_serviceProvider.GetRequiredService<IEnumerable<ISubCommand>>();
cancellationToken.ThrowIfCancellationRequested();
if (_serviceProvider.GetRequiredService<IPalacePalConfiguration>().HasBetaFeature(ObjectTableDebug.FeatureName))
_serviceProvider.GetRequiredService<ObjectTableDebug>();
cancellationToken.ThrowIfCancellationRequested();
_logger.LogInformation("Async init complete");
} }
}
private async Task CreateBackup() private async Task RemoveOldBackups()
{
await using var scope = _serviceProvider.CreateAsyncScope();
var pluginInterface = scope.ServiceProvider.GetRequiredService<IDalamudPluginInterface>();
string backupPath = Path.Join(pluginInterface.GetPluginConfigDirectory(),
$"backup-{DateTime.Now.ToUniversalTime():yyyy-MM-dd}.data.sqlite3");
string sourcePath = Path.Join(pluginInterface.GetPluginConfigDirectory(),
DependencyInjectionContext.DatabaseFileName);
if (File.Exists(sourcePath) && !File.Exists(backupPath))
{ {
try await using var scope = _serviceProvider.CreateAsyncScope();
var pluginInterface = scope.ServiceProvider.GetRequiredService<DalamudPluginInterface>();
var configuration = scope.ServiceProvider.GetRequiredService<IPalacePalConfiguration>();
var paths = Directory.GetFiles(pluginInterface.GetPluginConfigDirectory(), "backup-*.data.sqlite3",
new EnumerationOptions
{
IgnoreInaccessible = true,
RecurseSubdirectories = false,
MatchCasing = MatchCasing.CaseSensitive,
AttributesToSkip = FileAttributes.ReadOnly | FileAttributes.Hidden | FileAttributes.System,
ReturnSpecialDirectories = false,
});
if (paths.Length == 0)
return;
Regex backupRegex = new Regex(@"backup-([\d\-]{10})\.data\.sqlite3", RegexOptions.Compiled);
List<(DateTime Date, string Path)> backupFiles = new();
foreach (string path in paths)
{ {
if (File.Exists(sourcePath + "-shm") || File.Exists(sourcePath + "-wal")) var match = backupRegex.Match(Path.GetFileName(path));
if (!match.Success)
continue;
if (DateTime.TryParseExact(match.Groups[1].Value, "yyyy-MM-dd", CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal, out DateTime backupDate))
{ {
_logger.LogInformation("Creating database backup '{Path}' (open db)", backupPath); backupFiles.Add((backupDate, path));
await using var db = scope.ServiceProvider.GetRequiredService<PalClientContext>();
await using SqliteConnection source = new(db.Database.GetConnectionString());
await source.OpenAsync();
await using SqliteConnection backup = new($"Data Source={backupPath}");
source.BackupDatabase(backup);
SqliteConnection.ClearPool(backup);
}
else
{
_logger.LogInformation("Creating database backup '{Path}' (file copy)", backupPath);
File.Copy(sourcePath, backupPath);
} }
} }
catch (Exception e)
var toDelete = backupFiles.OrderByDescending(x => x.Date)
.Skip(configuration.Backups.MinimumBackupsToKeep)
.Where(x => (DateTime.Now.ToUniversalTime() - x.Date).Days > configuration.Backups.DaysToDeleteAfter)
.Select(x => x.Path);
foreach (var path in toDelete)
{ {
_logger.LogError(e, "Could not create backup"); try
{
File.Delete(path);
_logger.LogInformation("Deleted old backup file '{Path}'", path);
}
catch (Exception e)
{
_logger.LogWarning(e, "Could not delete backup file '{Path}'", path);
}
} }
} }
else
_logger.LogInformation("Database backup in '{Path}' already exists", backupPath);
}
private async Task RunMigrations(CancellationToken cancellationToken) private async Task CreateBackup()
{ {
await using var scope = _serviceProvider.CreateAsyncScope(); await using var scope = _serviceProvider.CreateAsyncScope();
_logger.LogInformation("Loading database & running migrations"); var pluginInterface = scope.ServiceProvider.GetRequiredService<DalamudPluginInterface>();
await using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>(); string backupPath = Path.Join(pluginInterface.GetPluginConfigDirectory(),
$"backup-{DateTime.Now.ToUniversalTime():yyyy-MM-dd}.data.sqlite3");
string sourcePath = Path.Join(pluginInterface.GetPluginConfigDirectory(),
DependencyInjectionContext.DatabaseFileName);
if (File.Exists(sourcePath) && !File.Exists(backupPath))
{
try
{
if (File.Exists(sourcePath + "-shm") || File.Exists(sourcePath + "-wal"))
{
_logger.LogInformation("Creating database backup '{Path}' (open db)", backupPath);
await using var db = scope.ServiceProvider.GetRequiredService<PalClientContext>();
await using SqliteConnection source = new(db.Database.GetConnectionString());
await source.OpenAsync();
await using SqliteConnection backup = new($"Data Source={backupPath}");
source.BackupDatabase(backup);
SqliteConnection.ClearPool(backup);
}
else
{
_logger.LogInformation("Creating database backup '{Path}' (file copy)", backupPath);
File.Copy(sourcePath, backupPath);
}
}
catch (Exception e)
{
_logger.LogError(e, "Could not create backup");
}
}
else
_logger.LogInformation("Database backup in '{Path}' already exists", backupPath);
}
// takes 2-3 seconds with initializing connections, loading driver etc. private async Task RunMigrations(CancellationToken cancellationToken)
await dbContext.Database.MigrateAsync(cancellationToken); {
_logger.LogInformation("Completed database migrations"); await using var scope = _serviceProvider.CreateAsyncScope();
}
private async Task RunCleanup() _logger.LogInformation("Loading database & running migrations");
{ await using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
await using var scope = _serviceProvider.CreateAsyncScope();
await using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
var cleanup = scope.ServiceProvider.GetRequiredService<Cleanup>();
cleanup.Purge(dbContext); // takes 2-3 seconds with initializing connections, loading driver etc.
await dbContext.Database.MigrateAsync(cancellationToken);
_logger.LogInformation("Completed database migrations");
}
await dbContext.SaveChangesAsync(); private async Task RunCleanup()
{
await using var scope = _serviceProvider.CreateAsyncScope();
await using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
var cleanup = scope.ServiceProvider.GetRequiredService<Cleanup>();
cleanup.Purge(dbContext);
await dbContext.SaveChangesAsync();
}
} }
} }

View File

@ -1,38 +1,38 @@
using Dalamud.Game.Text; using Dalamud.Game.Gui;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services;
using ECommons.DalamudServices.Legacy;
using Pal.Client.Properties; using Pal.Client.Properties;
namespace Pal.Client.DependencyInjection; namespace Pal.Client.DependencyInjection
internal sealed class Chat
{ {
private readonly IChatGui _chatGui; internal sealed class Chat
public Chat(IChatGui chatGui)
{ {
_chatGui = chatGui; private readonly ChatGui _chatGui;
}
public void Error(string e) public Chat(ChatGui chatGui)
{
_chatGui.PrintChat(new XivChatEntry
{ {
Message = new SeStringBuilder() _chatGui = chatGui;
.AddUiForeground($"[{Localization.Palace_Pal}] ", 16) }
.AddText(e).Build(),
Type = XivChatType.Urgent
});
}
public void Message(string message) public void Error(string e)
{ {
_chatGui.Print(new SeStringBuilder() _chatGui.PrintChat(new XivChatEntry
.AddUiForeground($"[{Localization.Palace_Pal}] ", 57) {
.AddText(message).Build()); Message = new SeStringBuilder()
} .AddUiForeground($"[{Localization.Palace_Pal}] ", 16)
.AddText(e).Build(),
Type = XivChatType.Urgent
});
}
public void UnformattedMessage(string message) public void Message(string message)
=> _chatGui.Print(message); {
_chatGui.Print(new SeStringBuilder()
.AddUiForeground($"[{Localization.Palace_Pal}] ", 57)
.AddText(message).Build());
}
public void UnformattedMessage(string message)
=> _chatGui.Print(message);
}
} }

View File

@ -1,116 +1,110 @@
using System; using System;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Dalamud.Data;
using Dalamud.Game.Gui;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
using Pal.Client.Configuration; using Pal.Client.Configuration;
using Pal.Client.Floors; using Pal.Client.Floors;
namespace Pal.Client.DependencyInjection; namespace Pal.Client.DependencyInjection
internal sealed class ChatService : IDisposable
{ {
private readonly IChatGui _chatGui; internal sealed class ChatService : IDisposable
private readonly TerritoryState _territoryState;
private readonly IPalacePalConfiguration _configuration;
private readonly IDataManager _dataManager;
private readonly LocalizedChatMessages _localizedChatMessages;
public ChatService(IChatGui chatGui, TerritoryState territoryState, IPalacePalConfiguration configuration,
IDataManager dataManager)
{ {
_chatGui = chatGui; private readonly ChatGui _chatGui;
_territoryState = territoryState; private readonly TerritoryState _territoryState;
_configuration = configuration; private readonly IPalacePalConfiguration _configuration;
_dataManager = dataManager; private readonly DataManager _dataManager;
private readonly LocalizedChatMessages _localizedChatMessages;
_localizedChatMessages = LoadLanguageStrings(); public ChatService(ChatGui chatGui, TerritoryState territoryState, IPalacePalConfiguration configuration,
DataManager dataManager)
_chatGui.ChatMessage += OnChatMessage;
}
public void Dispose()
=> _chatGui.ChatMessage -= OnChatMessage;
private void OnChatMessage(XivChatType type, int senderId, ref SeString sender, ref SeString seMessage,
ref bool isHandled)
{
if (_configuration.FirstUse)
return;
if (type != (XivChatType)2105)
return;
string message = seMessage.ToString();
if (_localizedChatMessages.FloorChanged.IsMatch(message))
{ {
_territoryState.PomanderOfSight = PomanderState.Inactive; _chatGui = chatGui;
_territoryState = territoryState;
_configuration = configuration;
_dataManager = dataManager;
if (_territoryState.PomanderOfIntuition == PomanderState.FoundOnCurrentFloor) _localizedChatMessages = LoadLanguageStrings();
_territoryState.PomanderOfIntuition = PomanderState.Inactive;
_chatGui.ChatMessage += OnChatMessage;
} }
else if (message.EndsWith(_localizedChatMessages.MapRevealed))
public void Dispose()
=> _chatGui.ChatMessage -= OnChatMessage;
private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString seMessage,
ref bool isHandled)
{ {
_territoryState.PomanderOfSight = PomanderState.Active; if (_configuration.FirstUse)
return;
if (type != (XivChatType)2105)
return;
string message = seMessage.ToString();
if (_localizedChatMessages.FloorChanged.IsMatch(message))
{
_territoryState.PomanderOfSight = PomanderState.Inactive;
if (_territoryState.PomanderOfIntuition == PomanderState.FoundOnCurrentFloor)
_territoryState.PomanderOfIntuition = PomanderState.Inactive;
}
else if (message.EndsWith(_localizedChatMessages.MapRevealed))
{
_territoryState.PomanderOfSight = PomanderState.Active;
}
else if (message.EndsWith(_localizedChatMessages.AllTrapsRemoved))
{
_territoryState.PomanderOfSight = PomanderState.PomanderOfSafetyUsed;
}
else if (message.EndsWith(_localizedChatMessages.HoardNotOnCurrentFloor) ||
message.EndsWith(_localizedChatMessages.HoardOnCurrentFloor))
{
// There is no functional difference between these - if you don't open the marked coffer,
// going to higher floors will keep the pomander active.
_territoryState.PomanderOfIntuition = PomanderState.Active;
}
else if (message.EndsWith(_localizedChatMessages.HoardCofferOpened))
{
_territoryState.PomanderOfIntuition = PomanderState.FoundOnCurrentFloor;
}
} }
else if (message.EndsWith(_localizedChatMessages.AllTrapsRemoved))
private LocalizedChatMessages LoadLanguageStrings()
{ {
_territoryState.PomanderOfSight = PomanderState.PomanderOfSafetyUsed; return new LocalizedChatMessages
{
MapRevealed = GetLocalizedString(7256),
AllTrapsRemoved = GetLocalizedString(7255),
HoardOnCurrentFloor = GetLocalizedString(7272),
HoardNotOnCurrentFloor = GetLocalizedString(7273),
HoardCofferOpened = GetLocalizedString(7274),
FloorChanged =
new Regex("^" + GetLocalizedString(7270).Replace("\u0002 \u0003\ufffd\u0002\u0003", @"(\d+)") +
"$"),
};
} }
else if (message.EndsWith(_localizedChatMessages.HoardNotOnCurrentFloor) ||
message.EndsWith(_localizedChatMessages.HoardOnCurrentFloor)) private string GetLocalizedString(uint id)
{ {
// There is no functional difference between these - if you don't open the marked coffer, return _dataManager.GetExcelSheet<LogMessage>()?.GetRow(id)?.Text?.ToString() ?? "Unknown";
// going to higher floors will keep the pomander active.
_territoryState.PomanderOfIntuition = PomanderState.Active;
} }
else if (message.EndsWith(_localizedChatMessages.HoardCofferOpened))
private sealed class LocalizedChatMessages
{ {
_territoryState.PomanderOfIntuition = PomanderState.FoundOnCurrentFloor; public string MapRevealed { get; init; } = "???"; //"The map for this floor has been revealed!";
public string AllTrapsRemoved { get; init; } = "???"; // "All the traps on this floor have disappeared!";
public string HoardOnCurrentFloor { get; init; } = "???"; // "You sense the Accursed Hoard calling you...";
public string HoardNotOnCurrentFloor { get; init; } =
"???"; // "You do not sense the call of the Accursed Hoard on this floor...";
public string HoardCofferOpened { get; init; } = "???"; // "You discover a piece of the Accursed Hoard!";
public Regex FloorChanged { get; init; } =
new(@"This isn't a game message, but will be replaced"); // new Regex(@"^Floor (\d+)$");
} }
} }
private LocalizedChatMessages LoadLanguageStrings()
{
return new LocalizedChatMessages
{
MapRevealed = GetLocalizedString(7256),
AllTrapsRemoved = GetLocalizedString(7255),
HoardOnCurrentFloor = GetLocalizedString(7272),
HoardNotOnCurrentFloor = GetLocalizedString(7273),
HoardCofferOpened = GetLocalizedString(7274),
FloorChanged =
new Regex("^" + GetLocalizedString(7270, true).Replace("\u0002 \u0003\ufffd\u0002\u0003", @"(\d+)") +
"$"),
};
}
private string GetLocalizedString(uint id, bool asRawData = false)
{
var text = _dataManager.GetExcelSheet<LogMessage>()?.GetRow(id)?.Text;
if (text == null)
return "Unknown";
if (asRawData)
return Encoding.UTF8.GetString(text.RawData);
else
return text.ToString();
}
private sealed class LocalizedChatMessages
{
public string MapRevealed { get; init; } = "???"; //"The map for this floor has been revealed!";
public string AllTrapsRemoved { get; init; } = "???"; // "All the traps on this floor have disappeared!";
public string HoardOnCurrentFloor { get; init; } = "???"; // "You sense the Accursed Hoard calling you...";
public string HoardNotOnCurrentFloor { get; init; } =
"???"; // "You do not sense the call of the Accursed Hoard on this floor...";
public string HoardCofferOpened { get; init; } = "???"; // "You discover a piece of the Accursed Hoard!";
public Regex FloorChanged { get; init; } =
new(@"This isn't a game message, but will be replaced"); // new Regex(@"^Floor (\d+)$");
}
} }

View File

@ -1,14 +1,15 @@
using System; using System;
namespace Pal.Client.DependencyInjection; namespace Pal.Client.DependencyInjection
internal sealed class DebugState
{ {
public string? DebugMessage { get; set; } internal sealed class DebugState
{
public string? DebugMessage { get; set; }
public void SetFromException(Exception e) public void SetFromException(Exception e)
=> DebugMessage = $"{DateTime.Now}\n{e}"; => DebugMessage = $"{DateTime.Now}\n{e}";
public void Reset() public void Reset()
=> DebugMessage = null; => DebugMessage = null;
}
} }

View File

@ -4,103 +4,103 @@ using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Memory; using Dalamud.Memory;
using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures; using Dalamud.Utility.Signatures;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Pal.Client.Floors; using Pal.Client.Floors;
namespace Pal.Client.DependencyInjection; namespace Pal.Client.DependencyInjection
internal sealed unsafe class GameHooks : IDisposable
{ {
private readonly ILogger<GameHooks> _logger; internal sealed unsafe class GameHooks : IDisposable
private readonly IObjectTable _objectTable; {
private readonly TerritoryState _territoryState; private readonly ILogger<GameHooks> _logger;
private readonly FrameworkService _frameworkService; private readonly ObjectTable _objectTable;
private readonly TerritoryState _territoryState;
private readonly FrameworkService _frameworkService;
#pragma warning disable CS0649 #pragma warning disable CS0649
private delegate nint ActorVfxCreateDelegate(char* a1, nint a2, nint a3, float a4, char a5, ushort a6, char a7); private delegate nint ActorVfxCreateDelegate(char* a1, nint a2, nint a3, float a4, char a5, ushort a6, char a7);
[Signature("40 53 55 56 57 48 81 EC ?? ?? ?? ?? 0F 29 B4 24 ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 0F B6 AC 24 ?? ?? ?? ?? 0F 28 F3 49 8B F8", DetourName = nameof(ActorVfxCreate))] [Signature("40 53 55 56 57 48 81 EC ?? ?? ?? ?? 0F 29 B4 24 ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 0F B6 AC 24 ?? ?? ?? ?? 0F 28 F3 49 8B F8", DetourName = nameof(ActorVfxCreate))]
private Hook<ActorVfxCreateDelegate> ActorVfxCreateHook { get; init; } = null!; private Hook<ActorVfxCreateDelegate> ActorVfxCreateHook { get; init; } = null!;
#pragma warning restore CS0649 #pragma warning restore CS0649
public GameHooks(ILogger<GameHooks> logger, IObjectTable objectTable, TerritoryState territoryState, FrameworkService frameworkService, IGameInteropProvider gameInteropProvider) public GameHooks(ILogger<GameHooks> logger, ObjectTable objectTable, TerritoryState territoryState, FrameworkService frameworkService)
{
_logger = logger;
_objectTable = objectTable;
_territoryState = territoryState;
_frameworkService = frameworkService;
_logger.LogDebug("Initializing game hooks");
gameInteropProvider.InitializeFromAttributes(this);
ActorVfxCreateHook.Enable();
_logger.LogDebug("Game hooks initialized");
}
/// <summary>
/// Even with a pomander of sight, the BattleChara's position for the trap remains at {0, 0, 0} until it is activated.
/// Upon exploding, the trap's position is moved to the exact location that the pomander of sight would have revealed.
///
/// That exact position appears to be used for VFX playing when you walk into it - even if you barely walk into the
/// outer ring of an otter/luring/impeding/landmine trap, the VFX plays at the exact center and not at your character's
/// location.
///
/// Especially at higher floors, you're more likely to walk into an undiscovered trap compared to e.g. 51-60,
/// and you probably don't want to/can't use sight on every floor - yet the trap location is still useful information.
///
/// Some (but not all) chests also count as BattleChara named 'Trap', however the effect upon opening isn't played via
/// ActorVfxCreate even if they explode (but probably as a Vfx with static location, doesn't matter for here).
///
/// Landmines and luring traps also don't play a VFX attached to their BattleChara.
///
/// otter: vfx/common/eff/dk05th_stdn0t.avfx <br/>
/// toading: vfx/common/eff/dk05th_stdn0t.avfx <br/>
/// enfeebling: vfx/common/eff/dk05th_stdn0t.avfx <br/>
/// landmine: none <br/>
/// luring: none <br/>
/// impeding: vfx/common/eff/dk05ht_ipws0t.avfx (one of silence/pacification) <br/>
/// impeding: vfx/common/eff/dk05ht_slet0t.avfx (the other of silence/pacification) <br/>
///
/// It is of course annoying that, when testing, almost all traps are landmines.
/// There's also vfx/common/eff/dk01gd_inv0h.avfx for e.g. impeding when you're invulnerable, but not sure if that
/// has other trigger conditions.
/// </summary>
public nint ActorVfxCreate(char* a1, nint a2, nint a3, float a4, char a5, ushort a6, char a7)
{
try
{ {
if (_territoryState.IsInDeepDungeon()) _logger = logger;
_objectTable = objectTable;
_territoryState = territoryState;
_frameworkService = frameworkService;
_logger.LogDebug("Initializing game hooks");
SignatureHelper.Initialise(this);
ActorVfxCreateHook.Enable();
_logger.LogDebug("Game hooks initialized");
}
/// <summary>
/// Even with a pomander of sight, the BattleChara's position for the trap remains at {0, 0, 0} until it is activated.
/// Upon exploding, the trap's position is moved to the exact location that the pomander of sight would have revealed.
///
/// That exact position appears to be used for VFX playing when you walk into it - even if you barely walk into the
/// outer ring of an otter/luring/impeding/landmine trap, the VFX plays at the exact center and not at your character's
/// location.
///
/// Especially at higher floors, you're more likely to walk into an undiscovered trap compared to e.g. 51-60,
/// and you probably don't want to/can't use sight on every floor - yet the trap location is still useful information.
///
/// Some (but not all) chests also count as BattleChara named 'Trap', however the effect upon opening isn't played via
/// ActorVfxCreate even if they explode (but probably as a Vfx with static location, doesn't matter for here).
///
/// Landmines and luring traps also don't play a VFX attached to their BattleChara.
///
/// otter: vfx/common/eff/dk05th_stdn0t.avfx <br/>
/// toading: vfx/common/eff/dk05th_stdn0t.avfx <br/>
/// enfeebling: vfx/common/eff/dk05th_stdn0t.avfx <br/>
/// landmine: none <br/>
/// luring: none <br/>
/// impeding: vfx/common/eff/dk05ht_ipws0t.avfx (one of silence/pacification) <br/>
/// impeding: vfx/common/eff/dk05ht_slet0t.avfx (the other of silence/pacification) <br/>
///
/// It is of course annoying that, when testing, almost all traps are landmines.
/// There's also vfx/common/eff/dk01gd_inv0h.avfx for e.g. impeding when you're invulnerable, but not sure if that
/// has other trigger conditions.
/// </summary>
public nint ActorVfxCreate(char* a1, nint a2, nint a3, float a4, char a5, ushort a6, char a7)
{
try
{ {
var vfxPath = MemoryHelper.ReadString(new nint(a1), Encoding.ASCII, 256); if (_territoryState.IsInDeepDungeon())
var obj = _objectTable.CreateObjectReference(a2);
/*
if (Service.Configuration.BetaKey == "VFX")
_chat.PalPrint($"{vfxPath} on {obj}");
*/
if (obj is IBattleChara bc && (bc.NameId == /* potd */ 5042 || bc.NameId == /* hoh */ 7395))
{ {
if (vfxPath == "vfx/common/eff/dk05th_stdn0t.avfx" || vfxPath == "vfx/common/eff/dk05ht_ipws0t.avfx") var vfxPath = MemoryHelper.ReadString(new nint(a1), Encoding.ASCII, 256);
var obj = _objectTable.CreateObjectReference(a2);
/*
if (Service.Configuration.BetaKey == "VFX")
_chat.PalPrint($"{vfxPath} on {obj}");
*/
if (obj is BattleChara bc && (bc.NameId == /* potd */ 5042 || bc.NameId == /* hoh */ 7395))
{ {
_logger.LogDebug("VFX '{Path}' playing at {Location}", vfxPath, obj.Position); if (vfxPath == "vfx/common/eff/dk05th_stdn0t.avfx" || vfxPath == "vfx/common/eff/dk05ht_ipws0t.avfx")
_frameworkService.NextUpdateObjects.Enqueue(obj.Address); {
_logger.LogDebug("VFX '{Path}' playing at {Location}", vfxPath, obj.Position);
_frameworkService.NextUpdateObjects.Enqueue(obj.Address);
}
} }
} }
} }
catch (Exception e)
{
_logger.LogError(e, "VFX Create Hook failed");
}
return ActorVfxCreateHook.Original(a1, a2, a3, a4, a5, a6, a7);
} }
catch (Exception e)
{
_logger.LogError(e, "VFX Create Hook failed");
}
return ActorVfxCreateHook.Original(a1, a2, a3, a4, a5, a6, a7);
}
public void Dispose() public void Dispose()
{ {
_logger.LogDebug("Disposing game hooks"); _logger.LogDebug("Disposing game hooks");
ActorVfxCreateHook.Dispose(); ActorVfxCreateHook.Dispose();
}
} }
} }

View File

@ -12,154 +12,155 @@ using Pal.Client.Floors;
using Pal.Client.Floors.Tasks; using Pal.Client.Floors.Tasks;
using Pal.Common; using Pal.Common;
namespace Pal.Client.DependencyInjection; namespace Pal.Client.DependencyInjection
internal sealed class ImportService
{ {
private readonly IServiceProvider _serviceProvider; internal sealed class ImportService
private readonly FloorService _floorService;
private readonly Cleanup _cleanup;
public ImportService(
IServiceProvider serviceProvider,
FloorService floorService,
Cleanup cleanup)
{ {
_serviceProvider = serviceProvider; private readonly IServiceProvider _serviceProvider;
_floorService = floorService; private readonly FloorService _floorService;
_cleanup = cleanup; private readonly Cleanup _cleanup;
}
public async Task<ImportHistory?> FindLast(CancellationToken token = default) public ImportService(
{ IServiceProvider serviceProvider,
await using var scope = _serviceProvider.CreateAsyncScope(); FloorService floorService,
await using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>(); Cleanup cleanup)
return await dbContext.Imports.OrderByDescending(x => x.ImportedAt).ThenBy(x => x.Id)
.FirstOrDefaultAsync(cancellationToken: token);
}
public (int traps, int hoard) Import(ExportRoot import)
{
try
{ {
_floorService.SetToImportState(); _serviceProvider = serviceProvider;
_floorService = floorService;
_cleanup = cleanup;
}
using var scope = _serviceProvider.CreateScope(); public async Task<ImportHistory?> FindLast(CancellationToken token = default)
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>(); {
await using var scope = _serviceProvider.CreateAsyncScope();
await using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
dbContext.Imports.RemoveRange(dbContext.Imports.Where(x => x.RemoteUrl == import.ServerUrl).ToList()); return await dbContext.Imports.OrderByDescending(x => x.ImportedAt).ThenBy(x => x.Id)
dbContext.SaveChanges(); .FirstOrDefaultAsync(cancellationToken: token);
}
ImportHistory importHistory = new ImportHistory public (int traps, int hoard) Import(ExportRoot import)
{
try
{ {
Id = Guid.Parse(import.ExportId), _floorService.SetToImportState();
RemoteUrl = import.ServerUrl,
ExportedAt = import.CreatedAt.ToDateTime(),
ImportedAt = DateTime.UtcNow,
};
dbContext.Imports.Add(importHistory);
int traps = 0; using var scope = _serviceProvider.CreateScope();
int hoard = 0; using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
foreach (var floor in import.Floors)
{
ETerritoryType territoryType = (ETerritoryType)floor.TerritoryType;
List<PersistentLocation> existingLocations = dbContext.Locations dbContext.Imports.RemoveRange(dbContext.Imports.Where(x => x.RemoteUrl == import.ServerUrl).ToList());
.Where(loc => loc.TerritoryType == floor.TerritoryType) dbContext.SaveChanges();
.ToList()
.Select(LoadTerritory.ToMemoryLocation) ImportHistory importHistory = new ImportHistory
.ToList();
foreach (var exportLocation in floor.Objects)
{ {
PersistentLocation persistentLocation = new PersistentLocation Id = Guid.Parse(import.ExportId),
{ RemoteUrl = import.ServerUrl,
Type = ToMemoryType(exportLocation.Type), ExportedAt = import.CreatedAt.ToDateTime(),
Position = new Vector3(exportLocation.X, exportLocation.Y, exportLocation.Z), ImportedAt = DateTime.UtcNow,
Source = ClientLocation.ESource.Unknown, };
}; dbContext.Imports.Add(importHistory);
var existingLocation = existingLocations.FirstOrDefault(x => x == persistentLocation); int traps = 0;
if (existingLocation != null) int hoard = 0;
{ foreach (var floor in import.Floors)
var clientLoc = dbContext.Locations.FirstOrDefault(o => o.LocalId == existingLocation.LocalId); {
clientLoc?.ImportedBy.Add(importHistory); ETerritoryType territoryType = (ETerritoryType)floor.TerritoryType;
continue; List<PersistentLocation> existingLocations = dbContext.Locations
.Where(loc => loc.TerritoryType == floor.TerritoryType)
.ToList()
.Select(LoadTerritory.ToMemoryLocation)
.ToList();
foreach (var exportLocation in floor.Objects)
{
PersistentLocation persistentLocation = new PersistentLocation
{
Type = ToMemoryType(exportLocation.Type),
Position = new Vector3(exportLocation.X, exportLocation.Y, exportLocation.Z),
Source = ClientLocation.ESource.Unknown,
};
var existingLocation = existingLocations.FirstOrDefault(x => x == persistentLocation);
if (existingLocation != null)
{
var clientLoc = dbContext.Locations.FirstOrDefault(o => o.LocalId == existingLocation.LocalId);
clientLoc?.ImportedBy.Add(importHistory);
continue;
}
ClientLocation clientLocation = new ClientLocation
{
TerritoryType = (ushort)territoryType,
Type = ToClientLocationType(exportLocation.Type),
X = exportLocation.X,
Y = exportLocation.Y,
Z = exportLocation.Z,
Seen = false,
Source = ClientLocation.ESource.Import,
ImportedBy = new List<ImportHistory> { importHistory },
SinceVersion = typeof(Plugin).Assembly.GetName().Version!.ToString(2),
};
dbContext.Locations.Add(clientLocation);
if (exportLocation.Type == ExportObjectType.Trap)
traps++;
else if (exportLocation.Type == ExportObjectType.Hoard)
hoard++;
} }
ClientLocation clientLocation = new ClientLocation
{
TerritoryType = (ushort)territoryType,
Type = ToClientLocationType(exportLocation.Type),
X = exportLocation.X,
Y = exportLocation.Y,
Z = exportLocation.Z,
Seen = false,
Source = ClientLocation.ESource.Import,
ImportedBy = new List<ImportHistory> { importHistory },
SinceVersion = typeof(Plugin).Assembly.GetName().Version!.ToString(2),
};
dbContext.Locations.Add(clientLocation);
if (exportLocation.Type == ExportObjectType.Trap)
traps++;
else if (exportLocation.Type == ExportObjectType.Hoard)
hoard++;
} }
dbContext.SaveChanges();
_cleanup.Purge(dbContext);
dbContext.SaveChanges();
return (traps, hoard);
}
finally
{
_floorService.ResetAll();
} }
dbContext.SaveChanges();
_cleanup.Purge(dbContext);
dbContext.SaveChanges();
return (traps, hoard);
} }
finally
private MemoryLocation.EType ToMemoryType(ExportObjectType exportLocationType)
{ {
_floorService.ResetAll(); return exportLocationType switch
{
ExportObjectType.Trap => MemoryLocation.EType.Trap,
ExportObjectType.Hoard => MemoryLocation.EType.Hoard,
_ => throw new ArgumentOutOfRangeException(nameof(exportLocationType), exportLocationType, null)
};
} }
}
private MemoryLocation.EType ToMemoryType(ExportObjectType exportLocationType) private ClientLocation.EType ToClientLocationType(ExportObjectType exportLocationType)
{
return exportLocationType switch
{ {
ExportObjectType.Trap => MemoryLocation.EType.Trap, return exportLocationType switch
ExportObjectType.Hoard => MemoryLocation.EType.Hoard, {
_ => throw new ArgumentOutOfRangeException(nameof(exportLocationType), exportLocationType, null) ExportObjectType.Trap => ClientLocation.EType.Trap,
}; ExportObjectType.Hoard => ClientLocation.EType.Hoard,
} _ => throw new ArgumentOutOfRangeException(nameof(exportLocationType), exportLocationType, null)
};
private ClientLocation.EType ToClientLocationType(ExportObjectType exportLocationType)
{
return exportLocationType switch
{
ExportObjectType.Trap => ClientLocation.EType.Trap,
ExportObjectType.Hoard => ClientLocation.EType.Hoard,
_ => throw new ArgumentOutOfRangeException(nameof(exportLocationType), exportLocationType, null)
};
}
public void RemoveById(Guid id)
{
try
{
_floorService.SetToImportState();
using var scope = _serviceProvider.CreateScope();
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
dbContext.RemoveRange(dbContext.Imports.Where(x => x.Id == id));
dbContext.SaveChanges();
_cleanup.Purge(dbContext);
dbContext.SaveChanges();
} }
finally
public void RemoveById(Guid id)
{ {
_floorService.ResetAll(); try
{
_floorService.SetToImportState();
using var scope = _serviceProvider.CreateScope();
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
dbContext.RemoveRange(dbContext.Imports.Where(x => x.Id == id));
dbContext.SaveChanges();
_cleanup.Purge(dbContext);
dbContext.SaveChanges();
}
finally
{
_floorService.ResetAll();
}
} }
} }
} }

View File

@ -6,22 +6,25 @@ using Microsoft.Extensions.Logging;
using Pal.Client.Extensions; using Pal.Client.Extensions;
using Pal.Client.Properties; using Pal.Client.Properties;
namespace Pal.Client.DependencyInjection; namespace Pal.Client.DependencyInjection
internal sealed class RepoVerification
{ {
public RepoVerification(ILogger<RepoVerification> logger, IDalamudPluginInterface pluginInterface, Chat chat) internal sealed class RepoVerification
{ {
logger.LogInformation("Install source: {Repo}", pluginInterface.SourceRepository); public RepoVerification(ILogger<RepoVerification> logger, DalamudPluginInterface pluginInterface, Chat chat)
if (!pluginInterface.IsDev && pluginInterface.SourceRepository.TrimEnd('/') != "https://plugins.carvel.li") {
logger.LogInformation("Install source: {Repo}", pluginInterface.SourceRepository);
if (!pluginInterface.IsDev
&& !pluginInterface.SourceRepository.StartsWith("https://raw.githubusercontent.com/carvelli/")
&& !pluginInterface.SourceRepository.StartsWith("https://github.com/carvelli/"))
{
chat.Error(string.Format(Localization.Error_WrongRepository,
"https://github.com/carvelli/Dalamud-Plugins"));
throw new RepoVerificationFailedException();
}
}
internal sealed class RepoVerificationFailedException : Exception
{ {
chat.Error(string.Format(Localization.Error_WrongRepository,
"https://plugins.carvel.li"));
throw new RepoVerificationFailedException();
} }
} }
internal sealed class RepoVerificationFailedException : Exception
{
}
} }

View File

@ -9,66 +9,67 @@ using Pal.Client.Net;
using Pal.Client.Properties; using Pal.Client.Properties;
using Pal.Client.Windows; using Pal.Client.Windows;
namespace Pal.Client.DependencyInjection; namespace Pal.Client.DependencyInjection
internal sealed class StatisticsService
{ {
private readonly IPalacePalConfiguration _configuration; internal sealed class StatisticsService
private readonly ILogger<StatisticsService> _logger;
private readonly RemoteApi _remoteApi;
private readonly StatisticsWindow _statisticsWindow;
private readonly Chat _chat;
public StatisticsService(
IPalacePalConfiguration configuration,
ILogger<StatisticsService> logger,
RemoteApi remoteApi,
StatisticsWindow statisticsWindow,
Chat chat)
{ {
_configuration = configuration; private readonly IPalacePalConfiguration _configuration;
_logger = logger; private readonly ILogger<StatisticsService> _logger;
_remoteApi = remoteApi; private readonly RemoteApi _remoteApi;
_statisticsWindow = statisticsWindow; private readonly StatisticsWindow _statisticsWindow;
_chat = chat; private readonly Chat _chat;
}
public void ShowGlobalStatistics() public StatisticsService(
{ IPalacePalConfiguration configuration,
Task.Run(async () => await FetchFloorStatistics()); ILogger<StatisticsService> logger,
} RemoteApi remoteApi,
StatisticsWindow statisticsWindow,
private async Task FetchFloorStatistics() Chat chat)
{
try
{ {
if (!_configuration.HasRoleOnCurrentServer(RemoteApi.RemoteUrl, "statistics:view")) _configuration = configuration;
_logger = logger;
_remoteApi = remoteApi;
_statisticsWindow = statisticsWindow;
_chat = chat;
}
public void ShowGlobalStatistics()
{
Task.Run(async () => await FetchFloorStatistics());
}
private async Task FetchFloorStatistics()
{
try
{ {
if (!_configuration.HasRoleOnCurrentServer(RemoteApi.RemoteUrl, "statistics:view"))
{
_chat.Error(Localization.Command_pal_stats_CurrentFloor);
return;
}
var (success, floorStatistics) = await _remoteApi.FetchStatistics();
if (success)
{
_statisticsWindow.SetFloorData(floorStatistics);
_statisticsWindow.IsOpen = true;
}
else
{
_chat.Error(Localization.Command_pal_stats_UnableToFetchStatistics);
}
}
catch (RpcException e) when (e.StatusCode == StatusCode.PermissionDenied)
{
_logger.LogWarning(e, "Access denied while fetching floor statistics");
_chat.Error(Localization.Command_pal_stats_CurrentFloor); _chat.Error(Localization.Command_pal_stats_CurrentFloor);
return;
} }
catch (Exception e)
var (success, floorStatistics) = await _remoteApi.FetchStatistics();
if (success)
{ {
_statisticsWindow.SetFloorData(floorStatistics); _logger.LogError(e, "Could not fetch floor statistics");
_statisticsWindow.IsOpen = true; _chat.Error(string.Format(Localization.Error_CommandFailed,
$"{e.GetType()} - {e.Message}"));
} }
else
{
_chat.Error(Localization.Command_pal_stats_UnableToFetchStatistics);
}
}
catch (RpcException e) when (e.StatusCode == StatusCode.PermissionDenied)
{
_logger.LogWarning(e, "Access denied while fetching floor statistics");
_chat.Error(Localization.Command_pal_stats_CurrentFloor);
}
catch (Exception e)
{
_logger.LogError(e, "Could not fetch floor statistics");
_chat.Error(string.Format(Localization.Error_CommandFailed,
$"{e.GetType()} - {e.Message}"));
} }
} }
} }

View File

@ -10,7 +10,6 @@ using Dalamud.Game.Command;
using Dalamud.Game.Gui; using Dalamud.Game.Gui;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -26,169 +25,166 @@ using Pal.Client.Rendering;
using Pal.Client.Scheduled; using Pal.Client.Scheduled;
using Pal.Client.Windows; using Pal.Client.Windows;
namespace Pal.Client; namespace Pal.Client
/// <summary>
/// DI-aware Plugin.
/// </summary>
internal sealed class DependencyInjectionContext : IDisposable
{ {
public const string DatabaseFileName = "palace-pal.data.sqlite3";
/// <summary> /// <summary>
/// Initialized as temporary logger, will be overriden once context is ready with a logger that supports scopes. /// DI-aware Plugin.
/// </summary> /// </summary>
private ILogger _logger; internal sealed class DependencyInjectionContext : IDisposable
private readonly string _sqliteConnectionString;
private readonly ServiceCollection _serviceCollection = new();
private ServiceProvider? _serviceProvider;
public DependencyInjectionContext(
IDalamudPluginInterface pluginInterface,
IClientState clientState,
IGameGui gameGui,
IChatGui chatGui,
IObjectTable objectTable,
IFramework framework,
ICondition condition,
ICommandManager commandManager,
IDataManager dataManager,
IGameInteropProvider gameInteropProvider,
IPluginLog pluginLog,
Plugin plugin)
{ {
var loggerProvider = new DalamudLoggerProvider(pluginLog); public const string DatabaseFileName = "palace-pal.data.sqlite3";
_logger = loggerProvider.CreateLogger<DependencyInjectionContext>(); public static DalamudLoggerProvider LoggerProvider { get; } = new(typeof(Plugin).Assembly);
_logger.LogInformation("Building dalamud service container for {Assembly}",
typeof(DependencyInjectionContext).Assembly.FullName);
// set up legacy services /// <summary>
/// Initialized as temporary logger, will be overriden once context is ready with a logger that supports scopes.
/// </summary>
private ILogger _logger = LoggerProvider.CreateLogger<DependencyInjectionContext>();
private readonly string _sqliteConnectionString;
private readonly ServiceCollection _serviceCollection = new();
private ServiceProvider? _serviceProvider;
public DependencyInjectionContext(
DalamudPluginInterface pluginInterface,
ClientState clientState,
GameGui gameGui,
ChatGui chatGui,
ObjectTable objectTable,
Framework framework,
Condition condition,
CommandManager commandManager,
DataManager dataManager,
Plugin plugin)
{
_logger.LogInformation("Building dalamud service container for {Assembly}",
typeof(DependencyInjectionContext).Assembly.FullName);
// set up legacy services
#pragma warning disable CS0612 #pragma warning disable CS0612
JsonFloorState.SetContextProperties(pluginInterface.GetPluginConfigDirectory()); JsonFloorState.SetContextProperties(pluginInterface.GetPluginConfigDirectory());
#pragma warning restore CS0612 #pragma warning restore CS0612
// set up logging // set up logging
_serviceCollection.AddLogging(builder => _serviceCollection.AddLogging(builder =>
builder.AddFilter("Pal", LogLevel.Trace) builder.AddFilter("Pal", LogLevel.Trace)
.AddFilter("Microsoft.EntityFrameworkCore.Database", LogLevel.Warning) .AddFilter("Microsoft.EntityFrameworkCore.Database", LogLevel.Warning)
.AddFilter("Grpc", LogLevel.Debug) .AddFilter("Grpc", LogLevel.Debug)
.ClearProviders() .ClearProviders()
.AddDalamudLogger(pluginLog)); .AddDalamudLogger(plugin));
// dalamud // dalamud
_serviceCollection.AddSingleton<IDalamudPlugin>(plugin); _serviceCollection.AddSingleton<IDalamudPlugin>(plugin);
_serviceCollection.AddSingleton(pluginInterface); _serviceCollection.AddSingleton(pluginInterface);
_serviceCollection.AddSingleton(clientState); _serviceCollection.AddSingleton(clientState);
_serviceCollection.AddSingleton(gameGui); _serviceCollection.AddSingleton(gameGui);
_serviceCollection.AddSingleton(chatGui); _serviceCollection.AddSingleton(chatGui);
_serviceCollection.AddSingleton<Chat>(); _serviceCollection.AddSingleton<Chat>();
_serviceCollection.AddSingleton(objectTable); _serviceCollection.AddSingleton(objectTable);
_serviceCollection.AddSingleton(framework); _serviceCollection.AddSingleton(framework);
_serviceCollection.AddSingleton(condition); _serviceCollection.AddSingleton(condition);
_serviceCollection.AddSingleton(commandManager); _serviceCollection.AddSingleton(commandManager);
_serviceCollection.AddSingleton(dataManager); _serviceCollection.AddSingleton(dataManager);
_serviceCollection.AddSingleton(gameInteropProvider); _serviceCollection.AddSingleton(new WindowSystem(typeof(DependencyInjectionContext).AssemblyQualifiedName));
_serviceCollection.AddSingleton(new WindowSystem(typeof(DependencyInjectionContext).AssemblyQualifiedName));
_sqliteConnectionString = _sqliteConnectionString =
$"Data Source={Path.Join(pluginInterface.GetPluginConfigDirectory(), DatabaseFileName)}"; $"Data Source={Path.Join(pluginInterface.GetPluginConfigDirectory(), DatabaseFileName)}";
} }
public IServiceProvider BuildServiceContainer() public IServiceProvider BuildServiceContainer()
{
_logger.LogInformation("Building async service container for {Assembly}",
typeof(DependencyInjectionContext).Assembly.FullName);
// EF core
_serviceCollection.AddDbContext<PalClientContext>(o => o
.UseSqlite(_sqliteConnectionString)
.UseModel(Database.Compiled.PalClientContextModel.Instance));
_serviceCollection.AddTransient<JsonMigration>();
_serviceCollection.AddScoped<Cleanup>();
// plugin-specific
_serviceCollection.AddScoped<DependencyContextInitializer>();
_serviceCollection.AddScoped<DebugState>();
_serviceCollection.AddScoped<GameHooks>();
_serviceCollection.AddScoped<RemoteApi>();
_serviceCollection.AddScoped<ConfigurationManager>();
_serviceCollection.AddScoped<IPalacePalConfiguration>(sp =>
sp.GetRequiredService<ConfigurationManager>().Load());
_serviceCollection.AddTransient<RepoVerification>();
// commands
_serviceCollection.AddScoped<PalConfigCommand>();
_serviceCollection.AddScoped<ISubCommand, PalConfigCommand>();
_serviceCollection.AddScoped<ISubCommand, PalNearCommand>();
_serviceCollection.AddScoped<ISubCommand, PalStatsCommand>();
_serviceCollection.AddScoped<ISubCommand, PalTestConnectionCommand>();
// territory & marker related services
_serviceCollection.AddScoped<TerritoryState>();
_serviceCollection.AddScoped<FrameworkService>();
_serviceCollection.AddScoped<ChatService>();
_serviceCollection.AddScoped<FloorService>();
_serviceCollection.AddScoped<ImportService>();
_serviceCollection.AddScoped<ObjectTableDebug>();
// windows & related services
_serviceCollection.AddScoped<AgreementWindow>();
_serviceCollection.AddScoped<ConfigWindow>();
_serviceCollection.AddScoped<StatisticsService>();
_serviceCollection.AddScoped<StatisticsWindow>();
// rendering
_serviceCollection.AddScoped<SimpleRenderer>();
_serviceCollection.AddScoped<SplatoonRenderer>();
_serviceCollection.AddScoped<RenderAdapter>();
// queue handling
_serviceCollection.AddTransient<IQueueOnFrameworkThread.Handler<QueuedImport>, QueuedImport.Handler>();
_serviceCollection
.AddTransient<IQueueOnFrameworkThread.Handler<QueuedUndoImport>, QueuedUndoImport.Handler>();
_serviceCollection
.AddTransient<IQueueOnFrameworkThread.Handler<QueuedConfigUpdate>, QueuedConfigUpdate.Handler>();
_serviceCollection
.AddTransient<IQueueOnFrameworkThread.Handler<QueuedSyncResponse>, QueuedSyncResponse.Handler>();
// build
_serviceProvider = _serviceCollection.BuildServiceProvider(new ServiceProviderOptions
{ {
ValidateOnBuild = true, _logger.LogInformation("Building async service container for {Assembly}",
ValidateScopes = true, typeof(DependencyInjectionContext).Assembly.FullName);
});
// EF core
_serviceCollection.AddDbContext<PalClientContext>(o => o
.UseSqlite(_sqliteConnectionString)
.UseModel(Database.Compiled.PalClientContextModel.Instance));
_serviceCollection.AddTransient<JsonMigration>();
_serviceCollection.AddScoped<Cleanup>();
// plugin-specific
_serviceCollection.AddScoped<DependencyContextInitializer>();
_serviceCollection.AddScoped<DebugState>();
_serviceCollection.AddScoped<GameHooks>();
_serviceCollection.AddScoped<RemoteApi>();
_serviceCollection.AddScoped<ConfigurationManager>();
_serviceCollection.AddScoped<IPalacePalConfiguration>(sp =>
sp.GetRequiredService<ConfigurationManager>().Load());
_serviceCollection.AddTransient<RepoVerification>();
// commands
_serviceCollection.AddScoped<PalConfigCommand>();
_serviceCollection.AddScoped<ISubCommand, PalConfigCommand>();
_serviceCollection.AddScoped<ISubCommand, PalNearCommand>();
_serviceCollection.AddScoped<ISubCommand, PalStatsCommand>();
_serviceCollection.AddScoped<ISubCommand, PalTestConnectionCommand>();
// territory & marker related services
_serviceCollection.AddScoped<TerritoryState>();
_serviceCollection.AddScoped<FrameworkService>();
_serviceCollection.AddScoped<ChatService>();
_serviceCollection.AddScoped<FloorService>();
_serviceCollection.AddScoped<ImportService>();
_serviceCollection.AddScoped<ObjectTableDebug>();
// windows & related services
_serviceCollection.AddScoped<AgreementWindow>();
_serviceCollection.AddScoped<ConfigWindow>();
_serviceCollection.AddScoped<StatisticsService>();
_serviceCollection.AddScoped<StatisticsWindow>();
// rendering
_serviceCollection.AddScoped<SimpleRenderer>();
_serviceCollection.AddScoped<SplatoonRenderer>();
_serviceCollection.AddScoped<RenderAdapter>();
// queue handling
_serviceCollection.AddTransient<IQueueOnFrameworkThread.Handler<QueuedImport>, QueuedImport.Handler>();
_serviceCollection
.AddTransient<IQueueOnFrameworkThread.Handler<QueuedUndoImport>, QueuedUndoImport.Handler>();
_serviceCollection
.AddTransient<IQueueOnFrameworkThread.Handler<QueuedConfigUpdate>, QueuedConfigUpdate.Handler>();
_serviceCollection
.AddTransient<IQueueOnFrameworkThread.Handler<QueuedSyncResponse>, QueuedSyncResponse.Handler>();
// build
_serviceProvider = _serviceCollection.BuildServiceProvider(new ServiceProviderOptions
{
ValidateOnBuild = true,
ValidateScopes = true,
});
#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
// - you host your own server instance // - you host your own server instance
// //
// This is mainly to avoid this plugin being included in 'mega-repos' that, for whatever reason, decide // This is mainly to avoid this plugin being included in 'mega-repos' that, for whatever reason, decide
// that collecting all plugins is a good idea (and break half in the process). // that collecting all plugins is a good idea (and break half in the process).
_serviceProvider.GetService<RepoVerification>(); _serviceProvider.GetService<RepoVerification>();
#endif #endif
// This is not ideal as far as loading the plugin goes, because there's no way to check for errors and // This is not ideal as far as loading the plugin goes, because there's no way to check for errors and
// tell Dalamud that no, the plugin isn't ready -- so the plugin will count as properly initialized, // tell Dalamud that no, the plugin isn't ready -- so the plugin will count as properly initialized,
// even if it's not. // even if it's not.
// //
// There's 2-3 seconds of slowdown primarily caused by the sqlite init, but that needs to happen for // There's 2-3 seconds of slowdown primarily caused by the sqlite init, but that needs to happen for
// config stuff. // config stuff.
_logger = _serviceProvider.GetRequiredService<ILogger<DependencyInjectionContext>>(); _logger = _serviceProvider.GetRequiredService<ILogger<DependencyInjectionContext>>();
_logger.LogInformation("Service container built"); _logger.LogInformation("Service container built");
return _serviceProvider; return _serviceProvider;
} }
public void Dispose() public void Dispose()
{ {
_logger.LogInformation("Disposing DI Context"); _logger.LogInformation("Disposing DI Context");
_serviceProvider?.Dispose(); _serviceProvider?.Dispose();
// ensure we're not keeping the file open longer than the plugin is loaded // ensure we're not keeping the file open longer than the plugin is loaded
using (SqliteConnection sqliteConnection = new(_sqliteConnectionString)) using (SqliteConnection sqliteConnection = new(_sqliteConnectionString))
SqliteConnection.ClearPool(sqliteConnection); SqliteConnection.ClearPool(sqliteConnection);
}
} }
} }

View File

@ -1,12 +1,13 @@
using System; using System;
namespace Pal.Client.Extensions; namespace Pal.Client.Extensions
public static class GuidExtensions
{ {
public static string ToPartialId(this Guid g, int length = 13) public static class GuidExtensions
=> g.ToString().ToPartialId(); {
public static string ToPartialId(this Guid g, int length = 13)
=> g.ToString().ToPartialId();
public static string ToPartialId(this string s, int length = 13) public static string ToPartialId(this string s, int length = 13)
=> s.PadRight(length + 1).Substring(0, length); => s.PadRight(length + 1).Substring(0, length);
}
} }

View File

@ -3,33 +3,34 @@ using System.Runtime.InteropServices;
using System.Text; using System.Text;
using ImGuiNET; using ImGuiNET;
namespace Pal.Client.Extensions; namespace Pal.Client.Extensions
internal static class PalImGui
{ {
/// <summary> internal static class PalImGui
/// None of the default BeginTabItem methods allow using flags without making the tab have a close button for some reason.
/// </summary>
internal static unsafe bool BeginTabItemWithFlags(string label, ImGuiTabItemFlags flags)
{ {
int labelLength = Encoding.UTF8.GetByteCount(label); /// <summary>
byte* labelPtr = stackalloc byte[labelLength + 1]; /// None of the default BeginTabItem methods allow using flags without making the tab have a close button for some reason.
byte[] labelBytes = Encoding.UTF8.GetBytes(label); /// </summary>
internal static unsafe bool BeginTabItemWithFlags(string label, ImGuiTabItemFlags flags)
{
int labelLength = Encoding.UTF8.GetByteCount(label);
byte* labelPtr = stackalloc byte[labelLength + 1];
byte[] labelBytes = Encoding.UTF8.GetBytes(label);
Marshal.Copy(labelBytes, 0, (IntPtr)labelPtr, labelLength); Marshal.Copy(labelBytes, 0, (IntPtr)labelPtr, labelLength);
labelPtr[labelLength] = 0; labelPtr[labelLength] = 0;
return ImGuiNative.igBeginTabItem(labelPtr, null, flags) != 0; return ImGuiNative.igBeginTabItem(labelPtr, null, flags) != 0;
} }
public static void RadioButtonWrapped(string label, ref int choice, int value) public static void RadioButtonWrapped(string label, ref int choice, int value)
{ {
ImGui.BeginGroup(); ImGui.BeginGroup();
ImGui.RadioButton($"##radio{value}", value == choice); ImGui.RadioButton($"##radio{value}", value == choice);
ImGui.SameLine(); ImGui.SameLine();
ImGui.TextWrapped(label); ImGui.TextWrapped(label);
ImGui.EndGroup(); ImGui.EndGroup();
if (ImGui.IsItemClicked()) if (ImGui.IsItemClicked())
choice = value; choice = value;
}
} }
} }

View File

@ -1,28 +1,29 @@
using System; using System;
namespace Pal.Client.Floors; namespace Pal.Client.Floors
/// <summary>
/// This is a currently-visible marker.
/// </summary>
internal sealed class EphemeralLocation : MemoryLocation
{ {
public override bool Equals(object? obj) => obj is EphemeralLocation && base.Equals(obj); /// <summary>
/// This is a currently-visible marker.
public override int GetHashCode() => base.GetHashCode(); /// </summary>
internal sealed class EphemeralLocation : MemoryLocation
public static bool operator ==(EphemeralLocation? a, object? b)
{ {
return Equals(a, b); public override bool Equals(object? obj) => obj is EphemeralLocation && base.Equals(obj);
}
public static bool operator !=(EphemeralLocation? a, object? b) public override int GetHashCode() => base.GetHashCode();
{
return !Equals(a, b);
}
public override string ToString() public static bool operator ==(EphemeralLocation? a, object? b)
{ {
return $"EphemeralLocation(Position={Position}, Type={Type})"; return Equals(a, b);
}
public static bool operator !=(EphemeralLocation? a, object? b)
{
return !Equals(a, b);
}
public override string ToString()
{
return $"EphemeralLocation(Position={Position}, Type={Type})";
}
} }
} }

View File

@ -10,153 +10,154 @@ using Pal.Client.Floors.Tasks;
using Pal.Client.Net; using Pal.Client.Net;
using Pal.Common; using Pal.Common;
namespace Pal.Client.Floors; namespace Pal.Client.Floors
internal sealed class FloorService
{ {
private readonly IPalacePalConfiguration _configuration; internal sealed class FloorService
private readonly Cleanup _cleanup;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly IReadOnlyDictionary<ETerritoryType, MemoryTerritory> _territories;
private ConcurrentBag<EphemeralLocation> _ephemeralLocations = new();
public FloorService(IPalacePalConfiguration configuration, Cleanup cleanup,
IServiceScopeFactory serviceScopeFactory)
{ {
_configuration = configuration; private readonly IPalacePalConfiguration _configuration;
_cleanup = cleanup; private readonly Cleanup _cleanup;
_serviceScopeFactory = serviceScopeFactory; private readonly IServiceScopeFactory _serviceScopeFactory;
_territories = Enum.GetValues<ETerritoryType>().ToDictionary(o => o, o => new MemoryTerritory(o)); private readonly IReadOnlyDictionary<ETerritoryType, MemoryTerritory> _territories;
}
public IReadOnlyCollection<EphemeralLocation> EphemeralLocations => _ephemeralLocations; private ConcurrentBag<EphemeralLocation> _ephemeralLocations = new();
public bool IsImportRunning { get; private set; }
public void ChangeTerritory(ushort territoryType) public FloorService(IPalacePalConfiguration configuration, Cleanup cleanup,
{ IServiceScopeFactory serviceScopeFactory)
_ephemeralLocations = new ConcurrentBag<EphemeralLocation>();
if (typeof(ETerritoryType).IsEnumDefined(territoryType))
ChangeTerritory((ETerritoryType)territoryType);
}
private void ChangeTerritory(ETerritoryType newTerritory)
{
var territory = _territories[newTerritory];
if (territory.ReadyState == MemoryTerritory.EReadyState.NotLoaded)
{ {
territory.ReadyState = MemoryTerritory.EReadyState.Loading; _configuration = configuration;
new LoadTerritory(_serviceScopeFactory, _cleanup, territory).Start(); _cleanup = cleanup;
_serviceScopeFactory = serviceScopeFactory;
_territories = Enum.GetValues<ETerritoryType>().ToDictionary(o => o, o => new MemoryTerritory(o));
} }
}
public MemoryTerritory? GetTerritoryIfReady(ushort territoryType) public IReadOnlyCollection<EphemeralLocation> EphemeralLocations => _ephemeralLocations;
{ public bool IsImportRunning { get; private set; }
if (typeof(ETerritoryType).IsEnumDefined(territoryType))
return GetTerritoryIfReady((ETerritoryType)territoryType);
return null; public void ChangeTerritory(ushort territoryType)
}
public MemoryTerritory? GetTerritoryIfReady(ETerritoryType territoryType)
{
var territory = _territories[territoryType];
if (territory.ReadyState != MemoryTerritory.EReadyState.Ready)
return null;
return territory;
}
public bool IsReady(ushort territoryId) => GetTerritoryIfReady(territoryId) != null;
public bool MergePersistentLocations(
ETerritoryType territoryType,
IReadOnlyList<PersistentLocation> visibleLocations,
bool recreateLayout,
out List<PersistentLocation> locationsToSync)
{
MemoryTerritory? territory = GetTerritoryIfReady(territoryType);
locationsToSync = new();
if (territory == null)
return false;
var partialAccountId = _configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId();
var persistentLocations = territory.Locations.ToList();
List<PersistentLocation> markAsSeen = new();
List<PersistentLocation> newLocations = new();
foreach (var visibleLocation in visibleLocations)
{ {
PersistentLocation? existingLocation = persistentLocations.SingleOrDefault(x => x == visibleLocation); _ephemeralLocations = new ConcurrentBag<EphemeralLocation>();
if (existingLocation != null)
if (typeof(ETerritoryType).IsEnumDefined(territoryType))
ChangeTerritory((ETerritoryType)territoryType);
}
private void ChangeTerritory(ETerritoryType newTerritory)
{
var territory = _territories[newTerritory];
if (territory.ReadyState == MemoryTerritory.EReadyState.NotLoaded)
{ {
if (existingLocation is { Seen: false, LocalId: { } }) territory.ReadyState = MemoryTerritory.EReadyState.Loading;
new LoadTerritory(_serviceScopeFactory, _cleanup, territory).Start();
}
}
public MemoryTerritory? GetTerritoryIfReady(ushort territoryType)
{
if (typeof(ETerritoryType).IsEnumDefined(territoryType))
return GetTerritoryIfReady((ETerritoryType)territoryType);
return null;
}
public MemoryTerritory? GetTerritoryIfReady(ETerritoryType territoryType)
{
var territory = _territories[territoryType];
if (territory.ReadyState != MemoryTerritory.EReadyState.Ready)
return null;
return territory;
}
public bool IsReady(ushort territoryId) => GetTerritoryIfReady(territoryId) != null;
public bool MergePersistentLocations(
ETerritoryType territoryType,
IReadOnlyList<PersistentLocation> visibleLocations,
bool recreateLayout,
out List<PersistentLocation> locationsToSync)
{
MemoryTerritory? territory = GetTerritoryIfReady(territoryType);
locationsToSync = new();
if (territory == null)
return false;
var partialAccountId = _configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId();
var persistentLocations = territory.Locations.ToList();
List<PersistentLocation> markAsSeen = new();
List<PersistentLocation> newLocations = new();
foreach (var visibleLocation in visibleLocations)
{
PersistentLocation? existingLocation = persistentLocations.SingleOrDefault(x => x == visibleLocation);
if (existingLocation != null)
{ {
existingLocation.Seen = true; if (existingLocation is { Seen: false, LocalId: { } })
markAsSeen.Add(existingLocation); {
existingLocation.Seen = true;
markAsSeen.Add(existingLocation);
}
// This requires you to have seen a trap/hoard marker once per floor to synchronize this for older local states,
// markers discovered afterwards are automatically marked seen.
if (partialAccountId != null &&
existingLocation is { LocalId: { }, NetworkId: { }, RemoteSeenRequested: false } &&
!existingLocation.RemoteSeenOn.Contains(partialAccountId))
{
existingLocation.RemoteSeenRequested = true;
locationsToSync.Add(existingLocation);
}
continue;
} }
// This requires you to have seen a trap/hoard marker once per floor to synchronize this for older local states, territory.Locations.Add(visibleLocation);
// markers discovered afterwards are automatically marked seen. newLocations.Add(visibleLocation);
if (partialAccountId != null && recreateLayout = true;
existingLocation is { LocalId: { }, NetworkId: { }, RemoteSeenRequested: false } &&
!existingLocation.RemoteSeenOn.Contains(partialAccountId))
{
existingLocation.RemoteSeenRequested = true;
locationsToSync.Add(existingLocation);
}
continue;
} }
territory.Locations.Add(visibleLocation); if (markAsSeen.Count > 0)
newLocations.Add(visibleLocation); new MarkLocalSeen(_serviceScopeFactory, territory, markAsSeen).Start();
recreateLayout = true;
if (newLocations.Count > 0)
new SaveNewLocations(_serviceScopeFactory, territory, newLocations).Start();
return recreateLayout;
} }
if (markAsSeen.Count > 0) /// <returns>Whether the locations have changed</returns>
new MarkLocalSeen(_serviceScopeFactory, territory, markAsSeen).Start(); public bool MergeEphemeralLocations(IReadOnlyList<EphemeralLocation> visibleLocations, bool recreate)
if (newLocations.Count > 0)
new SaveNewLocations(_serviceScopeFactory, territory, newLocations).Start();
return recreateLayout;
}
/// <returns>Whether the locations have changed</returns>
public bool MergeEphemeralLocations(IReadOnlyList<EphemeralLocation> visibleLocations, bool recreate)
{
recreate |= _ephemeralLocations.Any(loc => visibleLocations.All(x => x != loc));
recreate |= visibleLocations.Any(loc => _ephemeralLocations.All(x => x != loc));
if (!recreate)
return false;
_ephemeralLocations.Clear();
foreach (var visibleLocation in visibleLocations)
_ephemeralLocations.Add(visibleLocation);
return true;
}
public void ResetAll()
{
IsImportRunning = false;
foreach (var memoryTerritory in _territories.Values)
{ {
lock (memoryTerritory.LockObj) recreate |= _ephemeralLocations.Any(loc => visibleLocations.All(x => x != loc));
memoryTerritory.Reset(); recreate |= visibleLocations.Any(loc => _ephemeralLocations.All(x => x != loc));
if (!recreate)
return false;
_ephemeralLocations.Clear();
foreach (var visibleLocation in visibleLocations)
_ephemeralLocations.Add(visibleLocation);
return true;
} }
}
public void SetToImportState() public void ResetAll()
{
IsImportRunning = true;
foreach (var memoryTerritory in _territories.Values)
{ {
lock (memoryTerritory.LockObj) IsImportRunning = false;
memoryTerritory.ReadyState = MemoryTerritory.EReadyState.Importing; foreach (var memoryTerritory in _territories.Values)
{
lock (memoryTerritory.LockObj)
memoryTerritory.Reset();
}
}
public void SetToImportState()
{
IsImportRunning = true;
foreach (var memoryTerritory in _territories.Values)
{
lock (memoryTerritory.LockObj)
memoryTerritory.ReadyState = MemoryTerritory.EReadyState.Importing;
}
} }
} }
} }

View File

@ -4,8 +4,10 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Pal.Client.Configuration; using Pal.Client.Configuration;
@ -16,449 +18,451 @@ using Pal.Client.Rendering;
using Pal.Client.Scheduled; using Pal.Client.Scheduled;
using Pal.Common; using Pal.Common;
namespace Pal.Client.Floors; namespace Pal.Client.Floors
internal sealed class FrameworkService : IDisposable
{ {
private readonly IServiceProvider _serviceProvider; internal sealed class FrameworkService : IDisposable
private readonly ILogger<FrameworkService> _logger;
private readonly IFramework _framework;
private readonly ConfigurationManager _configurationManager;
private readonly IPalacePalConfiguration _configuration;
private readonly IClientState _clientState;
private readonly TerritoryState _territoryState;
private readonly FloorService _floorService;
private readonly DebugState _debugState;
private readonly RenderAdapter _renderAdapter;
private readonly IObjectTable _objectTable;
private readonly RemoteApi _remoteApi;
internal Queue<IQueueOnFrameworkThread> EarlyEventQueue { get; } = new();
internal Queue<IQueueOnFrameworkThread> LateEventQueue { get; } = new();
internal ConcurrentQueue<nint> NextUpdateObjects { get; } = new();
public FrameworkService(
IServiceProvider serviceProvider,
ILogger<FrameworkService> logger,
IFramework framework,
ConfigurationManager configurationManager,
IPalacePalConfiguration configuration,
IClientState clientState,
TerritoryState territoryState,
FloorService floorService,
DebugState debugState,
RenderAdapter renderAdapter,
IObjectTable objectTable,
RemoteApi remoteApi)
{ {
_serviceProvider = serviceProvider; private readonly IServiceProvider _serviceProvider;
_logger = logger; private readonly ILogger<FrameworkService> _logger;
_framework = framework; private readonly Framework _framework;
_configurationManager = configurationManager; private readonly ConfigurationManager _configurationManager;
_configuration = configuration; private readonly IPalacePalConfiguration _configuration;
_clientState = clientState; private readonly ClientState _clientState;
_territoryState = territoryState; private readonly TerritoryState _territoryState;
_floorService = floorService; private readonly FloorService _floorService;
_debugState = debugState; private readonly DebugState _debugState;
_renderAdapter = renderAdapter; private readonly RenderAdapter _renderAdapter;
_objectTable = objectTable; private readonly ObjectTable _objectTable;
_remoteApi = remoteApi; private readonly RemoteApi _remoteApi;
_framework.Update += OnUpdate; internal Queue<IQueueOnFrameworkThread> EarlyEventQueue { get; } = new();
_configurationManager.Saved += OnSaved; internal Queue<IQueueOnFrameworkThread> LateEventQueue { get; } = new();
} internal ConcurrentQueue<nint> NextUpdateObjects { get; } = new();
public void Dispose() public FrameworkService(
{ IServiceProvider serviceProvider,
_framework.Update -= OnUpdate; ILogger<FrameworkService> logger,
_configurationManager.Saved -= OnSaved; Framework framework,
} ConfigurationManager configurationManager,
IPalacePalConfiguration configuration,
private void OnSaved(object? sender, IPalacePalConfiguration? config) ClientState clientState,
=> EarlyEventQueue.Enqueue(new QueuedConfigUpdate()); TerritoryState territoryState,
FloorService floorService,
private void OnUpdate(IFramework framework) DebugState debugState,
{ RenderAdapter renderAdapter,
if (_configuration.FirstUse) ObjectTable objectTable,
return; RemoteApi remoteApi)
try
{ {
bool recreateLayout = false; _serviceProvider = serviceProvider;
_logger = logger;
_framework = framework;
_configurationManager = configurationManager;
_configuration = configuration;
_clientState = clientState;
_territoryState = territoryState;
_floorService = floorService;
_debugState = debugState;
_renderAdapter = renderAdapter;
_objectTable = objectTable;
_remoteApi = remoteApi;
while (EarlyEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued)) _framework.Update += OnUpdate;
HandleQueued(queued, ref recreateLayout); _configurationManager.Saved += OnSaved;
}
if (_territoryState.LastTerritory != _clientState.TerritoryType) public void Dispose()
{ {
MemoryTerritory? oldTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory); _framework.Update -= OnUpdate;
if (oldTerritory != null) _configurationManager.Saved -= OnSaved;
oldTerritory.SyncState = ESyncState.NotAttempted; }
_territoryState.LastTerritory = _clientState.TerritoryType; private void OnSaved(object? sender, IPalacePalConfiguration? config)
NextUpdateObjects.Clear(); => EarlyEventQueue.Enqueue(new QueuedConfigUpdate());
_floorService.ChangeTerritory(_territoryState.LastTerritory); private void OnUpdate(Framework framework)
_territoryState.PomanderOfSight = PomanderState.Inactive; {
_territoryState.PomanderOfIntuition = PomanderState.Inactive; if (_configuration.FirstUse)
recreateLayout = true;
_debugState.Reset();
}
if (!_territoryState.IsInDeepDungeon() || !_floorService.IsReady(_territoryState.LastTerritory))
return; return;
if (_renderAdapter.RequireRedraw)
{
recreateLayout = true;
_renderAdapter.RequireRedraw = false;
}
ETerritoryType territoryType = (ETerritoryType)_territoryState.LastTerritory;
MemoryTerritory memoryTerritory = _floorService.GetTerritoryIfReady(territoryType)!;
if (_configuration.Mode == EMode.Online && memoryTerritory.SyncState == ESyncState.NotAttempted)
{
memoryTerritory.SyncState = ESyncState.Started;
Task.Run(async () => await DownloadLocationsForTerritory(_territoryState.LastTerritory));
}
while (LateEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued))
HandleQueued(queued, ref recreateLayout);
(IReadOnlyList<PersistentLocation> visiblePersistentMarkers,
IReadOnlyList<EphemeralLocation> visibleEphemeralMarkers) =
GetRelevantGameObjects();
HandlePersistentLocations(territoryType, visiblePersistentMarkers, recreateLayout);
if (_floorService.MergeEphemeralLocations(visibleEphemeralMarkers, recreateLayout))
RecreateEphemeralLayout();
}
catch (Exception e)
{
_debugState.SetFromException(e);
}
}
#region Render Markers
private void HandlePersistentLocations(ETerritoryType territoryType,
IReadOnlyList<PersistentLocation> visiblePersistentMarkers,
bool recreateLayout)
{
bool recreatePersistentLocations = _floorService.MergePersistentLocations(
territoryType,
visiblePersistentMarkers,
recreateLayout,
out List<PersistentLocation> locationsToSync);
recreatePersistentLocations |= CheckLocationsForPomanders(visiblePersistentMarkers);
if (locationsToSync.Count > 0)
{
Task.Run(async () =>
await SyncSeenMarkersForTerritory(_territoryState.LastTerritory, locationsToSync));
}
UploadLocations();
if (recreatePersistentLocations)
RecreatePersistentLayout(visiblePersistentMarkers);
}
private bool CheckLocationsForPomanders(IReadOnlyList<PersistentLocation> visibleLocations)
{
MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory);
if (memoryTerritory is { Locations.Count: > 0 } &&
(_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander ||
_configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander))
{
try try
{ {
foreach (var location in memoryTerritory.Locations) bool recreateLayout = false;
while (EarlyEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued))
HandleQueued(queued, ref recreateLayout);
if (_territoryState.LastTerritory != _clientState.TerritoryType)
{ {
bool isEnabled = DetermineVisibility(location, visibleLocations); MemoryTerritory? oldTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory);
if (location.RenderElement == null) if (oldTerritory != null)
{ oldTerritory.SyncState = ESyncState.NotAttempted;
if (isEnabled)
return true;
else
continue;
}
if (!location.RenderElement.IsValid) _territoryState.LastTerritory = _clientState.TerritoryType;
return true; NextUpdateObjects.Clear();
if (location.RenderElement.Enabled != isEnabled) _floorService.ChangeTerritory(_territoryState.LastTerritory);
location.RenderElement.Enabled = isEnabled; _territoryState.PomanderOfSight = PomanderState.Inactive;
_territoryState.PomanderOfIntuition = PomanderState.Inactive;
recreateLayout = true;
_debugState.Reset();
} }
if (!_territoryState.IsInDeepDungeon() || !_floorService.IsReady(_territoryState.LastTerritory))
return;
if (_renderAdapter.RequireRedraw)
{
recreateLayout = true;
_renderAdapter.RequireRedraw = false;
}
ETerritoryType territoryType = (ETerritoryType)_territoryState.LastTerritory;
MemoryTerritory memoryTerritory = _floorService.GetTerritoryIfReady(territoryType)!;
if (_configuration.Mode == EMode.Online && memoryTerritory.SyncState == ESyncState.NotAttempted)
{
memoryTerritory.SyncState = ESyncState.Started;
Task.Run(async () => await DownloadLocationsForTerritory(_territoryState.LastTerritory));
}
while (LateEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued))
HandleQueued(queued, ref recreateLayout);
(IReadOnlyList<PersistentLocation> visiblePersistentMarkers,
IReadOnlyList<EphemeralLocation> visibleEphemeralMarkers) =
GetRelevantGameObjects();
HandlePersistentLocations(territoryType, visiblePersistentMarkers, recreateLayout);
if (_floorService.MergeEphemeralLocations(visibleEphemeralMarkers, recreateLayout))
RecreateEphemeralLayout();
} }
catch (Exception e) catch (Exception e)
{ {
_debugState.SetFromException(e); _debugState.SetFromException(e);
return true;
} }
} }
return false; #region Render Markers
}
private void UploadLocations() private void HandlePersistentLocations(ETerritoryType territoryType,
{ IReadOnlyList<PersistentLocation> visiblePersistentMarkers,
MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory); bool recreateLayout)
if (memoryTerritory == null || memoryTerritory.SyncState != ESyncState.Complete)
return;
List<PersistentLocation> locationsToUpload = memoryTerritory.Locations
.Where(loc => loc.NetworkId == null && loc.UploadRequested == false)
.ToList();
if (locationsToUpload.Count > 0)
{ {
foreach (var location in locationsToUpload) bool recreatePersistentLocations = _floorService.MergePersistentLocations(
location.UploadRequested = true; territoryType,
visiblePersistentMarkers,
Task.Run(async () => recreateLayout,
await UploadLocationsForTerritory(_territoryState.LastTerritory, locationsToUpload)); out List<PersistentLocation> locationsToSync);
} recreatePersistentLocations |= CheckLocationsForPomanders(visiblePersistentMarkers);
} if (locationsToSync.Count > 0)
private void RecreatePersistentLayout(IReadOnlyList<PersistentLocation> visibleMarkers)
{
_renderAdapter.ResetLayer(ELayer.TrapHoard);
MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory);
if (memoryTerritory == null)
return;
List<IRenderElement> elements = new();
foreach (var location in memoryTerritory.Locations)
{
if (location.Type == MemoryLocation.EType.Trap)
{ {
CreateRenderElement(location, elements, DetermineVisibility(location, visibleMarkers), Task.Run(async () =>
_configuration.DeepDungeons.Traps); await SyncSeenMarkersForTerritory(_territoryState.LastTerritory, locationsToSync));
} }
else if (location.Type == MemoryLocation.EType.Hoard)
UploadLocations();
if (recreatePersistentLocations)
RecreatePersistentLayout(visiblePersistentMarkers);
}
private bool CheckLocationsForPomanders(IReadOnlyList<PersistentLocation> visibleLocations)
{
MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory);
if (memoryTerritory is { Locations.Count: > 0 } &&
(_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander ||
_configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander))
{ {
CreateRenderElement(location, elements, DetermineVisibility(location, visibleMarkers), try
_configuration.DeepDungeons.HoardCoffers); {
foreach (var location in memoryTerritory.Locations)
{
uint desiredColor = DetermineColor(location, visibleLocations);
if (location.RenderElement == null || !location.RenderElement.IsValid)
return true;
if (location.RenderElement.Color != desiredColor)
location.RenderElement.Color = desiredColor;
}
}
catch (Exception e)
{
_debugState.SetFromException(e);
return true;
}
}
return false;
}
private void UploadLocations()
{
MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory);
if (memoryTerritory == null || memoryTerritory.SyncState != ESyncState.Complete)
return;
List<PersistentLocation> locationsToUpload = memoryTerritory.Locations
.Where(loc => loc.NetworkId == null && loc.UploadRequested == false)
.ToList();
if (locationsToUpload.Count > 0)
{
foreach (var location in locationsToUpload)
location.UploadRequested = true;
Task.Run(async () =>
await UploadLocationsForTerritory(_territoryState.LastTerritory, locationsToUpload));
} }
} }
if (elements.Count == 0) private void RecreatePersistentLayout(IReadOnlyList<PersistentLocation> visibleMarkers)
return;
_renderAdapter.SetLayer(ELayer.TrapHoard, elements);
}
private void RecreateEphemeralLayout()
{
_renderAdapter.ResetLayer(ELayer.RegularCoffers);
List<IRenderElement> elements = new();
foreach (var location in _floorService.EphemeralLocations)
{ {
if (location.Type == MemoryLocation.EType.SilverCoffer && _renderAdapter.ResetLayer(ELayer.TrapHoard);
_configuration.DeepDungeons.SilverCoffers.Show)
MemoryTerritory? memoryTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory);
if (memoryTerritory == null)
return;
List<IRenderElement> elements = new();
foreach (var location in memoryTerritory.Locations)
{ {
CreateRenderElement(location, elements, true, _configuration.DeepDungeons.SilverCoffers); if (location.Type == MemoryLocation.EType.Trap)
{
CreateRenderElement(location, elements, DetermineColor(location, visibleMarkers),
_configuration.DeepDungeons.Traps);
}
else if (location.Type == MemoryLocation.EType.Hoard)
{
CreateRenderElement(location, elements, DetermineColor(location, visibleMarkers),
_configuration.DeepDungeons.HoardCoffers);
}
} }
else if (location.Type == MemoryLocation.EType.GoldCoffer &&
_configuration.DeepDungeons.GoldCoffers.Show) if (elements.Count == 0)
return;
_renderAdapter.SetLayer(ELayer.TrapHoard, elements);
}
private void RecreateEphemeralLayout()
{
_renderAdapter.ResetLayer(ELayer.RegularCoffers);
List<IRenderElement> elements = new();
foreach (var location in _floorService.EphemeralLocations)
{ {
CreateRenderElement(location, elements, true, _configuration.DeepDungeons.GoldCoffers); if (location.Type == MemoryLocation.EType.SilverCoffer &&
_configuration.DeepDungeons.SilverCoffers.Show)
{
CreateRenderElement(location, elements, DetermineColor(location),
_configuration.DeepDungeons.SilverCoffers);
}
else if (location.Type == MemoryLocation.EType.GoldCoffer &&
_configuration.DeepDungeons.GoldCoffers.Show)
{
CreateRenderElement(location, elements, DetermineColor(location),
_configuration.DeepDungeons.GoldCoffers);
}
}
if (elements.Count == 0)
return;
_renderAdapter.SetLayer(ELayer.RegularCoffers, elements);
}
private uint DetermineColor(PersistentLocation location, IReadOnlyList<PersistentLocation> visibleLocations)
{
switch (location.Type)
{
case MemoryLocation.EType.Trap
when _territoryState.PomanderOfSight == PomanderState.Inactive ||
!_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander ||
visibleLocations.Any(x => x == location):
return _configuration.DeepDungeons.Traps.Color;
case MemoryLocation.EType.Hoard
when _territoryState.PomanderOfIntuition == PomanderState.Inactive ||
!_configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander ||
visibleLocations.Any(x => x == location):
return _configuration.DeepDungeons.HoardCoffers.Color;
default:
return RenderData.ColorInvisible;
} }
} }
if (elements.Count == 0) private uint DetermineColor(EphemeralLocation location)
return;
_renderAdapter.SetLayer(ELayer.RegularCoffers, elements);
}
private bool DetermineVisibility(PersistentLocation location, IReadOnlyList<PersistentLocation> visibleLocations)
{
switch (location.Type)
{ {
case MemoryLocation.EType.Trap return location.Type switch
when _territoryState.PomanderOfSight == PomanderState.Inactive ||
!_configuration.DeepDungeons.Traps.OnlyVisibleAfterPomander ||
visibleLocations.Any(x => x == location):
return true;
case MemoryLocation.EType.Hoard
when _territoryState.PomanderOfIntuition == PomanderState.Inactive ||
!_configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander ||
visibleLocations.Any(x => x == location):
return true;
default:
return false;
}
}
private void CreateRenderElement(MemoryLocation location, List<IRenderElement> elements, bool enabled,
MarkerConfiguration config)
{
if (!config.Show)
{
location.RenderElement = null;
return;
}
var element =
_renderAdapter.CreateElement(location.Type, location.Position, enabled, config.Color, config.Fill);
location.RenderElement = element;
elements.Add(element);
}
#endregion
#region Up-/Download
private async Task DownloadLocationsForTerritory(ushort territoryId)
{
try
{
_logger.LogInformation("Downloading territory {Territory} from server", (ETerritoryType)territoryId);
var (success, downloadedMarkers) = await _remoteApi.DownloadRemoteMarkers(territoryId);
LateEventQueue.Enqueue(new QueuedSyncResponse
{ {
Type = SyncType.Download, MemoryLocation.EType.SilverCoffer => _configuration.DeepDungeons.SilverCoffers.Color,
TerritoryType = territoryId, MemoryLocation.EType.GoldCoffer => _configuration.DeepDungeons.GoldCoffers.Color,
Success = success, _ => RenderData.ColorInvisible
Locations = downloadedMarkers };
});
} }
catch (Exception e)
{
_debugState.SetFromException(e);
}
}
private async Task UploadLocationsForTerritory(ushort territoryId, List<PersistentLocation> locationsToUpload) private void CreateRenderElement(MemoryLocation location, List<IRenderElement> elements, uint color,
{ MarkerConfiguration config)
try
{ {
_logger.LogInformation("Uploading {Count} locations for territory {Territory} to server", if (!config.Show)
locationsToUpload.Count, (ETerritoryType)territoryId); return;
var (success, uploadedLocations) = await _remoteApi.UploadLocations(territoryId, locationsToUpload);
LateEventQueue.Enqueue(new QueuedSyncResponse var element = _renderAdapter.CreateElement(location.Type, location.Position, color, config.Fill);
location.RenderElement = element;
elements.Add(element);
}
#endregion
#region Up-/Download
private async Task DownloadLocationsForTerritory(ushort territoryId)
{
try
{ {
Type = SyncType.Upload, _logger.LogInformation("Downloading territory {Territory} from server", (ETerritoryType)territoryId);
TerritoryType = territoryId, var (success, downloadedMarkers) = await _remoteApi.DownloadRemoteMarkers(territoryId);
Success = success, LateEventQueue.Enqueue(new QueuedSyncResponse
Locations = uploadedLocations {
}); Type = SyncType.Download,
} TerritoryType = territoryId,
catch (Exception e) Success = success,
{ Locations = downloadedMarkers
_debugState.SetFromException(e); });
} }
} catch (Exception e)
private async Task SyncSeenMarkersForTerritory(ushort territoryId,
IReadOnlyList<PersistentLocation> locationsToUpdate)
{
try
{
_logger.LogInformation("Syncing {Count} seen locations for territory {Territory} to server",
locationsToUpdate.Count, (ETerritoryType)territoryId);
var success = await _remoteApi.MarkAsSeen(territoryId, locationsToUpdate);
LateEventQueue.Enqueue(new QueuedSyncResponse
{ {
Type = SyncType.MarkSeen, _debugState.SetFromException(e);
TerritoryType = territoryId, }
Success = success,
Locations = locationsToUpdate,
});
} }
catch (Exception e)
private async Task UploadLocationsForTerritory(ushort territoryId, List<PersistentLocation> locationsToUpload)
{ {
_debugState.SetFromException(e); try
}
}
#endregion
private (IReadOnlyList<PersistentLocation>, IReadOnlyList<EphemeralLocation>) GetRelevantGameObjects()
{
List<PersistentLocation> persistentLocations = new();
List<EphemeralLocation> ephemeralLocations = new();
for (int i = 246; i < _objectTable.Length; i++)
{
IGameObject? obj = _objectTable[i];
if (obj == null)
continue;
switch ((uint)Marshal.ReadInt32(obj.Address + 128))
{ {
case 2007182: _logger.LogInformation("Uploading {Count} locations for territory {Territory} to server",
case 2007183: locationsToUpload.Count, (ETerritoryType)territoryId);
case 2007184: var (success, uploadedLocations) = await _remoteApi.UploadLocations(territoryId, locationsToUpload);
case 2007185: LateEventQueue.Enqueue(new QueuedSyncResponse
case 2007186: {
case 2009504: Type = SyncType.Upload,
case 2013284: TerritoryType = territoryId,
Success = success,
Locations = uploadedLocations
});
}
catch (Exception e)
{
_debugState.SetFromException(e);
}
}
private async Task SyncSeenMarkersForTerritory(ushort territoryId,
IReadOnlyList<PersistentLocation> locationsToUpdate)
{
try
{
_logger.LogInformation("Syncing {Count} seen locations for territory {Territory} to server",
locationsToUpdate.Count, (ETerritoryType)territoryId);
var success = await _remoteApi.MarkAsSeen(territoryId, locationsToUpdate);
LateEventQueue.Enqueue(new QueuedSyncResponse
{
Type = SyncType.MarkSeen,
TerritoryType = territoryId,
Success = success,
Locations = locationsToUpdate,
});
}
catch (Exception e)
{
_debugState.SetFromException(e);
}
}
#endregion
private (IReadOnlyList<PersistentLocation>, IReadOnlyList<EphemeralLocation>) GetRelevantGameObjects()
{
List<PersistentLocation> persistentLocations = new();
List<EphemeralLocation> ephemeralLocations = new();
for (int i = 246; i < _objectTable.Length; i++)
{
GameObject? obj = _objectTable[i];
if (obj == null)
continue;
switch ((uint)Marshal.ReadInt32(obj.Address + 128))
{
case 2007182:
case 2007183:
case 2007184:
case 2007185:
case 2007186:
case 2009504:
case 2013284:
persistentLocations.Add(new PersistentLocation
{
Type = MemoryLocation.EType.Trap,
Position = obj.Position,
Seen = true,
Source = ClientLocation.ESource.SeenLocally,
});
break;
case 2007542:
case 2007543:
persistentLocations.Add(new PersistentLocation
{
Type = MemoryLocation.EType.Hoard,
Position = obj.Position,
Seen = true,
Source = ClientLocation.ESource.SeenLocally,
});
break;
case 2007357:
ephemeralLocations.Add(new EphemeralLocation
{
Type = MemoryLocation.EType.SilverCoffer,
Position = obj.Position,
Seen = true,
});
break;
case 2007358:
ephemeralLocations.Add(new EphemeralLocation
{
Type = MemoryLocation.EType.GoldCoffer,
Position = obj.Position,
Seen = true
});
break;
}
}
while (NextUpdateObjects.TryDequeue(out nint address))
{
var obj = _objectTable.FirstOrDefault(x => x.Address == address);
if (obj != null && obj.Position.Length() > 0.1)
{
persistentLocations.Add(new PersistentLocation persistentLocations.Add(new PersistentLocation
{ {
Type = MemoryLocation.EType.Trap, Type = MemoryLocation.EType.Trap,
Position = obj.Position, Position = obj.Position,
Seen = true, Seen = true,
Source = ClientLocation.ESource.SeenLocally, Source = ClientLocation.ESource.ExplodedLocally,
});
break;
case 2007542:
case 2007543:
persistentLocations.Add(new PersistentLocation
{
Type = MemoryLocation.EType.Hoard,
Position = obj.Position,
Seen = true,
Source = ClientLocation.ESource.SeenLocally,
}); });
break; }
case 2007357:
ephemeralLocations.Add(new EphemeralLocation
{
Type = MemoryLocation.EType.SilverCoffer,
Position = obj.Position,
Seen = true,
});
break;
case 2007358:
ephemeralLocations.Add(new EphemeralLocation
{
Type = MemoryLocation.EType.GoldCoffer,
Position = obj.Position,
Seen = true
});
break;
} }
return (persistentLocations, ephemeralLocations);
} }
while (NextUpdateObjects.TryDequeue(out nint address)) private void HandleQueued(IQueueOnFrameworkThread queued, ref bool recreateLayout)
{ {
var obj = _objectTable.FirstOrDefault(x => x.Address == address); Type handlerType = typeof(IQueueOnFrameworkThread.Handler<>).MakeGenericType(queued.GetType());
if (obj != null && obj.Position.Length() > 0.1) var handler = (IQueueOnFrameworkThread.IHandler)_serviceProvider.GetRequiredService(handlerType);
{
persistentLocations.Add(new PersistentLocation handler.RunIfCompatible(queued, ref recreateLayout);
{
Type = MemoryLocation.EType.Trap,
Position = obj.Position,
Seen = true,
Source = ClientLocation.ESource.ExplodedLocally,
});
}
} }
return (persistentLocations, ephemeralLocations);
}
private void HandleQueued(IQueueOnFrameworkThread queued, ref bool recreateLayout)
{
Type handlerType = typeof(IQueueOnFrameworkThread.Handler<>).MakeGenericType(queued.GetType());
var handler = (IQueueOnFrameworkThread.IHandler)_serviceProvider.GetRequiredService(handlerType);
handler.RunIfCompatible(queued, ref recreateLayout);
} }
} }

View File

@ -5,62 +5,63 @@ using Pal.Client.Rendering;
using Pal.Common; using Pal.Common;
using Palace; using Palace;
namespace Pal.Client.Floors; namespace Pal.Client.Floors
/// <summary>
/// Base class for <see cref="MemoryLocation"/> and <see cref="EphemeralLocation"/>.
/// </summary>
internal abstract class MemoryLocation
{ {
public required EType Type { get; init; } /// <summary>
public required Vector3 Position { get; init; } /// Base class for <see cref="MemoryLocation"/> and <see cref="EphemeralLocation"/>.
public bool Seen { get; set; } /// </summary>
internal abstract class MemoryLocation
public IRenderElement? RenderElement { get; set; }
public enum EType
{ {
Unknown, public required EType Type { get; init; }
public required Vector3 Position { get; init; }
public bool Seen { get; set; }
Trap, public IRenderElement? RenderElement { get; set; }
Hoard,
SilverCoffer, public enum EType
GoldCoffer, {
Unknown,
Trap,
Hoard,
SilverCoffer,
GoldCoffer,
}
public override bool Equals(object? obj)
{
return obj is MemoryLocation otherLocation &&
Type == otherLocation.Type &&
PalaceMath.IsNearlySamePosition(Position, otherLocation.Position);
}
public override int GetHashCode()
{
return HashCode.Combine(Type, PalaceMath.GetHashCode(Position));
}
} }
public override bool Equals(object? obj) internal static class ETypeExtensions
{ {
return obj is MemoryLocation otherLocation && public static MemoryLocation.EType ToMemoryType(this ObjectType objectType)
Type == otherLocation.Type && {
PalaceMath.IsNearlySamePosition(Position, otherLocation.Position); return objectType switch
} {
ObjectType.Trap => MemoryLocation.EType.Trap,
ObjectType.Hoard => MemoryLocation.EType.Hoard,
_ => throw new ArgumentOutOfRangeException(nameof(objectType), objectType, null)
};
}
public override int GetHashCode() public static ObjectType ToObjectType(this MemoryLocation.EType type)
{ {
return HashCode.Combine(Type, PalaceMath.GetHashCode(Position)); return type switch
} {
} MemoryLocation.EType.Trap => ObjectType.Trap,
MemoryLocation.EType.Hoard => ObjectType.Hoard,
internal static class ETypeExtensions _ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
{ };
public static MemoryLocation.EType ToMemoryType(this ObjectType objectType) }
{
return objectType switch
{
ObjectType.Trap => MemoryLocation.EType.Trap,
ObjectType.Hoard => MemoryLocation.EType.Hoard,
_ => throw new ArgumentOutOfRangeException(nameof(objectType), objectType, null)
};
}
public static ObjectType ToObjectType(this MemoryLocation.EType type)
{
return type switch
{
MemoryLocation.EType.Trap => ObjectType.Trap,
MemoryLocation.EType.Hoard => ObjectType.Hoard,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
} }
} }

View File

@ -5,58 +5,59 @@ using Pal.Client.Configuration;
using Pal.Client.Scheduled; using Pal.Client.Scheduled;
using Pal.Common; using Pal.Common;
namespace Pal.Client.Floors; namespace Pal.Client.Floors
/// <summary>
/// A single set of floors loaded entirely in memory, can be e.g. POTD 51-60.
/// </summary>
internal sealed class MemoryTerritory
{ {
public MemoryTerritory(ETerritoryType territoryType) /// <summary>
/// A single set of floors loaded entirely in memory, can be e.g. POTD 51-60.
/// </summary>
internal sealed class MemoryTerritory
{ {
TerritoryType = territoryType; public MemoryTerritory(ETerritoryType territoryType)
} {
TerritoryType = territoryType;
}
public ETerritoryType TerritoryType { get; } public ETerritoryType TerritoryType { get; }
public EReadyState ReadyState { get; set; } = EReadyState.NotLoaded; public EReadyState ReadyState { get; set; } = EReadyState.NotLoaded;
public ESyncState SyncState { get; set; } = ESyncState.NotAttempted; public ESyncState SyncState { get; set; } = ESyncState.NotAttempted;
public ConcurrentBag<PersistentLocation> Locations { get; } = new(); public ConcurrentBag<PersistentLocation> Locations { get; } = new();
public object LockObj { get; } = new(); public object LockObj { get; } = new();
public void Initialize(IEnumerable<PersistentLocation> locations) public void Initialize(IEnumerable<PersistentLocation> locations)
{ {
Locations.Clear(); Locations.Clear();
foreach (var location in locations) foreach (var location in locations)
Locations.Add(location); Locations.Add(location);
ReadyState = EReadyState.Ready; ReadyState = EReadyState.Ready;
} }
public void Reset() public void Reset()
{ {
Locations.Clear(); Locations.Clear();
SyncState = ESyncState.NotAttempted; SyncState = ESyncState.NotAttempted;
ReadyState = EReadyState.NotLoaded; ReadyState = EReadyState.NotLoaded;
} }
public enum EReadyState public enum EReadyState
{ {
NotLoaded, NotLoaded,
/// <summary> /// <summary>
/// Currently loading from the database. /// Currently loading from the database.
/// </summary> /// </summary>
Loading, Loading,
/// <summary> /// <summary>
/// Locations loaded, no import running. /// Locations loaded, no import running.
/// </summary> /// </summary>
Ready, Ready,
/// <summary> /// <summary>
/// Import running, should probably not interact with this too much. /// Import running, should probably not interact with this too much.
/// </summary> /// </summary>
Importing, Importing,
}
} }
} }

View File

@ -1,99 +1,101 @@
using System; using System;
using System.Numerics; using System.Numerics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.Gui;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using ImGuiNET; using ImGuiNET;
namespace Pal.Client.Floors; namespace Pal.Client.Floors
/// <summary>
/// This isn't very useful for running deep dungeons normally, but it is for plugin dev.
///
/// Needs the corresponding beta feature to be enabled.
/// </summary>
internal sealed class ObjectTableDebug : IDisposable
{ {
public const string FeatureName = nameof(ObjectTableDebug); /// <summary>
/// This isn't very useful for running deep dungeons normally, but it is for plugin dev.
private readonly IDalamudPluginInterface _pluginInterface; ///
private readonly IObjectTable _objectTable; /// Needs the corresponding beta feature to be enabled.
private readonly IGameGui _gameGui; /// </summary>
private readonly IClientState _clientState; internal sealed class ObjectTableDebug : IDisposable
public ObjectTableDebug(IDalamudPluginInterface pluginInterface, IObjectTable objectTable, IGameGui gameGui,
IClientState clientState)
{ {
_pluginInterface = pluginInterface; public const string FeatureName = nameof(ObjectTableDebug);
_objectTable = objectTable;
_gameGui = gameGui;
_clientState = clientState;
_pluginInterface.UiBuilder.Draw += Draw; private readonly DalamudPluginInterface _pluginInterface;
} private readonly ObjectTable _objectTable;
private readonly GameGui _gameGui;
private readonly ClientState _clientState;
private void Draw() public ObjectTableDebug(DalamudPluginInterface pluginInterface, ObjectTable objectTable, GameGui gameGui, ClientState clientState)
{
int index = 0;
foreach (IGameObject obj in _objectTable)
{ {
if (obj is IEventObj eventObj && string.IsNullOrEmpty(eventObj.Name.ToString())) _pluginInterface = pluginInterface;
_objectTable = objectTable;
_gameGui = gameGui;
_clientState = clientState;
_pluginInterface.UiBuilder.Draw += Draw;
}
private void Draw()
{
int index = 0;
foreach (GameObject obj in _objectTable)
{ {
++index; if (obj is EventObj eventObj && string.IsNullOrEmpty(eventObj.Name.ToString()))
int model = Marshal.ReadInt32(obj.Address + 128);
if (_gameGui.WorldToScreen(obj.Position, out var screenCoords))
{ {
// So, while WorldToScreen will return false if the point is off of game client screen, to ++index;
// to avoid performance issues, we have to manually determine if creating a window would int model = Marshal.ReadInt32(obj.Address + 128);
// produce a new viewport, and skip rendering it if so
float distance = DistanceToPlayer(obj.Position);
var objectText =
$"{obj.Address.ToInt64():X}:{obj.EntityId:X}[{index}]\nkind: {obj.ObjectKind} sub: {obj.SubKind}\nmodel: {model}\nname: {obj.Name}\ndata id: {obj.DataId}";
var screenPos = ImGui.GetMainViewport().Pos; if (_gameGui.WorldToScreen(obj.Position, out var screenCoords))
var screenSize = ImGui.GetMainViewport().Size; {
// So, while WorldToScreen will return false if the point is off of game client screen, to
// to avoid performance issues, we have to manually determine if creating a window would
// produce a new viewport, and skip rendering it if so
float distance = DistanceToPlayer(obj.Position);
var objectText =
$"{obj.Address.ToInt64():X}:{obj.ObjectId:X}[{index}]\nkind: {obj.ObjectKind} sub: {obj.SubKind}\nmodel: {model}\nname: {obj.Name}\ndata id: {obj.DataId}";
var windowSize = ImGui.CalcTextSize(objectText); var screenPos = ImGui.GetMainViewport().Pos;
var screenSize = ImGui.GetMainViewport().Size;
// Add some extra safety padding var windowSize = ImGui.CalcTextSize(objectText);
windowSize.X += ImGui.GetStyle().WindowPadding.X + 10;
windowSize.Y += ImGui.GetStyle().WindowPadding.Y + 10;
if (screenCoords.X + windowSize.X > screenPos.X + screenSize.X || // Add some extra safety padding
screenCoords.Y + windowSize.Y > screenPos.Y + screenSize.Y) windowSize.X += ImGui.GetStyle().WindowPadding.X + 10;
continue; windowSize.Y += ImGui.GetStyle().WindowPadding.Y + 10;
if (distance > 50f) if (screenCoords.X + windowSize.X > screenPos.X + screenSize.X ||
continue; screenCoords.Y + windowSize.Y > screenPos.Y + screenSize.Y)
continue;
ImGui.SetNextWindowPos(new Vector2(screenCoords.X, screenCoords.Y)); if (distance > 50f)
continue;
ImGui.SetNextWindowBgAlpha(Math.Max(1f - (distance / 50f), 0.2f)); ImGui.SetNextWindowPos(new Vector2(screenCoords.X, screenCoords.Y));
if (ImGui.Begin(
$"PalacePal_{nameof(ObjectTableDebug)}_{index}", ImGui.SetNextWindowBgAlpha(Math.Max(1f - (distance / 50f), 0.2f));
ImGuiWindowFlags.NoDecoration | if (ImGui.Begin(
ImGuiWindowFlags.AlwaysAutoResize | $"PalacePal_{nameof(ObjectTableDebug)}_{index}",
ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoDecoration |
ImGuiWindowFlags.NoMove | ImGuiWindowFlags.AlwaysAutoResize |
ImGuiWindowFlags.NoMouseInputs | ImGuiWindowFlags.NoSavedSettings |
ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoMove |
ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoMouseInputs |
ImGuiWindowFlags.NoNav)) ImGuiWindowFlags.NoDocking |
ImGui.Text(objectText); ImGuiWindowFlags.NoFocusOnAppearing |
ImGui.End(); ImGuiWindowFlags.NoNav))
ImGui.Text(objectText);
ImGui.End();
}
} }
} }
} }
}
private float DistanceToPlayer(Vector3 center) private float DistanceToPlayer(Vector3 center)
=> Vector3.Distance(_clientState.LocalPlayer?.Position ?? Vector3.Zero, center); => Vector3.Distance(_clientState.LocalPlayer?.Position ?? Vector3.Zero, center);
public void Dispose() public void Dispose()
{ {
_pluginInterface.UiBuilder.Draw -= Draw; _pluginInterface.UiBuilder.Draw -= Draw;
}
} }
} }

View File

@ -2,53 +2,54 @@
using System.Collections.Generic; using System.Collections.Generic;
using Pal.Client.Database; using Pal.Client.Database;
namespace Pal.Client.Floors; namespace Pal.Client.Floors
/// <summary>
/// A <see cref="ClientLocation"/> loaded in memory, with certain extra attributes as needed.
/// </summary>
internal sealed class PersistentLocation : MemoryLocation
{ {
/// <see cref="ClientLocation.LocalId"/>
public int? LocalId { get; set; }
/// <summary> /// <summary>
/// Network id for the server you're currently connected to. /// A <see cref="ClientLocation"/> loaded in memory, with certain extra attributes as needed.
/// </summary> /// </summary>
public Guid? NetworkId { get; set; } internal sealed class PersistentLocation : MemoryLocation
/// <summary>
/// For markers that the server you're connected to doesn't know: Whether this was requested to be uploaded, to avoid duplicate requests.
/// </summary>
public bool UploadRequested { get; set; }
/// <see cref="ClientLocation.RemoteEncounters"/>
///
public List<string> RemoteSeenOn { get; set; } = new();
/// <summary>
/// Whether this marker was requested to be seen, to avoid duplicate requests.
/// </summary>
public bool RemoteSeenRequested { get; set; }
public ClientLocation.ESource Source { get; init; }
public override bool Equals(object? obj) => obj is PersistentLocation && base.Equals(obj);
public override int GetHashCode() => base.GetHashCode();
public static bool operator ==(PersistentLocation? a, object? b)
{ {
return Equals(a, b); /// <see cref="ClientLocation.LocalId"/>
} public int? LocalId { get; set; }
public static bool operator !=(PersistentLocation? a, object? b) /// <summary>
{ /// Network id for the server you're currently connected to.
return !Equals(a, b); /// </summary>
} public Guid? NetworkId { get; set; }
public override string ToString() /// <summary>
{ /// For markers that the server you're connected to doesn't know: Whether this was requested to be uploaded, to avoid duplicate requests.
return $"PersistentLocation(Position={Position}, Type={Type})"; /// </summary>
public bool UploadRequested { get; set; }
/// <see cref="ClientLocation.RemoteEncounters"/>
///
public List<string> RemoteSeenOn { get; set; } = new();
/// <summary>
/// Whether this marker was requested to be seen, to avoid duplicate requests.
/// </summary>
public bool RemoteSeenRequested { get; set; }
public ClientLocation.ESource Source { get; init; }
public override bool Equals(object? obj) => obj is PersistentLocation && base.Equals(obj);
public override int GetHashCode() => base.GetHashCode();
public static bool operator ==(PersistentLocation? a, object? b)
{
return Equals(a, b);
}
public static bool operator !=(PersistentLocation? a, object? b)
{
return !Equals(a, b);
}
public override string ToString()
{
return $"PersistentLocation(Position={Position}, Type={Type})";
}
} }
} }

View File

@ -4,43 +4,38 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Pal.Client.Database; using Pal.Client.Database;
namespace Pal.Client.Floors.Tasks; namespace Pal.Client.Floors.Tasks
internal abstract class DbTask<T>
where T : DbTask<T>
{ {
private readonly IServiceScopeFactory _serviceScopeFactory; internal abstract class DbTask<T>
where T : DbTask<T>
protected DbTask(IServiceScopeFactory serviceScopeFactory)
{ {
_serviceScopeFactory = serviceScopeFactory; private readonly IServiceScopeFactory _serviceScopeFactory;
}
public void Start() protected DbTask(IServiceScopeFactory serviceScopeFactory)
{
Task.Run(() =>
{ {
try _serviceScopeFactory = serviceScopeFactory;
}
public void Start()
{
Task.Run(() =>
{ {
using var scope = _serviceScopeFactory.CreateScope();
ILogger<T> logger = scope.ServiceProvider.GetRequiredService<ILogger<T>>();
try try
{ {
using var scope = _serviceScopeFactory.CreateScope();
ILogger<T> logger = scope.ServiceProvider.GetRequiredService<ILogger<T>>();
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>(); using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
Run(dbContext, logger); Run(dbContext, logger);
} }
catch (Exception e) catch (Exception e)
{ {
logger.LogError(e, "Failed to run DbTask"); DependencyInjectionContext.LoggerProvider.CreateLogger<DbTask<T>>()
.LogError(e, "Failed to run DbTask");
} }
} });
catch (Exception) }
{
// nothing we can do here but catch it, if we don't we crash the game
}
});
}
protected abstract void Run(PalClientContext dbContext, ILogger<T> logger); protected abstract void Run(PalClientContext dbContext, ILogger<T> logger);
}
} }

View File

@ -7,72 +7,73 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Pal.Client.Database; using Pal.Client.Database;
namespace Pal.Client.Floors.Tasks; namespace Pal.Client.Floors.Tasks
internal sealed class LoadTerritory : DbTask<LoadTerritory>
{ {
private readonly Cleanup _cleanup; internal sealed class LoadTerritory : DbTask<LoadTerritory>
private readonly MemoryTerritory _territory;
public LoadTerritory(IServiceScopeFactory serviceScopeFactory,
Cleanup cleanup,
MemoryTerritory territory)
: base(serviceScopeFactory)
{ {
_cleanup = cleanup; private readonly Cleanup _cleanup;
_territory = territory; private readonly MemoryTerritory _territory;
}
protected override void Run(PalClientContext dbContext, ILogger<LoadTerritory> logger) public LoadTerritory(IServiceScopeFactory serviceScopeFactory,
{ Cleanup cleanup,
lock (_territory.LockObj) MemoryTerritory territory)
: base(serviceScopeFactory)
{ {
if (_territory.ReadyState != MemoryTerritory.EReadyState.Loading) _cleanup = cleanup;
_territory = territory;
}
protected override void Run(PalClientContext dbContext, ILogger<LoadTerritory> logger)
{
lock (_territory.LockObj)
{ {
logger.LogInformation("Territory {Territory} is in state {State}", _territory.TerritoryType, if (_territory.ReadyState != MemoryTerritory.EReadyState.Loading)
_territory.ReadyState); {
return; logger.LogInformation("Territory {Territory} is in state {State}", _territory.TerritoryType,
_territory.ReadyState);
return;
}
logger.LogInformation("Loading territory {Territory}", _territory.TerritoryType);
// purge outdated locations
_cleanup.Purge(dbContext, _territory.TerritoryType);
// load good locations
List<ClientLocation> locations = dbContext.Locations
.Where(o => o.TerritoryType == (ushort)_territory.TerritoryType)
.Include(o => o.ImportedBy)
.Include(o => o.RemoteEncounters)
.AsSplitQuery()
.ToList();
_territory.Initialize(locations.Select(ToMemoryLocation));
logger.LogInformation("Loaded {Count} locations for territory {Territory}", locations.Count,
_territory.TerritoryType);
} }
}
logger.LogInformation("Loading territory {Territory}", _territory.TerritoryType); public static PersistentLocation ToMemoryLocation(ClientLocation location)
{
return new PersistentLocation
{
LocalId = location.LocalId,
Type = ToMemoryLocationType(location.Type),
Position = new Vector3(location.X, location.Y, location.Z),
Seen = location.Seen,
Source = location.Source,
RemoteSeenOn = location.RemoteEncounters.Select(o => o.AccountId).ToList(),
};
}
// purge outdated locations private static MemoryLocation.EType ToMemoryLocationType(ClientLocation.EType type)
_cleanup.Purge(dbContext, _territory.TerritoryType); {
return type switch
// load good locations {
List<ClientLocation> locations = dbContext.Locations ClientLocation.EType.Trap => MemoryLocation.EType.Trap,
.Where(o => o.TerritoryType == (ushort)_territory.TerritoryType) ClientLocation.EType.Hoard => MemoryLocation.EType.Hoard,
.Include(o => o.ImportedBy) _ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
.Include(o => o.RemoteEncounters) };
.AsSplitQuery()
.ToList();
_territory.Initialize(locations.Select(ToMemoryLocation));
logger.LogInformation("Loaded {Count} locations for territory {Territory}", locations.Count,
_territory.TerritoryType);
} }
} }
public static PersistentLocation ToMemoryLocation(ClientLocation location)
{
return new PersistentLocation
{
LocalId = location.LocalId,
Type = ToMemoryLocationType(location.Type),
Position = new Vector3(location.X, location.Y, location.Z),
Seen = location.Seen,
Source = location.Source,
RemoteSeenOn = location.RemoteEncounters.Select(o => o.AccountId).ToList(),
};
}
private static MemoryLocation.EType ToMemoryLocationType(ClientLocation.EType type)
{
return type switch
{
ClientLocation.EType.Trap => MemoryLocation.EType.Trap,
ClientLocation.EType.Hoard => MemoryLocation.EType.Hoard,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
}
} }

View File

@ -5,33 +5,33 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Pal.Client.Database; using Pal.Client.Database;
namespace Pal.Client.Floors.Tasks; namespace Pal.Client.Floors.Tasks
internal sealed class MarkLocalSeen : DbTask<MarkLocalSeen>
{ {
private readonly MemoryTerritory _territory; internal sealed class MarkLocalSeen : DbTask<MarkLocalSeen>
private readonly IReadOnlyList<PersistentLocation> _locations;
public MarkLocalSeen(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory,
IReadOnlyList<PersistentLocation> locations)
: base(serviceScopeFactory)
{ {
_territory = territory; private readonly MemoryTerritory _territory;
_locations = locations; private readonly IReadOnlyList<PersistentLocation> _locations;
}
protected override void Run(PalClientContext dbContext, ILogger<MarkLocalSeen> logger) public MarkLocalSeen(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory,
{ IReadOnlyList<PersistentLocation> locations)
lock (_territory.LockObj) : base(serviceScopeFactory)
{ {
logger.LogInformation("Marking {Count} locations as seen locally in territory {Territory}", _territory = territory;
_locations.Count, _locations = locations;
_territory.TerritoryType); }
List<int> localIds = _locations.Select(l => l.LocalId).Where(x => x != null).Cast<int>().ToList();
dbContext.Locations protected override void Run(PalClientContext dbContext, ILogger<MarkLocalSeen> logger)
.Where(loc => localIds.Contains(loc.LocalId)) {
.ExecuteUpdate(loc => loc.SetProperty(l => l.Seen, true)); lock (_territory.LockObj)
dbContext.SaveChanges(); {
logger.LogInformation("Marking {Count} locations as seen locally in territory {Territory}", _locations.Count,
_territory.TerritoryType);
List<int> localIds = _locations.Select(l => l.LocalId).Where(x => x != null).Cast<int>().ToList();
dbContext.Locations
.Where(loc => localIds.Contains(loc.LocalId))
.ExecuteUpdate(loc => loc.SetProperty(l => l.Seen, true));
dbContext.SaveChanges();
}
} }
} }
} }

View File

@ -5,46 +5,47 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Pal.Client.Database; using Pal.Client.Database;
namespace Pal.Client.Floors.Tasks; namespace Pal.Client.Floors.Tasks
internal sealed class MarkRemoteSeen : DbTask<MarkRemoteSeen>
{ {
private readonly MemoryTerritory _territory; internal sealed class MarkRemoteSeen : DbTask<MarkRemoteSeen>
private readonly IReadOnlyList<PersistentLocation> _locations;
private readonly string _accountId;
public MarkRemoteSeen(IServiceScopeFactory serviceScopeFactory,
MemoryTerritory territory,
IReadOnlyList<PersistentLocation> locations,
string accountId)
: base(serviceScopeFactory)
{ {
_territory = territory; private readonly MemoryTerritory _territory;
_locations = locations; private readonly IReadOnlyList<PersistentLocation> _locations;
_accountId = accountId; private readonly string _accountId;
}
protected override void Run(PalClientContext dbContext, ILogger<MarkRemoteSeen> logger) public MarkRemoteSeen(IServiceScopeFactory serviceScopeFactory,
{ MemoryTerritory territory,
lock (_territory.LockObj) IReadOnlyList<PersistentLocation> locations,
string accountId)
: base(serviceScopeFactory)
{ {
logger.LogInformation("Marking {Count} locations as seen remotely on {Account} in territory {Territory}", _territory = territory;
_locations.Count, _accountId, _territory.TerritoryType); _locations = locations;
_accountId = accountId;
}
List<int> locationIds = _locations.Select(x => x.LocalId).Where(x => x != null).Cast<int>().ToList(); protected override void Run(PalClientContext dbContext, ILogger<MarkRemoteSeen> logger)
List<ClientLocation> locationsToUpdate = {
dbContext.Locations lock (_territory.LockObj)
.Include(x => x.RemoteEncounters)
.Where(x => locationIds.Contains(x.LocalId))
.ToList()
.Where(x => x.RemoteEncounters.All(encounter => encounter.AccountId != _accountId))
.ToList();
foreach (var clientLocation in locationsToUpdate)
{ {
clientLocation.RemoteEncounters.Add(new RemoteEncounter(clientLocation, _accountId)); logger.LogInformation("Marking {Count} locations as seen remotely on {Account} in territory {Territory}",
} _locations.Count, _accountId, _territory.TerritoryType);
dbContext.SaveChanges(); List<int> locationIds = _locations.Select(x => x.LocalId).Where(x => x != null).Cast<int>().ToList();
List<ClientLocation> locationsToUpdate =
dbContext.Locations
.Include(x => x.RemoteEncounters)
.Where(x => locationIds.Contains(x.LocalId))
.ToList()
.Where(x => x.RemoteEncounters.All(encounter => encounter.AccountId != _accountId))
.ToList();
foreach (var clientLocation in locationsToUpdate)
{
clientLocation.RemoteEncounters.Add(new RemoteEncounter(clientLocation, _accountId));
}
dbContext.SaveChanges();
}
} }
} }
} }

View File

@ -6,71 +6,72 @@ using Microsoft.Extensions.Logging;
using Pal.Client.Database; using Pal.Client.Database;
using Pal.Common; using Pal.Common;
namespace Pal.Client.Floors.Tasks; namespace Pal.Client.Floors.Tasks
internal sealed class SaveNewLocations : DbTask<SaveNewLocations>
{ {
private readonly MemoryTerritory _territory; internal sealed class SaveNewLocations : DbTask<SaveNewLocations>
private readonly List<PersistentLocation> _newLocations;
public SaveNewLocations(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory,
List<PersistentLocation> newLocations)
: base(serviceScopeFactory)
{ {
_territory = territory; private readonly MemoryTerritory _territory;
_newLocations = newLocations; private readonly List<PersistentLocation> _newLocations;
}
protected override void Run(PalClientContext dbContext, ILogger<SaveNewLocations> logger) public SaveNewLocations(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory,
{ List<PersistentLocation> newLocations)
Run(_territory, dbContext, logger, _newLocations); : base(serviceScopeFactory)
}
public static void Run<T>(
MemoryTerritory territory,
PalClientContext dbContext,
ILogger<T> logger,
List<PersistentLocation> locations)
{
lock (territory.LockObj)
{ {
logger.LogInformation("Saving {Count} new locations for territory {Territory}", locations.Count, _territory = territory;
territory.TerritoryType); _newLocations = newLocations;
}
Dictionary<PersistentLocation, ClientLocation> mapping = protected override void Run(PalClientContext dbContext, ILogger<SaveNewLocations> logger)
locations.ToDictionary(x => x, x => ToDatabaseLocation(x, territory.TerritoryType)); {
dbContext.Locations.AddRange(mapping.Values); Run(_territory, dbContext, logger, _newLocations);
dbContext.SaveChanges(); }
foreach ((PersistentLocation persistentLocation, ClientLocation clientLocation) in mapping) public static void Run<T>(
MemoryTerritory territory,
PalClientContext dbContext,
ILogger<T> logger,
List<PersistentLocation> locations)
{
lock (territory.LockObj)
{ {
persistentLocation.LocalId = clientLocation.LocalId; logger.LogInformation("Saving {Count} new locations for territory {Territory}", locations.Count,
territory.TerritoryType);
Dictionary<PersistentLocation, ClientLocation> mapping =
locations.ToDictionary(x => x, x => ToDatabaseLocation(x, territory.TerritoryType));
dbContext.Locations.AddRange(mapping.Values);
dbContext.SaveChanges();
foreach ((PersistentLocation persistentLocation, ClientLocation clientLocation) in mapping)
{
persistentLocation.LocalId = clientLocation.LocalId;
}
} }
} }
}
private static ClientLocation ToDatabaseLocation(PersistentLocation location, ETerritoryType territoryType) private static ClientLocation ToDatabaseLocation(PersistentLocation location, ETerritoryType territoryType)
{
return new ClientLocation
{ {
TerritoryType = (ushort)territoryType, return new ClientLocation
Type = ToDatabaseType(location.Type), {
X = location.Position.X, TerritoryType = (ushort)territoryType,
Y = location.Position.Y, Type = ToDatabaseType(location.Type),
Z = location.Position.Z, X = location.Position.X,
Seen = location.Seen, Y = location.Position.Y,
Source = location.Source, Z = location.Position.Z,
SinceVersion = typeof(Plugin).Assembly.GetName().Version!.ToString(2), Seen = location.Seen,
}; Source = location.Source,
} SinceVersion = typeof(Plugin).Assembly.GetName().Version!.ToString(2),
};
}
private static ClientLocation.EType ToDatabaseType(MemoryLocation.EType type) private static ClientLocation.EType ToDatabaseType(MemoryLocation.EType type)
{
return type switch
{ {
MemoryLocation.EType.Trap => ClientLocation.EType.Trap, return type switch
MemoryLocation.EType.Hoard => ClientLocation.EType.Hoard, {
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null) MemoryLocation.EType.Trap => ClientLocation.EType.Trap,
}; MemoryLocation.EType.Hoard => ClientLocation.EType.Hoard,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
}
} }
} }

View File

@ -1,34 +1,36 @@
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState;
using Dalamud.Plugin.Services; using Dalamud.Game.ClientState.Conditions;
using Pal.Common; using Pal.Common;
namespace Pal.Client.Floors; namespace Pal.Client.Floors
public sealed class TerritoryState
{ {
private readonly IClientState _clientState; public sealed class TerritoryState
private readonly ICondition _condition;
public TerritoryState(IClientState clientState, ICondition condition)
{ {
_clientState = clientState; private readonly ClientState _clientState;
_condition = condition; private readonly Condition _condition;
public TerritoryState(ClientState clientState, Condition condition)
{
_clientState = clientState;
_condition = condition;
}
public ushort LastTerritory { get; set; }
public PomanderState PomanderOfSight { get; set; } = PomanderState.Inactive;
public PomanderState PomanderOfIntuition { get; set; } = PomanderState.Inactive;
public bool IsInDeepDungeon() =>
_clientState.IsLoggedIn
&& _condition[ConditionFlag.InDeepDungeon]
&& typeof(ETerritoryType).IsEnumDefined(_clientState.TerritoryType);
} }
public ushort LastTerritory { get; set; } public enum PomanderState
public PomanderState PomanderOfSight { get; set; } = PomanderState.Inactive; {
public PomanderState PomanderOfIntuition { get; set; } = PomanderState.Inactive; Inactive,
Active,
public bool IsInDeepDungeon() => FoundOnCurrentFloor,
_clientState.IsLoggedIn PomanderOfSafetyUsed,
&& _condition[ConditionFlag.InDeepDungeon] }
&& typeof(ETerritoryType).IsEnumDefined(_clientState.TerritoryType);
}
public enum PomanderState
{
Inactive,
Active,
FoundOnCurrentFloor,
PomanderOfSafetyUsed,
} }

View File

@ -4,9 +4,10 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Pal.Client; namespace Pal.Client
internal interface ILanguageChanged
{ {
void LanguageChanged(); internal interface ILanguageChanged
{
void LanguageChanged();
}
} }

View File

@ -1,79 +1,95 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace Pal.Client.Net; namespace Pal.Client.Net
internal sealed class JwtClaims
{ {
[JsonPropertyName("nameid")] internal sealed class JwtClaims
public Guid NameId { get; set; }
[JsonPropertyName("role")]
[JsonConverter(typeof(JwtRoleConverter))]
public List<string> Roles { get; set; } = new();
[JsonPropertyName("nbf")]
[JsonConverter(typeof(JwtDateConverter))]
public DateTimeOffset NotBefore { get; set; }
[JsonPropertyName("exp")]
[JsonConverter(typeof(JwtDateConverter))]
public DateTimeOffset ExpiresAt { get; set; }
public static JwtClaims FromAuthToken(string authToken)
{ {
if (string.IsNullOrEmpty(authToken)) [JsonPropertyName("nameid")]
throw new ArgumentException("Server sent no auth token", nameof(authToken)); public Guid NameId { get; set; }
string[] parts = authToken.Split('.'); [JsonPropertyName("role")]
if (parts.Length != 3) [JsonConverter(typeof(JwtRoleConverter))]
throw new ArgumentException("Unsupported token type", nameof(authToken)); public List<string> Roles { get; set; } = new();
// fix padding manually [JsonPropertyName("nbf")]
string payload = parts[1].Replace(",", "=").Replace("-", "+").Replace("/", "_"); [JsonConverter(typeof(JwtDateConverter))]
if (payload.Length % 4 == 2) public DateTimeOffset NotBefore { get; set; }
payload += "==";
else if (payload.Length % 4 == 3)
payload += "=";
string content = Encoding.UTF8.GetString(Convert.FromBase64String(payload)); [JsonPropertyName("exp")]
return JsonSerializer.Deserialize<JwtClaims>(content) ?? [JsonConverter(typeof(JwtDateConverter))]
throw new InvalidOperationException("token deserialization returned null"); public DateTimeOffset ExpiresAt { get; set; }
}
}
internal sealed class JwtRoleConverter : JsonConverter<List<string>> public static JwtClaims FromAuthToken(string authToken)
{
public override List<string> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
return new List<string> { reader.GetString() ?? throw new JsonException("no value present") };
else if (reader.TokenType == JsonTokenType.StartArray)
{ {
List<string> result = new(); if (string.IsNullOrEmpty(authToken))
while (reader.Read()) throw new ArgumentException("Server sent no auth token", nameof(authToken));
string[] parts = authToken.Split('.');
if (parts.Length != 3)
throw new ArgumentException("Unsupported token type", nameof(authToken));
// fix padding manually
string payload = parts[1].Replace(",", "=").Replace("-", "+").Replace("/", "_");
if (payload.Length % 4 == 2)
payload += "==";
else if (payload.Length % 4 == 3)
payload += "=";
string content = Encoding.UTF8.GetString(Convert.FromBase64String(payload));
return JsonSerializer.Deserialize<JwtClaims>(content) ?? throw new InvalidOperationException("token deserialization returned null");
}
}
internal sealed class JwtRoleConverter : JsonConverter<List<string>>
{
public override List<string> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
return new List<string> { reader.GetString() ?? throw new JsonException("no value present") };
else if (reader.TokenType == JsonTokenType.StartArray)
{ {
if (reader.TokenType == JsonTokenType.EndArray) List<string> result = new();
while (reader.Read())
{ {
result.Sort(); if (reader.TokenType == JsonTokenType.EndArray)
return result; {
result.Sort();
return result;
}
if (reader.TokenType != JsonTokenType.String)
throw new JsonException("string expected");
result.Add(reader.GetString() ?? throw new JsonException("no value present"));
} }
if (reader.TokenType != JsonTokenType.String) throw new JsonException("read to end of document");
throw new JsonException("string expected");
result.Add(reader.GetString() ?? throw new JsonException("no value present"));
} }
else
throw new JsonException("read to end of document"); throw new JsonException("bad token type");
} }
else
throw new JsonException("bad token type"); public override void Write(Utf8JsonWriter writer, List<string> value, JsonSerializerOptions options) => throw new NotImplementedException();
} }
public override void Write(Utf8JsonWriter writer, List<string> value, JsonSerializerOptions options) => public sealed class JwtDateConverter : JsonConverter<DateTimeOffset>
throw new NotImplementedException(); {
static readonly DateTimeOffset Zero = new(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.Number)
throw new JsonException("bad token type");
return Zero.AddSeconds(reader.GetInt64());
}
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) => throw new NotImplementedException();
}
} }

View File

@ -1,21 +0,0 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Pal.Client.Net;
public sealed class JwtDateConverter : JsonConverter<DateTimeOffset>
{
static readonly DateTimeOffset Zero = new(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);
public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.Number)
throw new JsonException("bad token type");
return Zero.AddSeconds(reader.GetInt64());
}
public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) =>
throw new NotImplementedException();
}

View File

@ -11,238 +11,222 @@ using Microsoft.Extensions.Logging;
using Pal.Client.Configuration; using Pal.Client.Configuration;
using Pal.Client.Extensions; using Pal.Client.Extensions;
using Pal.Client.Properties; using Pal.Client.Properties;
using Version = System.Version;
namespace Pal.Client.Net; namespace Pal.Client.Net
internal partial class RemoteApi
{ {
private static readonly Version PluginVersion = typeof(Plugin).Assembly.GetName().Version!; internal partial class RemoteApi
private readonly SemaphoreSlim _connectLock = new(1, 1);
private async Task<(bool Success, string Error)> TryConnect(CancellationToken cancellationToken,
ILoggerFactory? loggerFactory = null, bool retry = true)
{ {
using IDisposable? logScope = _logger.BeginScope("TryConnect"); private readonly SemaphoreSlim _connectLock = new(1, 1);
var result = await TryConnectImpl(cancellationToken, loggerFactory); private async Task<(bool Success, string Error)> TryConnect(CancellationToken cancellationToken,
if (retry && result.ShouldRetry) ILoggerFactory? loggerFactory = null, bool retry = true)
result = await TryConnectImpl(cancellationToken, loggerFactory);
return (result.Success, result.Error);
}
private async Task<(bool Success, string Error, bool ShouldRetry)> TryConnectImpl(
CancellationToken cancellationToken,
ILoggerFactory? loggerFactory)
{
if (_configuration.Mode != EMode.Online)
{ {
_logger.LogDebug("Not Online, not attempting to establish a connection"); using IDisposable? logScope = _logger.BeginScope("TryConnect");
return (false, Localization.ConnectionError_NotOnline, false);
var result = await TryConnectImpl(cancellationToken, loggerFactory);
if (retry && result.ShouldRetry)
result = await TryConnectImpl(cancellationToken, loggerFactory);
return (result.Success, result.Error);
} }
if (_channel == null || private async Task<(bool Success, string Error, bool ShouldRetry)> TryConnectImpl(
!(_channel.State == ConnectivityState.Ready || _channel.State == ConnectivityState.Idle)) CancellationToken cancellationToken,
ILoggerFactory? loggerFactory)
{ {
Dispose(); if (_configuration.Mode != EMode.Online)
_logger.LogInformation("Creating new gRPC channel");
_channel = GrpcChannel.ForAddress(RemoteUrl, new GrpcChannelOptions
{ {
HttpHandler = new SocketsHttpHandler _logger.LogDebug("Not Online, not attempting to establish a connection");
return (false, Localization.ConnectionError_NotOnline, false);
}
if (_channel == null ||
!(_channel.State == ConnectivityState.Ready || _channel.State == ConnectivityState.Idle))
{
Dispose();
_logger.LogInformation("Creating new gRPC channel");
_channel = GrpcChannel.ForAddress(RemoteUrl, new GrpcChannelOptions
{ {
ConnectTimeout = TimeSpan.FromSeconds(5), HttpHandler = new SocketsHttpHandler
SslOptions = GetSslClientAuthenticationOptions(),
},
LoggerFactory = loggerFactory,
});
_logger.LogInformation("Connecting to upstream service at {Url}", RemoteUrl);
await _channel.ConnectAsync(cancellationToken);
}
cancellationToken.ThrowIfCancellationRequested();
_logger.LogTrace("Acquiring connect lock");
await _connectLock.WaitAsync(cancellationToken);
_logger.LogTrace("Obtained connect lock");
try
{
var accountClient = new AccountService.AccountServiceClient(_channel);
IAccountConfiguration? configuredAccount = _configuration.FindAccount(RemoteUrl);
if (configuredAccount == null)
{
_logger.LogInformation("No account information saved for {Url}, creating new account", RemoteUrl);
var createAccountReply = await accountClient.CreateAccountAsync(new CreateAccountRequest
{ {
Version = new() ConnectTimeout = TimeSpan.FromSeconds(5),
{ SslOptions = GetSslClientAuthenticationOptions(),
Major = PluginVersion.Major,
Minor = PluginVersion.Minor,
},
}, },
headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), LoggerFactory = loggerFactory,
cancellationToken: cancellationToken); });
if (createAccountReply.Success)
{
if (!Guid.TryParse(createAccountReply.AccountId, out Guid accountId))
throw new InvalidOperationException("invalid account id returned");
configuredAccount = _configuration.CreateAccount(RemoteUrl, accountId); _logger.LogInformation("Connecting to upstream service at {Url}", RemoteUrl);
_logger.LogInformation("Account created with id {AccountId}", accountId.ToPartialId()); await _channel.ConnectAsync(cancellationToken);
_configurationManager.Save(_configuration);
}
else
{
_logger.LogError("Account creation failed with error {Error}", createAccountReply.Error);
if (createAccountReply.Error == CreateAccountError.UpgradeRequired && !_warnedAboutUpgrade)
{
_chat.Error(Localization.ConnectionError_OldVersion);
_warnedAboutUpgrade = true;
}
return (false,
string.Format(Localization.ConnectionError_CreateAccountFailed, createAccountReply.Error),
false);
}
} }
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract _logger.LogTrace("Acquiring connect lock");
if (configuredAccount == null) await _connectLock.WaitAsync(cancellationToken);
{ _logger.LogTrace("Obtained connect lock");
_logger.LogWarning("No account to login with");
return (false, Localization.ConnectionError_CreateAccountReturnedNoId, false);
}
if (!_loginInfo.IsValid) try
{ {
_logger.LogInformation("Logging in with account id {AccountId}", var accountClient = new AccountService.AccountServiceClient(_channel);
configuredAccount.AccountId.ToPartialId()); IAccountConfiguration? configuredAccount = _configuration.FindAccount(RemoteUrl);
LoginReply loginReply = await accountClient.LoginAsync( if (configuredAccount == null)
new LoginRequest
{
AccountId = configuredAccount.AccountId.ToString(),
Version = new()
{
Major = PluginVersion.Major,
Minor = PluginVersion.Minor,
},
},
headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10),
cancellationToken: cancellationToken);
if (loginReply.Success)
{ {
_logger.LogInformation("Login successful with account id: {AccountId}", _logger.LogInformation("No account information saved for {Url}, creating new account", RemoteUrl);
var createAccountReply = await accountClient.CreateAccountAsync(new CreateAccountRequest(),
headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10),
cancellationToken: cancellationToken);
if (createAccountReply.Success)
{
if (!Guid.TryParse(createAccountReply.AccountId, out Guid accountId))
throw new InvalidOperationException("invalid account id returned");
configuredAccount = _configuration.CreateAccount(RemoteUrl, accountId);
_logger.LogInformation("Account created with id {AccountId}", accountId.ToPartialId());
_configurationManager.Save(_configuration);
}
else
{
_logger.LogError("Account creation failed with error {Error}", createAccountReply.Error);
if (createAccountReply.Error == CreateAccountError.UpgradeRequired && !_warnedAboutUpgrade)
{
_chat.Error(Localization.ConnectionError_OldVersion);
_warnedAboutUpgrade = true;
}
return (false,
string.Format(Localization.ConnectionError_CreateAccountFailed, createAccountReply.Error),
false);
}
}
cancellationToken.ThrowIfCancellationRequested();
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (configuredAccount == null)
{
_logger.LogWarning("No account to login with");
return (false, Localization.ConnectionError_CreateAccountReturnedNoId, false);
}
if (!_loginInfo.IsValid)
{
_logger.LogInformation("Logging in with account id {AccountId}",
configuredAccount.AccountId.ToPartialId()); configuredAccount.AccountId.ToPartialId());
_loginInfo = new LoginInfo(loginReply.AuthToken); LoginReply loginReply = await accountClient.LoginAsync(
new LoginRequest { AccountId = configuredAccount.AccountId.ToString() },
headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10),
cancellationToken: cancellationToken);
bool save = configuredAccount.EncryptIfNeeded(); if (loginReply.Success)
List<string> newRoles = _loginInfo.Claims?.Roles.ToList() ?? new();
if (!newRoles.SequenceEqual(configuredAccount.CachedRoles))
{ {
configuredAccount.CachedRoles = newRoles; _logger.LogInformation("Login successful with account id: {AccountId}",
save = true; configuredAccount.AccountId.ToPartialId());
} _loginInfo = new LoginInfo(loginReply.AuthToken);
if (save) bool save = configuredAccount.EncryptIfNeeded();
_configurationManager.Save(_configuration);
List<string> newRoles = _loginInfo.Claims?.Roles.ToList() ?? new();
if (!newRoles.SequenceEqual(configuredAccount.CachedRoles))
{
configuredAccount.CachedRoles = newRoles;
save = true;
}
if (save)
_configurationManager.Save(_configuration);
}
else
{
_logger.LogError("Login failed with error {Error}", loginReply.Error);
_loginInfo = new LoginInfo(null);
if (loginReply.Error == LoginError.InvalidAccountId)
{
_configuration.RemoveAccount(RemoteUrl);
_configurationManager.Save(_configuration);
_logger.LogInformation("Attempting connection retry without account id");
return (false, Localization.ConnectionError_InvalidAccountId, true);
}
if (loginReply.Error == LoginError.UpgradeRequired && !_warnedAboutUpgrade)
{
_chat.Error(Localization.ConnectionError_OldVersion);
_warnedAboutUpgrade = true;
}
return (false, string.Format(Localization.ConnectionError_LoginFailed, loginReply.Error),
false);
}
}
if (!_loginInfo.IsValid)
{
_logger.LogError("Login state is loggedIn={LoggedIn}, expired={Expired}", _loginInfo.IsLoggedIn,
_loginInfo.IsExpired);
return (false, Localization.ConnectionError_LoginReturnedNoToken, false);
}
cancellationToken.ThrowIfCancellationRequested();
return (true, string.Empty, false);
}
finally
{
_logger.LogTrace("Releasing connectLock");
_connectLock.Release();
}
}
private async Task<bool> Connect(CancellationToken cancellationToken)
{
var result = await TryConnect(cancellationToken);
return result.Success;
}
public async Task<string> VerifyConnection(CancellationToken cancellationToken = default)
{
using IDisposable? logScope = _logger.BeginScope("VerifyConnection");
_warnedAboutUpgrade = false;
var connectionResult = await TryConnect(cancellationToken, loggerFactory: _loggerFactory);
if (!connectionResult.Success)
return string.Format(Localization.ConnectionError_CouldNotConnectToServer, connectionResult.Error);
_logger.LogInformation("Connection established, trying to verify auth token");
var accountClient = new AccountService.AccountServiceClient(_channel);
await accountClient.VerifyAsync(new VerifyRequest(), headers: AuthorizedHeaders(),
deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken);
_logger.LogInformation("Verification returned no errors.");
return Localization.ConnectionSuccessful;
}
internal sealed class LoginInfo
{
public LoginInfo(string? authToken)
{
if (!string.IsNullOrEmpty(authToken))
{
IsLoggedIn = true;
AuthToken = authToken;
Claims = JwtClaims.FromAuthToken(authToken);
} }
else else
{ IsLoggedIn = false;
_logger.LogError("Login failed with error {Error}", loginReply.Error);
_loginInfo = new LoginInfo(null);
if (loginReply.Error == LoginError.InvalidAccountId)
{
_configuration.RemoveAccount(RemoteUrl);
_configurationManager.Save(_configuration);
_logger.LogInformation("Attempting connection retry without account id");
return (false, Localization.ConnectionError_InvalidAccountId, true);
}
if (loginReply.Error == LoginError.UpgradeRequired && !_warnedAboutUpgrade)
{
_chat.Error(Localization.ConnectionError_OldVersion);
_warnedAboutUpgrade = true;
}
return (false, string.Format(Localization.ConnectionError_LoginFailed, loginReply.Error),
false);
}
} }
if (!_loginInfo.IsValid) public bool IsLoggedIn { get; }
{ public string? AuthToken { get; }
_logger.LogError("Login state is loggedIn={LoggedIn}, expired={Expired}", _loginInfo.IsLoggedIn, public JwtClaims? Claims { get; }
_loginInfo.IsExpired);
return (false, Localization.ConnectionError_LoginReturnedNoToken, false);
}
cancellationToken.ThrowIfCancellationRequested(); private DateTimeOffset ExpiresAt =>
return (true, string.Empty, false); Claims?.ExpiresAt.Subtract(TimeSpan.FromMinutes(5)) ?? DateTimeOffset.MinValue;
public bool IsExpired => ExpiresAt < DateTimeOffset.UtcNow;
public bool IsValid => IsLoggedIn && !IsExpired;
} }
finally
{
_logger.LogTrace("Releasing connectLock");
_connectLock.Release();
}
}
private async Task<bool> Connect(CancellationToken cancellationToken)
{
var result = await TryConnect(cancellationToken);
return result.Success;
}
public async Task<string> VerifyConnection(CancellationToken cancellationToken = default)
{
using IDisposable? logScope = _logger.BeginScope("VerifyConnection");
_warnedAboutUpgrade = false;
var connectionResult = await TryConnect(cancellationToken, loggerFactory: _loggerFactory);
if (!connectionResult.Success)
return string.Format(Localization.ConnectionError_CouldNotConnectToServer, connectionResult.Error);
_logger.LogInformation("Connection established, trying to verify auth token");
var accountClient = new AccountService.AccountServiceClient(_channel);
await accountClient.VerifyAsync(new VerifyRequest(), headers: AuthorizedHeaders(),
deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken);
_logger.LogInformation("Verification returned no errors.");
return Localization.ConnectionSuccessful;
}
internal sealed class LoginInfo
{
public LoginInfo(string? authToken)
{
if (!string.IsNullOrEmpty(authToken))
{
IsLoggedIn = true;
AuthToken = authToken;
Claims = JwtClaims.FromAuthToken(authToken);
}
else
IsLoggedIn = false;
}
public bool IsLoggedIn { get; }
public string? AuthToken { get; }
public JwtClaims? Claims { get; }
private DateTimeOffset ExpiresAt =>
Claims?.ExpiresAt.Subtract(TimeSpan.FromMinutes(5)) ?? DateTimeOffset.MinValue;
public bool IsExpired => ExpiresAt < DateTimeOffset.UtcNow;
public bool IsValid => IsLoggedIn && !IsExpired;
} }
} }

View File

@ -3,21 +3,21 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Export; using Export;
namespace Pal.Client.Net; namespace Pal.Client.Net
internal partial class RemoteApi
{ {
public async Task<(bool, ExportRoot)> DoExport(CancellationToken cancellationToken = default) internal partial class RemoteApi
{ {
if (!await Connect(cancellationToken)) public async Task<(bool, ExportRoot)> DoExport(CancellationToken cancellationToken = default)
return new(false, new()); {
if (!await Connect(cancellationToken))
return new(false, new());
var exportClient = new ExportService.ExportServiceClient(_channel); var exportClient = new ExportService.ExportServiceClient(_channel);
var exportReply = await exportClient.ExportAsync(new ExportRequest var exportReply = await exportClient.ExportAsync(new ExportRequest
{ {
ServerUrl = RemoteUrl, ServerUrl = RemoteUrl,
}, headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(120), }, headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(120), cancellationToken: cancellationToken);
cancellationToken: cancellationToken); return (exportReply.Success, exportReply.Data);
return (exportReply.Success, exportReply.Data); }
} }
} }

View File

@ -8,88 +8,80 @@ using Pal.Client.Database;
using Pal.Client.Floors; using Pal.Client.Floors;
using Palace; using Palace;
namespace Pal.Client.Net; namespace Pal.Client.Net
internal partial class RemoteApi
{ {
public async Task<(bool, List<PersistentLocation>)> DownloadRemoteMarkers(ushort territoryId, internal partial class RemoteApi
CancellationToken cancellationToken = default)
{ {
if (!await Connect(cancellationToken)) public async Task<(bool, List<PersistentLocation>)> DownloadRemoteMarkers(ushort territoryId, CancellationToken cancellationToken = default)
return (false, new());
var palaceClient = new PalaceService.PalaceServiceClient(_channel);
var downloadReply = await palaceClient.DownloadFloorsAsync(
new DownloadFloorsRequest { TerritoryType = territoryId }, headers: AuthorizedHeaders(),
cancellationToken: cancellationToken);
return (downloadReply.Success, downloadReply.Objects.Select(CreateLocationFromNetworkObject).ToList());
}
public async Task<(bool, List<PersistentLocation>)> UploadLocations(ushort territoryType,
IReadOnlyList<PersistentLocation> locations, CancellationToken cancellationToken = default)
{
if (locations.Count == 0)
return (true, new());
if (!await Connect(cancellationToken))
return (false, new());
var palaceClient = new PalaceService.PalaceServiceClient(_channel);
var uploadRequest = new UploadFloorsRequest
{ {
TerritoryType = territoryType, if (!await Connect(cancellationToken))
}; return (false, new());
uploadRequest.Objects.AddRange(locations.Select(m => new PalaceObject
var palaceClient = new PalaceService.PalaceServiceClient(_channel);
var downloadReply = await palaceClient.DownloadFloorsAsync(new DownloadFloorsRequest { TerritoryType = territoryId }, headers: AuthorizedHeaders(), cancellationToken: cancellationToken);
return (downloadReply.Success, downloadReply.Objects.Select(CreateLocationFromNetworkObject).ToList());
}
public async Task<(bool, List<PersistentLocation>)> UploadLocations(ushort territoryType, IReadOnlyList<PersistentLocation> locations, CancellationToken cancellationToken = default)
{ {
Type = m.Type.ToObjectType(), if (locations.Count == 0)
X = m.Position.X, return (true, new());
Y = m.Position.Y,
Z = m.Position.Z
}));
var uploadReply = await palaceClient.UploadFloorsAsync(uploadRequest, headers: AuthorizedHeaders(),
cancellationToken: cancellationToken);
return (uploadReply.Success, uploadReply.Objects.Select(CreateLocationFromNetworkObject).ToList());
}
public async Task<bool> MarkAsSeen(ushort territoryType, IReadOnlyList<PersistentLocation> locations, if (!await Connect(cancellationToken))
CancellationToken cancellationToken = default) return (false, new());
{
if (locations.Count == 0)
return true;
if (!await Connect(cancellationToken)) var palaceClient = new PalaceService.PalaceServiceClient(_channel);
return false; var uploadRequest = new UploadFloorsRequest
{
TerritoryType = territoryType,
};
uploadRequest.Objects.AddRange(locations.Select(m => new PalaceObject
{
Type = m.Type.ToObjectType(),
X = m.Position.X,
Y = m.Position.Y,
Z = m.Position.Z
}));
var uploadReply = await palaceClient.UploadFloorsAsync(uploadRequest, headers: AuthorizedHeaders(), cancellationToken: cancellationToken);
return (uploadReply.Success, uploadReply.Objects.Select(CreateLocationFromNetworkObject).ToList());
}
var palaceClient = new PalaceService.PalaceServiceClient(_channel); public async Task<bool> MarkAsSeen(ushort territoryType, IReadOnlyList<PersistentLocation> locations, CancellationToken cancellationToken = default)
var seenRequest = new MarkObjectsSeenRequest { TerritoryType = territoryType };
foreach (var marker in locations)
seenRequest.NetworkIds.Add(marker.NetworkId.ToString());
var seenReply = await palaceClient.MarkObjectsSeenAsync(seenRequest, headers: AuthorizedHeaders(),
deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken);
return seenReply.Success;
}
private PersistentLocation CreateLocationFromNetworkObject(PalaceObject obj)
{
return new PersistentLocation
{ {
Type = obj.Type.ToMemoryType(), if (locations.Count == 0)
Position = new Vector3(obj.X, obj.Y, obj.Z), return true;
NetworkId = Guid.Parse(obj.NetworkId),
Source = ClientLocation.ESource.Download,
};
}
public async Task<(bool, List<FloorStatistics>)> FetchStatistics(CancellationToken cancellationToken = default) if (!await Connect(cancellationToken))
{ return false;
if (!await Connect(cancellationToken))
return new(false, new List<FloorStatistics>());
var palaceClient = new PalaceService.PalaceServiceClient(_channel); var palaceClient = new PalaceService.PalaceServiceClient(_channel);
var statisticsReply = await palaceClient.FetchStatisticsAsync(new StatisticsRequest(), var seenRequest = new MarkObjectsSeenRequest { TerritoryType = territoryType };
headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(30), foreach (var marker in locations)
cancellationToken: cancellationToken); seenRequest.NetworkIds.Add(marker.NetworkId.ToString());
return (statisticsReply.Success, statisticsReply.FloorStatistics.ToList());
var seenReply = await palaceClient.MarkObjectsSeenAsync(seenRequest, headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken);
return seenReply.Success;
}
private PersistentLocation CreateLocationFromNetworkObject(PalaceObject obj)
{
return new PersistentLocation
{
Type = obj.Type.ToMemoryType(),
Position = new Vector3(obj.X, obj.Y, obj.Z),
NetworkId = Guid.Parse(obj.NetworkId),
Source = ClientLocation.ESource.Download,
};
}
public async Task<(bool, List<FloorStatistics>)> FetchStatistics(CancellationToken cancellationToken = default)
{
if (!await Connect(cancellationToken))
return new(false, new List<FloorStatistics>());
var palaceClient = new PalaceService.PalaceServiceClient(_channel);
var statisticsReply = await palaceClient.FetchStatisticsAsync(new StatisticsRequest(), headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(30), cancellationToken: cancellationToken);
return (statisticsReply.Success, statisticsReply.FloorStatistics.ToList());
}
} }
} }

View File

@ -5,53 +5,54 @@ using Dalamud.Logging;
using Grpc.Core; using Grpc.Core;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Pal.Client.Net; namespace Pal.Client.Net
internal partial class RemoteApi
{ {
private Metadata UnauthorizedHeaders() => new() internal partial class RemoteApi
{ {
{ "User-Agent", _userAgent }, private Metadata UnauthorizedHeaders() => new()
};
private Metadata AuthorizedHeaders() => new()
{
{ "Authorization", $"Bearer {_loginInfo.AuthToken}" },
{ "User-Agent", _userAgent },
};
private SslClientAuthenticationOptions? GetSslClientAuthenticationOptions()
{
#if !DEBUG
var secrets = typeof(RemoteApi).Assembly.GetType("Pal.Client.Secrets");
if (secrets == null)
return null;
var pass = secrets.GetProperty("CertPassword")?.GetValue(null) as string;
if (pass == null)
return null;
var manifestResourceStream = typeof(RemoteApi).Assembly.GetManifestResourceStream("Pal.Client.Certificate.pfx");
if (manifestResourceStream == null)
return null;
var bytes = new byte[manifestResourceStream.Length];
int read = manifestResourceStream.Read(bytes, 0, bytes.Length);
if (read != bytes.Length)
throw new InvalidOperationException();
var certificate = new X509Certificate2(bytes, pass, X509KeyStorageFlags.DefaultKeySet);
_logger.LogDebug("Using client certificate {CertificateHash}", certificate.GetCertHashString());
return new SslClientAuthenticationOptions
{ {
ClientCertificates = new X509CertificateCollection() { "User-Agent", _userAgent },
{
certificate,
},
}; };
private Metadata AuthorizedHeaders() => new()
{
{ "Authorization", $"Bearer {_loginInfo.AuthToken}" },
{ "User-Agent", _userAgent },
};
private SslClientAuthenticationOptions? GetSslClientAuthenticationOptions()
{
#if !DEBUG
var secrets = typeof(RemoteApi).Assembly.GetType("Pal.Client.Secrets");
if (secrets == null)
return null;
var pass = secrets.GetProperty("CertPassword")?.GetValue(null) as string;
if (pass == null)
return null;
var manifestResourceStream = typeof(RemoteApi).Assembly.GetManifestResourceStream("Pal.Client.Certificate.pfx");
if (manifestResourceStream == null)
return null;
var bytes = new byte[manifestResourceStream.Length];
int read = manifestResourceStream.Read(bytes, 0, bytes.Length);
if (read != bytes.Length)
throw new InvalidOperationException();
var certificate = new X509Certificate2(bytes, pass, X509KeyStorageFlags.DefaultKeySet);
_logger.LogDebug("Using client certificate {CertificateHash}", certificate.GetCertHashString());
return new SslClientAuthenticationOptions
{
ClientCertificates = new X509CertificateCollection()
{
certificate,
},
};
#else #else
_logger.LogDebug("Not using client certificate"); _logger.LogDebug("Not using client certificate");
return null; return null;
#endif #endif
}
} }
} }

View File

@ -6,46 +6,47 @@ using Microsoft.Extensions.Logging;
using Pal.Client.Configuration; using Pal.Client.Configuration;
using Pal.Client.DependencyInjection; using Pal.Client.DependencyInjection;
namespace Pal.Client.Net; namespace Pal.Client.Net
internal sealed partial class RemoteApi : IDisposable
{ {
internal sealed partial class RemoteApi : IDisposable
{
#if DEBUG #if DEBUG
public const string RemoteUrl = "http://localhost:5415"; public const string RemoteUrl = "http://localhost:5415";
#else #else
public const string RemoteUrl = "https://connect.palacepal.com"; public const string RemoteUrl = "https://pal.liza.sh";
#endif #endif
private readonly string _userAgent = private readonly string _userAgent =
$"{typeof(RemoteApi).Assembly.GetName().Name?.Replace(" ", "")}/{typeof(RemoteApi).Assembly.GetName().Version?.ToString(2)}"; $"{typeof(RemoteApi).Assembly.GetName().Name?.Replace(" ", "")}/{typeof(RemoteApi).Assembly.GetName().Version?.ToString(2)}";
private readonly ILoggerFactory _loggerFactory; private readonly ILoggerFactory _loggerFactory;
private readonly ILogger<RemoteApi> _logger; private readonly ILogger<RemoteApi> _logger;
private readonly Chat _chat; private readonly Chat _chat;
private readonly ConfigurationManager _configurationManager; private readonly ConfigurationManager _configurationManager;
private readonly IPalacePalConfiguration _configuration; private readonly IPalacePalConfiguration _configuration;
private GrpcChannel? _channel; private GrpcChannel? _channel;
private LoginInfo _loginInfo = new(null); private LoginInfo _loginInfo = new(null);
private bool _warnedAboutUpgrade; private bool _warnedAboutUpgrade;
public RemoteApi( public RemoteApi(
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
ILogger<RemoteApi> logger, ILogger<RemoteApi> logger,
Chat chat, Chat chat,
ConfigurationManager configurationManager, ConfigurationManager configurationManager,
IPalacePalConfiguration configuration) IPalacePalConfiguration configuration)
{ {
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
_logger = logger; _logger = logger;
_chat = chat; _chat = chat;
_configurationManager = configurationManager; _configurationManager = configurationManager;
_configuration = configuration; _configuration = configuration;
} }
public void Dispose() public void Dispose()
{ {
_logger.LogDebug("Disposing gRPC channel"); _logger.LogDebug("Disposing gRPC channel");
_channel?.Dispose(); _channel?.Dispose();
_channel = null; _channel = null;
}
} }
} }

View File

@ -1,14 +1,28 @@
<Project Sdk="Dalamud.NET.Sdk/9.0.2"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<Version>6.0</Version> <TargetFramework>net7.0-windows</TargetFramework>
<AssemblyName>Palace Pal</AssemblyName> <LangVersion>11.0</LangVersion>
<PlatformTarget>x64</PlatformTarget> <Nullable>enable</Nullable>
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<Import Project="..\vendor\LLib\LLib.targets"/> <PropertyGroup>
<Import Project="..\vendor\LLib\RenameZip.targets"/> <ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<PlatformTarget>x64</PlatformTarget>
<AssemblyName>Palace Pal</AssemblyName>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DebugType>portable</DebugType>
<PathMap Condition="$(SolutionDir) != ''">$(SolutionDir)=X:\</PathMap>
<GitVersion>false</GitVersion>
<GenerateAssemblyVersionAttribute>false</GenerateAssemblyVersionAttribute>
<GenerateAssemblyFileVersionAttribute>false</GenerateAssemblyFileVersionAttribute>
<GenerateAssemblyInformationalVersionAttribute>false</GenerateAssemblyInformationalVersionAttribute>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'"> <PropertyGroup Condition="'$(Configuration)' == 'Release'">
<OutputPath>dist</OutputPath> <OutputPath>dist</OutputPath>
@ -23,26 +37,29 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Dalamud.Extensions.MicrosoftLogging" Version="4.0.1"/> <PackageReference Include="DalamudPackager" Version="2.1.11"/>
<PackageReference Include="Google.Protobuf" Version="3.27.2" /> <PackageReference Include="Dalamud.Extensions.MicrosoftLogging" Version="1.0.0"/>
<PackageReference Include="Grpc.Net.Client" Version="2.63.0"/> <PackageReference Include="Google.Protobuf" Version="3.22.1"/>
<PackageReference Include="Grpc.Tools" Version="2.64.0"> <PackageReference Include="Grpc.Net.Client" Version="2.52.0"/>
<PackageReference Include="GitInfo" Version="2.3.0">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Grpc.Tools" Version="2.53.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.4"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.6" Condition="'$(Configuration)' == 'EF'"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.4">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0"/> <PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0"/>
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0"/> <PackageReference Include="System.Security.Cryptography.ProtectedData" Version="7.0.1"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Pal.Common\Pal.Common.csproj"/> <ProjectReference Include="..\Pal.Common\Pal.Common.csproj"/>
<ProjectReference Include="..\vendor\ECommons\ECommons\ECommons.csproj"/> <ProjectReference Include="..\vendor\ECommons\ECommons\ECommons.csproj"/>
<ProjectReference Include="..\vendor\LLib\LLib.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -51,6 +68,42 @@
<Protobuf Include="..\Pal.Common\Protos\export.proto" Link="Protos\export.proto" GrpcServices="Client" Access="Internal"/> <Protobuf Include="..\Pal.Common\Protos\export.proto" Link="Protos\export.proto" GrpcServices="Client" Access="Internal"/>
</ItemGroup> </ItemGroup>
<ItemGroup>
<!--You may need to adjust these paths yourself. These point to a Dalamud assembly in AppData.-->
<Reference Include="Dalamud">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
<Private Condition="'$(Configuration)' != 'EF'">false</Private>
</Reference>
<Reference Include="ImGui.NET">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\ImGui.NET.dll</HintPath>
<Private Condition="'$(Configuration)' != 'EF'">false</Private>
</Reference>
<Reference Include="ImGuiScene">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll</HintPath>
<Private Condition="'$(Configuration)' != 'EF'">false</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll</HintPath>
<Private Condition="'$(Configuration)' != 'EF'">false</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll</HintPath>
<Private Condition="'$(Configuration)' != 'EF'">false</Private>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Newtonsoft.Json.dll</HintPath>
<Private Condition="'$(Configuration)' != 'EF'">false</Private>
</Reference>
<Reference Include="FFXIVClientStructs">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\FFXIVClientStructs.dll</HintPath>
<Private Condition="'$(Configuration)' != 'EF'">false</Private>
</Reference>
<Reference Include="Serilog">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Serilog.dll</HintPath>
<Private Condition="'$(Configuration)' != 'EF'">false</Private>
</Reference>
</ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Update="Properties\Localization.resx"> <EmbeddedResource Update="Properties\Localization.resx">
<Generator>ResXFileCodeGenerator</Generator> <Generator>ResXFileCodeGenerator</Generator>
@ -63,6 +116,17 @@
</Compile> </Compile>
</ItemGroup> </ItemGroup>
<Target Name="PopulateInfo" DependsOnTargets="GitVersion" BeforeTargets="GetAssemblyVersion;GenerateNuspec;GetPackageContents">
<PropertyGroup>
<Version>$(GitSemVerMajor).$(GitSemVerMinor)</Version>
<PackageVersion>$(Version)</PackageVersion>
</PropertyGroup>
</Target>
<Target Name="RenameLatestZip" AfterTargets="PackagePlugin" Condition="'$(Configuration)' == 'Release'">
<Exec Command="rename &quot;$(OutDir)$(AssemblyName)\latest.zip&quot; &quot;$(AssemblyName)-$(Version).zip&quot;"/>
</Target>
<Target Name="Clean"> <Target Name="Clean">
<RemoveDir Directories="dist"/> <RemoveDir Directories="dist"/>
</Target> </Target>

View File

@ -3,8 +3,8 @@
"Author": "Liza Carvelli", "Author": "Liza Carvelli",
"Punchline": "Shows possible trap & hoard coffer locations in Palace of the Dead & Heaven on High.", "Punchline": "Shows possible trap & hoard coffer locations in Palace of the Dead & Heaven on High.",
"Description": "Shows possible trap & hoard coffer locations in Palace of the Dead & Heaven on High.\n\nThe default configuration requires Splatoon to be installed. If you do not wish to install Splatoon, you can switch to the experimental 'Simple' renderer in the configuration.", "Description": "Shows possible trap & hoard coffer locations in Palace of the Dead & Heaven on High.\n\nThe default configuration requires Splatoon to be installed. If you do not wish to install Splatoon, you can switch to the experimental 'Simple' renderer in the configuration.",
"RepoUrl": "https://git.carvel.li/liza/PalacePal", "RepoUrl": "https://github.com/carvelli/PalacePal",
"IconUrl": "https://plugins.carvel.li/icons/PalacePal.png", "IconUrl": "https://raw.githubusercontent.com/carvelli/Dalamud-Plugins/master/dist/Palace Pal.png",
"Tags": [ "Tags": [
"potd", "potd",
"palace", "palace",

View File

@ -5,11 +5,12 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Extensions.MicrosoftLogging; using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.Command; using Dalamud.Game.Command;
using Dalamud.Game.Gui;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using ECommons; using ECommons;
using ECommons.DalamudServices; using ECommons.DalamudServices;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -20,216 +21,217 @@ using Pal.Client.DependencyInjection;
using Pal.Client.Properties; using Pal.Client.Properties;
using Pal.Client.Rendering; using Pal.Client.Rendering;
namespace Pal.Client; namespace Pal.Client
/// <summary>
/// With all DI logic elsewhere, this plugin shell really only takes care of a few things around events that
/// need to be sent to different receivers depending on priority or configuration .
/// </summary>
/// <see cref="DependencyInjectionContext"/>
internal sealed class Plugin : IDalamudPlugin
{ {
private readonly CancellationTokenSource _initCts = new(); /// <summary>
/// With all DI logic elsewhere, this plugin shell really only takes care of a few things around events that
private readonly IDalamudPluginInterface _pluginInterface; /// need to be sent to different receivers depending on priority or configuration .
private readonly ICommandManager _commandManager; /// </summary>
private readonly IClientState _clientState; /// <see cref="DependencyInjectionContext"/>
private readonly IChatGui _chatGui; internal sealed class Plugin : IDalamudPlugin
private readonly IFramework _framework;
private readonly TaskCompletionSource<IServiceScope> _rootScopeCompletionSource = new();
private ELoadState _loadState = ELoadState.Initializing;
private DependencyInjectionContext? _dependencyInjectionContext;
private ILogger _logger;
private WindowSystem? _windowSystem;
private IServiceScope? _rootScope;
private Action? _loginAction;
public Plugin(
IDalamudPluginInterface pluginInterface,
ICommandManager commandManager,
IClientState clientState,
IChatGui chatGui,
IFramework framework,
IPluginLog pluginLog)
{ {
_pluginInterface = pluginInterface; private readonly CancellationTokenSource _initCts = new();
_commandManager = commandManager;
_clientState = clientState;
_chatGui = chatGui;
_framework = framework;
_logger = new DalamudLoggerProvider(pluginLog).CreateLogger<Plugin>();
// set up the current UI language before creating anything private readonly DalamudPluginInterface _pluginInterface;
Localization.Culture = new CultureInfo(_pluginInterface.UiLanguage); private readonly CommandManager _commandManager;
private readonly ClientState _clientState;
private readonly ChatGui _chatGui;
private readonly Framework _framework;
_commandManager.AddHandler("/pal", new CommandInfo(OnCommand) private readonly TaskCompletionSource<IServiceScope> _rootScopeCompletionSource = new();
private ELoadState _loadState = ELoadState.Initializing;
private DependencyInjectionContext? _dependencyInjectionContext;
private ILogger _logger = DependencyInjectionContext.LoggerProvider.CreateLogger<Plugin>();
private WindowSystem? _windowSystem;
private IServiceScope? _rootScope;
private Action? _loginAction;
public Plugin(
DalamudPluginInterface pluginInterface,
CommandManager commandManager,
ClientState clientState,
ChatGui chatGui,
Framework framework)
{ {
HelpMessage = Localization.Command_pal_HelpText _pluginInterface = pluginInterface;
}); _commandManager = commandManager;
_clientState = clientState;
_chatGui = chatGui;
_framework = framework;
// Using TickScheduler requires ECommons to at least be partially initialized // set up the current UI language before creating anything
// ECommonsMain.Dispose leaves this untouched. Localization.Culture = new CultureInfo(_pluginInterface.UiLanguage);
Svc.Init(pluginInterface);
Task.Run(async () => await CreateDependencyContext()); _commandManager.AddHandler("/pal", new CommandInfo(OnCommand)
}
private async Task CreateDependencyContext()
{
try
{
_dependencyInjectionContext = _pluginInterface.Create<DependencyInjectionContext>(this)
?? throw new Exception("Could not create DI root context class");
var serviceProvider = _dependencyInjectionContext.BuildServiceContainer();
_initCts.Token.ThrowIfCancellationRequested();
_logger = serviceProvider.GetRequiredService<ILogger<Plugin>>();
_windowSystem = serviceProvider.GetRequiredService<WindowSystem>();
_rootScope = serviceProvider.CreateScope();
var loader = _rootScope.ServiceProvider.GetRequiredService<DependencyContextInitializer>();
await loader.InitializeAsync(_initCts.Token);
await _framework.RunOnFrameworkThread(() =>
{ {
_pluginInterface.UiBuilder.Draw += Draw; HelpMessage = Localization.Command_pal_HelpText
_pluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi;
_pluginInterface.LanguageChanged += LanguageChanged;
_clientState.Login += Login;
}); });
_rootScopeCompletionSource.SetResult(_rootScope);
_loadState = ELoadState.Loaded; // Using TickScheduler requires ECommons to at least be partially initialized
// ECommonsMain.Dispose leaves this untouched.
Svc.Init(pluginInterface);
Task.Run(async () => await CreateDependencyContext());
} }
catch (Exception e) when (e is ObjectDisposedException
or OperationCanceledException public string Name => Localization.Palace_Pal;
or RepoVerification.RepoVerificationFailedException
|| (e is FileLoadException && _pluginInterface.IsDev)) private async Task CreateDependencyContext()
{ {
_rootScopeCompletionSource.SetException(e);
_loadState = ELoadState.Error;
}
catch (Exception e)
{
_rootScopeCompletionSource.SetException(e);
_logger.LogError(e, "Async load failed");
ShowErrorOnLogin(() =>
new Chat(_chatGui).Error(string.Format(Localization.Error_LoadFailed,
$"{e.GetType()} - {e.Message}")));
_loadState = ELoadState.Error;
}
}
private void ShowErrorOnLogin(Action? loginAction)
{
if (_clientState.IsLoggedIn)
{
loginAction?.Invoke();
_loginAction = null;
}
else
_loginAction = loginAction;
}
private void Login()
{
_loginAction?.Invoke();
_loginAction = null;
}
private void OnCommand(string command, string arguments)
{
arguments = arguments.Trim();
Task.Run(async () =>
{
IServiceScope rootScope;
Chat chat;
try try
{ {
rootScope = await _rootScopeCompletionSource.Task; _dependencyInjectionContext = _pluginInterface.Create<DependencyInjectionContext>(this)
chat = rootScope.ServiceProvider.GetRequiredService<Chat>(); ?? throw new Exception("Could not create DI root context class");
var serviceProvider = _dependencyInjectionContext.BuildServiceContainer();
_initCts.Token.ThrowIfCancellationRequested();
_logger = serviceProvider.GetRequiredService<ILogger<Plugin>>();
_windowSystem = serviceProvider.GetRequiredService<WindowSystem>();
_rootScope = serviceProvider.CreateScope();
var loader = _rootScope.ServiceProvider.GetRequiredService<DependencyContextInitializer>();
await loader.InitializeAsync(_initCts.Token);
await _framework.RunOnFrameworkThread(() =>
{
_pluginInterface.UiBuilder.Draw += Draw;
_pluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi;
_pluginInterface.LanguageChanged += LanguageChanged;
_clientState.Login += Login;
});
_rootScopeCompletionSource.SetResult(_rootScope);
_loadState = ELoadState.Loaded;
}
catch (Exception e) when (e is ObjectDisposedException
or OperationCanceledException
or RepoVerification.RepoVerificationFailedException
|| (e is FileLoadException && _pluginInterface.IsDev))
{
_rootScopeCompletionSource.SetException(e);
_loadState = ELoadState.Error;
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, "Could not wait for command root scope"); _rootScopeCompletionSource.SetException(e);
return; _logger.LogError(e, "Async load failed");
} ShowErrorOnLogin(() =>
new Chat(_chatGui).Error(string.Format(Localization.Error_LoadFailed,
$"{e.GetType()} - {e.Message}")));
try _loadState = ELoadState.Error;
}
}
private void ShowErrorOnLogin(Action? loginAction)
{
if (_clientState.IsLoggedIn)
{ {
IPalacePalConfiguration configuration = loginAction?.Invoke();
rootScope.ServiceProvider.GetRequiredService<IPalacePalConfiguration>(); _loginAction = null;
if (configuration.FirstUse && arguments != "" && arguments != "config") }
else
_loginAction = loginAction;
}
private void Login(object? sender, EventArgs eventArgs)
{
_loginAction?.Invoke();
_loginAction = null;
}
private void OnCommand(string command, string arguments)
{
arguments = arguments.Trim();
Task.Run(async () =>
{
IServiceScope rootScope;
Chat chat;
try
{ {
chat.Error(Localization.Error_FirstTimeSetupRequired); rootScope = await _rootScopeCompletionSource.Task;
chat = rootScope.ServiceProvider.GetRequiredService<Chat>();
}
catch (Exception e)
{
_logger.LogError(e, "Could not wait for command root scope");
return; return;
} }
Action<string> commandHandler = rootScope.ServiceProvider try
.GetRequiredService<IEnumerable<ISubCommand>>() {
.SelectMany(cmd => cmd.GetHandlers()) IPalacePalConfiguration configuration =
.Where(cmd => cmd.Key == arguments.ToLowerInvariant()) rootScope.ServiceProvider.GetRequiredService<IPalacePalConfiguration>();
.Select(cmd => cmd.Value) if (configuration.FirstUse && arguments != "" && arguments != "config")
.SingleOrDefault(missingCommand =>
{ {
chat.Error(string.Format(Localization.Command_pal_UnknownSubcommand, missingCommand, chat.Error(Localization.Error_FirstTimeSetupRequired);
command)); return;
}); }
commandHandler.Invoke(arguments);
}
catch (Exception e)
{
_logger.LogError(e, "Could not execute command '{Command}' with arguments '{Arguments}'", command,
arguments);
chat.Error(string.Format(Localization.Error_CommandFailed,
$"{e.GetType()} - {e.Message}"));
}
});
}
private void OpenConfigUi() Action<string> commandHandler = rootScope.ServiceProvider
=> _rootScope!.ServiceProvider.GetRequiredService<PalConfigCommand>().Execute(); .GetRequiredService<IEnumerable<ISubCommand>>()
.SelectMany(cmd => cmd.GetHandlers())
private void LanguageChanged(string languageCode) .Where(cmd => cmd.Key == arguments.ToLowerInvariant())
{ .Select(cmd => cmd.Value)
_logger.LogInformation("Language set to '{Language}'", languageCode); .SingleOrDefault(missingCommand =>
{
Localization.Culture = new CultureInfo(languageCode); chat.Error(string.Format(Localization.Command_pal_UnknownSubcommand, missingCommand,
_windowSystem!.Windows.OfType<ILanguageChanged>() command));
.Each(w => w.LanguageChanged()); });
} commandHandler.Invoke(arguments);
}
private void Draw() catch (Exception e)
{ {
_rootScope!.ServiceProvider.GetRequiredService<RenderAdapter>().DrawLayers(); _logger.LogError(e, "Could not execute command '{Command}' with arguments '{Arguments}'", command,
_windowSystem!.Draw(); arguments);
} chat.Error(string.Format(Localization.Error_CommandFailed,
$"{e.GetType()} - {e.Message}"));
public void Dispose() }
{ });
_commandManager.RemoveHandler("/pal");
if (_loadState == ELoadState.Loaded)
{
_pluginInterface.UiBuilder.Draw -= Draw;
_pluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi;
_pluginInterface.LanguageChanged -= LanguageChanged;
_clientState.Login -= Login;
} }
_initCts.Cancel(); private void OpenConfigUi()
_rootScope?.Dispose(); => _rootScope!.ServiceProvider.GetRequiredService<PalConfigCommand>().Execute();
_dependencyInjectionContext?.Dispose();
}
private enum ELoadState private void LanguageChanged(string languageCode)
{ {
Initializing, _logger.LogInformation("Language set to '{Language}'", languageCode);
Loaded,
Error Localization.Culture = new CultureInfo(languageCode);
_windowSystem!.Windows.OfType<ILanguageChanged>()
.Each(w => w.LanguageChanged());
}
private void Draw()
{
_rootScope!.ServiceProvider.GetRequiredService<RenderAdapter>().DrawLayers();
_windowSystem!.Draw();
}
public void Dispose()
{
_commandManager.RemoveHandler("/pal");
if (_loadState == ELoadState.Loaded)
{
_pluginInterface.UiBuilder.Draw -= Draw;
_pluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi;
_pluginInterface.LanguageChanged -= LanguageChanged;
_clientState.Login -= Login;
}
_initCts.Cancel();
_rootScope?.Dispose();
_dependencyInjectionContext?.Dispose();
}
private enum ELoadState
{
Initializing,
Loaded,
Error
}
} }
} }

View File

@ -0,0 +1,8 @@
using System.Reflection;
[assembly: AssemblyVersion(ThisAssembly.Git.SemVer.Major + "." + ThisAssembly.Git.SemVer.Minor)]
[assembly: AssemblyFileVersion(ThisAssembly.Git.SemVer.Major + "." + ThisAssembly.Git.SemVer.Minor)]
[assembly: AssemblyInformationalVersion(
ThisAssembly.Git.SemVer.Major + "." +
ThisAssembly.Git.SemVer.Minor + "+" +
ThisAssembly.Git.Commit)]

View File

@ -15,7 +15,6 @@ dotnet ef migrations add MigrationName --configuration EF
``` ```
To rebuild the compiled model: To rebuild the compiled model:
```shell ```shell
dotnet ef dbcontext optimize --output-dir Database/Compiled --namespace Pal.Client.Database.Compiled --configuration EF dotnet ef dbcontext optimize --output-dir Database/Compiled --namespace Pal.Client.Database.Compiled --configuration EF
``` ```

View File

@ -1,8 +1,9 @@
namespace Pal.Client.Rendering; namespace Pal.Client.Rendering
internal enum ELayer
{ {
TrapHoard, internal enum ELayer
RegularCoffers, {
Test, TrapHoard,
RegularCoffers,
Test,
}
} }

View File

@ -1,8 +1,9 @@
namespace Pal.Client.Rendering; namespace Pal.Client.Rendering
public interface IRenderElement
{ {
bool IsValid { get; } public interface IRenderElement
{
bool IsValid { get; }
bool Enabled { get; set; } uint Color { get; set; }
}
} }

View File

@ -3,17 +3,18 @@ using System.Numerics;
using Pal.Client.Configuration; using Pal.Client.Configuration;
using Pal.Client.Floors; using Pal.Client.Floors;
namespace Pal.Client.Rendering; namespace Pal.Client.Rendering
internal interface IRenderer
{ {
ERenderer GetConfigValue(); internal interface IRenderer
{
ERenderer GetConfigValue();
void SetLayer(ELayer layer, IReadOnlyList<IRenderElement> elements); void SetLayer(ELayer layer, IReadOnlyList<IRenderElement> elements);
void ResetLayer(ELayer layer); void ResetLayer(ELayer layer);
IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, bool enabled, uint color, bool fill = false); IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, uint color, bool fill = false);
void DrawDebugItems(uint trapColor, uint hoardColor); void DrawDebugItems(uint trapColor, uint hoardColor);
}
} }

View File

@ -1,23 +1,24 @@
using System.Collections.Generic; using System.Collections.Generic;
using Pal.Client.Floors; using Pal.Client.Floors;
namespace Pal.Client.Rendering; namespace Pal.Client.Rendering
internal sealed class MarkerConfig
{ {
private static readonly MarkerConfig EmptyConfig = new(); internal sealed class MarkerConfig
private static readonly Dictionary<MemoryLocation.EType, MarkerConfig> MarkerConfigs = new()
{ {
{ MemoryLocation.EType.Trap, new MarkerConfig { Radius = 1.7f } }, private static readonly MarkerConfig EmptyConfig = new();
{ MemoryLocation.EType.Hoard, new MarkerConfig { Radius = 1.7f, OffsetY = -0.03f } },
{ MemoryLocation.EType.SilverCoffer, new MarkerConfig { Radius = 1f } },
{ MemoryLocation.EType.GoldCoffer, new MarkerConfig { Radius = 1f } },
};
public float OffsetY { get; private init; } private static readonly Dictionary<MemoryLocation.EType, MarkerConfig> MarkerConfigs = new()
public float Radius { get; private init; } = 0.25f; {
{ MemoryLocation.EType.Trap, new MarkerConfig { Radius = 1.7f } },
{ MemoryLocation.EType.Hoard, new MarkerConfig { Radius = 1.7f, OffsetY = -0.03f } },
{ MemoryLocation.EType.SilverCoffer, new MarkerConfig { Radius = 1f } },
{ MemoryLocation.EType.GoldCoffer, new MarkerConfig { Radius = 1f } },
};
public static MarkerConfig ForType(MemoryLocation.EType type) => public float OffsetY { get; private init; }
MarkerConfigs.GetValueOrDefault(type, EmptyConfig); public float Radius { get; private init; } = 0.25f;
public static MarkerConfig ForType(MemoryLocation.EType type) =>
MarkerConfigs.GetValueOrDefault(type, EmptyConfig);
}
} }

View File

@ -6,73 +6,73 @@ using Microsoft.Extensions.Logging;
using Pal.Client.Configuration; using Pal.Client.Configuration;
using Pal.Client.Floors; using Pal.Client.Floors;
namespace Pal.Client.Rendering; namespace Pal.Client.Rendering
internal sealed class RenderAdapter : IRenderer, IDisposable
{ {
private readonly IServiceScopeFactory _serviceScopeFactory; internal sealed class RenderAdapter : IRenderer, IDisposable
private readonly ILogger<RenderAdapter> _logger;
private readonly IPalacePalConfiguration _configuration;
private IServiceScope? _renderScope;
private IRenderer _implementation;
public RenderAdapter(IServiceScopeFactory serviceScopeFactory, ILogger<RenderAdapter> logger,
IPalacePalConfiguration configuration)
{ {
_serviceScopeFactory = serviceScopeFactory; private readonly IServiceScopeFactory _serviceScopeFactory;
_logger = logger; private readonly ILogger<RenderAdapter> _logger;
_configuration = configuration; private readonly IPalacePalConfiguration _configuration;
_implementation = Recreate(null); private IServiceScope? _renderScope;
} private IRenderer _implementation;
public bool RequireRedraw { get; set; } public RenderAdapter(IServiceScopeFactory serviceScopeFactory, ILogger<RenderAdapter> logger,
IPalacePalConfiguration configuration)
{
_serviceScopeFactory = serviceScopeFactory;
_logger = logger;
_configuration = configuration;
private IRenderer Recreate(ERenderer? currentRenderer) _implementation = Recreate(null);
{ }
ERenderer targetRenderer = _configuration.Renderer.SelectedRenderer;
if (targetRenderer == currentRenderer)
return _implementation;
_renderScope?.Dispose(); public bool RequireRedraw { get; set; }
_logger.LogInformation("Selected new renderer: {Renderer}", _configuration.Renderer.SelectedRenderer); private IRenderer Recreate(ERenderer? currentRenderer)
_renderScope = _serviceScopeFactory.CreateScope(); {
if (targetRenderer == ERenderer.Splatoon) ERenderer targetRenderer = _configuration.Renderer.SelectedRenderer;
return _renderScope.ServiceProvider.GetRequiredService<SplatoonRenderer>(); if (targetRenderer == currentRenderer)
else return _implementation;
return _renderScope.ServiceProvider.GetRequiredService<SimpleRenderer>();
}
public void ConfigUpdated() _renderScope?.Dispose();
{
_implementation = Recreate(_implementation.GetConfigValue());
RequireRedraw = true;
}
public void Dispose() _logger.LogInformation("Selected new renderer: {Renderer}", _configuration.Renderer.SelectedRenderer);
=> _renderScope?.Dispose(); _renderScope = _serviceScopeFactory.CreateScope();
if (targetRenderer == ERenderer.Splatoon)
return _renderScope.ServiceProvider.GetRequiredService<SplatoonRenderer>();
else
return _renderScope.ServiceProvider.GetRequiredService<SimpleRenderer>();
}
public void SetLayer(ELayer layer, IReadOnlyList<IRenderElement> elements) public void ConfigUpdated()
=> _implementation.SetLayer(layer, elements); {
_implementation = Recreate(_implementation.GetConfigValue());
RequireRedraw = true;
}
public void ResetLayer(ELayer layer) public void Dispose()
=> _implementation.ResetLayer(layer); => _renderScope?.Dispose();
public IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, bool enabled, uint color, public void SetLayer(ELayer layer, IReadOnlyList<IRenderElement> elements)
bool fill = false) => _implementation.SetLayer(layer, elements);
=> _implementation.CreateElement(type, pos, enabled, color, fill);
public ERenderer GetConfigValue() public void ResetLayer(ELayer layer)
=> throw new NotImplementedException(); => _implementation.ResetLayer(layer);
public void DrawDebugItems(uint trapColor, uint hoardColor) public IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, uint color, bool fill = false)
=> _implementation.DrawDebugItems(trapColor, hoardColor); => _implementation.CreateElement(type, pos, color, fill);
public void DrawLayers() public ERenderer GetConfigValue()
{ => throw new NotImplementedException();
if (_implementation is SimpleRenderer sr)
sr.DrawLayers(); public void DrawDebugItems(uint trapColor, uint hoardColor)
=> _implementation.DrawDebugItems(trapColor, hoardColor);
public void DrawLayers()
{
if (_implementation is SimpleRenderer sr)
sr.DrawLayers();
}
} }
} }

View File

@ -1,6 +1,8 @@
namespace Pal.Client.Rendering; namespace Pal.Client.Rendering
internal static class RenderData
{ {
public static readonly long TestLayerTimeout = 10_000; internal static class RenderData
{
public static readonly uint ColorInvisible = 0;
public static readonly long TestLayerTimeout = 10_000;
}
} }

View File

@ -3,204 +3,202 @@ using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Interface.Utility; using Dalamud.Game.ClientState;
using Dalamud.Plugin.Services; using Dalamud.Game.Gui;
using Dalamud.Interface;
using ImGuiNET; using ImGuiNET;
using Pal.Client.Configuration; using Pal.Client.Configuration;
using Pal.Client.DependencyInjection;
using Pal.Client.Floors; using Pal.Client.Floors;
namespace Pal.Client.Rendering; namespace Pal.Client.Rendering
/// <summary>
/// Simple renderer that only draws basic stuff.
///
/// This is based on what SliceIsRight uses, and what PalacePal used before it was
/// remade into PalacePal (which is the third or fourth iteration on the same idea
/// I made, just with a clear vision).
/// </summary>
internal sealed class SimpleRenderer : IRenderer, IDisposable
{ {
private const int SegmentCount = 20; /// <summary>
/// Simple renderer that only draws basic stuff.
private readonly IClientState _clientState; ///
private readonly IGameGui _gameGui; /// This is based on what SliceIsRight uses, and what PalacePal used before it was
private readonly IPalacePalConfiguration _configuration; /// remade into PalacePal (which is the third or fourth iteration on the same idea
private readonly TerritoryState _territoryState; /// I made, just with a clear vision).
private readonly ConcurrentDictionary<ELayer, SimpleLayer> _layers = new(); /// </summary>
internal sealed class SimpleRenderer : IRenderer, IDisposable
public SimpleRenderer(IClientState clientState, IGameGui gameGui, IPalacePalConfiguration configuration,
TerritoryState territoryState)
{ {
_clientState = clientState; private const int SegmentCount = 20;
_gameGui = gameGui;
_configuration = configuration;
_territoryState = territoryState;
}
public void SetLayer(ELayer layer, IReadOnlyList<IRenderElement> elements) private readonly ClientState _clientState;
{ private readonly GameGui _gameGui;
_layers[layer] = new SimpleLayer private readonly IPalacePalConfiguration _configuration;
private readonly TerritoryState _territoryState;
private readonly ConcurrentDictionary<ELayer, SimpleLayer> _layers = new();
public SimpleRenderer(ClientState clientState, GameGui gameGui, IPalacePalConfiguration configuration,
TerritoryState territoryState)
{ {
TerritoryType = _clientState.TerritoryType, _clientState = clientState;
Elements = elements.Cast<SimpleElement>().ToList() _gameGui = gameGui;
}; _configuration = configuration;
} _territoryState = territoryState;
}
public void ResetLayer(ELayer layer) public void SetLayer(ELayer layer, IReadOnlyList<IRenderElement> elements)
{
if (_layers.Remove(layer, out var l))
l.Dispose();
}
public IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, bool enabled, uint color,
bool fill = false)
{
var config = MarkerConfig.ForType(type);
return new SimpleElement
{ {
Type = type, _layers[layer] = new SimpleLayer
Position = pos + new Vector3(0, config.OffsetY, 0),
Enabled = enabled,
Color = color,
Radius = config.Radius,
Fill = fill,
};
}
public void DrawDebugItems(uint trapColor, uint hoardColor)
{
_layers[ELayer.Test] = new SimpleLayer
{
TerritoryType = _clientState.TerritoryType,
Elements = new List<SimpleElement>
{ {
(SimpleElement)CreateElement( TerritoryType = _clientState.TerritoryType,
MemoryLocation.EType.Trap, Elements = elements.Cast<SimpleElement>().ToList()
_clientState.LocalPlayer?.Position ?? default, };
true, }
trapColor),
(SimpleElement)CreateElement(
MemoryLocation.EType.Hoard,
_clientState.LocalPlayer?.Position ?? default,
true,
hoardColor)
},
ExpiresAt = Environment.TickCount64 + RenderData.TestLayerTimeout
};
}
public void DrawLayers() public void ResetLayer(ELayer layer)
{
if (_layers.Count == 0)
return;
ImGuiHelpers.ForceNextWindowMainViewport();
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
ImGuiHelpers.SetNextWindowPosRelativeMainViewport(Vector2.Zero, ImGuiCond.None, Vector2.Zero);
ImGui.SetNextWindowSize(ImGuiHelpers.MainViewport.Size);
if (ImGui.Begin("###PalacePalSimpleRender",
ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoBackground |
ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoSavedSettings |
ImGuiWindowFlags.AlwaysUseWindowPadding))
{ {
foreach (var layer in _layers.Values.Where(l => l.IsValid(_clientState))) if (_layers.Remove(layer, out var l))
l.Dispose();
}
public IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, uint color, bool fill = false)
{
var config = MarkerConfig.ForType(type);
return new SimpleElement
{ {
foreach (var e in layer.Elements) Type = type,
Draw(e); Position = pos + new Vector3(0, config.OffsetY, 0),
Color = color,
Radius = config.Radius,
Fill = fill,
};
}
public void DrawDebugItems(uint trapColor, uint hoardColor)
{
_layers[ELayer.Test] = new SimpleLayer
{
TerritoryType = _clientState.TerritoryType,
Elements = new List<SimpleElement>
{
(SimpleElement)CreateElement(
MemoryLocation.EType.Trap,
_clientState.LocalPlayer?.Position ?? default,
trapColor),
(SimpleElement)CreateElement(
MemoryLocation.EType.Hoard,
_clientState.LocalPlayer?.Position ?? default,
hoardColor)
},
ExpiresAt = Environment.TickCount64 + RenderData.TestLayerTimeout
};
}
public void DrawLayers()
{
if (_layers.Count == 0)
return;
ImGuiHelpers.ForceNextWindowMainViewport();
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
ImGuiHelpers.SetNextWindowPosRelativeMainViewport(Vector2.Zero, ImGuiCond.None, Vector2.Zero);
ImGui.SetNextWindowSize(ImGuiHelpers.MainViewport.Size);
if (ImGui.Begin("###PalacePalSimpleRender",
ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoBackground |
ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoSavedSettings |
ImGuiWindowFlags.AlwaysUseWindowPadding))
{
foreach (var layer in _layers.Values.Where(l => l.IsValid(_clientState)))
{
foreach (var e in layer.Elements)
Draw(e);
}
foreach (var key in _layers.Where(l => !l.Value.IsValid(_clientState))
.Select(l => l.Key)
.ToList())
ResetLayer(key);
ImGui.End();
} }
foreach (var key in _layers.Where(l => !l.Value.IsValid(_clientState)) ImGui.PopStyleVar();
.Select(l => l.Key)
.ToList())
ResetLayer(key);
ImGui.End();
} }
ImGui.PopStyleVar(); private void Draw(SimpleElement e)
}
private void Draw(SimpleElement e)
{
if (!e.Enabled)
return;
switch (e.Type)
{ {
case MemoryLocation.EType.Hoard: if (e.Color == RenderData.ColorInvisible)
// ignore distance if this is a found hoard coffer return;
if (_territoryState.PomanderOfIntuition == PomanderState.Active &&
_configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander) switch (e.Type)
{
case MemoryLocation.EType.Hoard:
// ignore distance if this is a found hoard coffer
if (_territoryState.PomanderOfIntuition == PomanderState.Active &&
_configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander)
break;
goto case MemoryLocation.EType.Trap;
case MemoryLocation.EType.Trap:
var playerPos = _clientState.LocalPlayer?.Position;
if (playerPos == null)
return;
if ((playerPos.Value - e.Position).Length() > 65)
return;
break; break;
}
goto case MemoryLocation.EType.Trap; bool onScreen = false;
for (int index = 0; index < 2 * SegmentCount; ++index)
{
onScreen |= _gameGui.WorldToScreen(new Vector3(
e.Position.X + e.Radius * (float)Math.Sin(Math.PI / SegmentCount * index),
e.Position.Y,
e.Position.Z + e.Radius * (float)Math.Cos(Math.PI / SegmentCount * index)),
out Vector2 vector2);
case MemoryLocation.EType.Trap: ImGui.GetWindowDrawList().PathLineTo(vector2);
var playerPos = _clientState.LocalPlayer?.Position; }
if (playerPos == null)
return;
if ((playerPos.Value - e.Position).Length() > 65) if (onScreen)
return; {
break; if (e.Fill)
} ImGui.GetWindowDrawList().PathFillConvex(e.Color);
else
bool onScreen = false; ImGui.GetWindowDrawList().PathStroke(e.Color, ImDrawFlags.Closed, 2);
for (int index = 0; index < 2 * SegmentCount; ++index) }
{
onScreen |= _gameGui.WorldToScreen(new Vector3(
e.Position.X + e.Radius * (float)Math.Sin(Math.PI / SegmentCount * index),
e.Position.Y,
e.Position.Z + e.Radius * (float)Math.Cos(Math.PI / SegmentCount * index)),
out Vector2 vector2);
ImGui.GetWindowDrawList().PathLineTo(vector2);
}
if (onScreen)
{
if (e.Fill)
ImGui.GetWindowDrawList().PathFillConvex(e.Color);
else else
ImGui.GetWindowDrawList().PathStroke(e.Color, ImDrawFlags.Closed, 2); ImGui.GetWindowDrawList().PathClear();
} }
else
ImGui.GetWindowDrawList().PathClear();
}
public ERenderer GetConfigValue() public ERenderer GetConfigValue()
=> ERenderer.Simple; => ERenderer.Simple;
public void Dispose()
{
foreach (var l in _layers.Values)
l.Dispose();
}
public sealed class SimpleLayer : IDisposable
{
public required ushort TerritoryType { get; init; }
public required IReadOnlyList<SimpleElement> Elements { get; init; }
public long ExpiresAt { get; init; } = long.MaxValue;
public bool IsValid(IClientState clientState) =>
TerritoryType == clientState.TerritoryType && ExpiresAt >= Environment.TickCount64;
public void Dispose() public void Dispose()
{ {
foreach (var e in Elements) foreach (var l in _layers.Values)
e.IsValid = false; l.Dispose();
}
public sealed class SimpleLayer : IDisposable
{
public required ushort TerritoryType { get; init; }
public required IReadOnlyList<SimpleElement> Elements { get; init; }
public long ExpiresAt { get; init; } = long.MaxValue;
public bool IsValid(ClientState clientState) =>
TerritoryType == clientState.TerritoryType && ExpiresAt >= Environment.TickCount64;
public void Dispose()
{
foreach (var e in Elements)
e.IsValid = false;
}
}
public sealed class SimpleElement : IRenderElement
{
public bool IsValid { get; set; } = true;
public required MemoryLocation.EType Type { get; init; }
public required Vector3 Position { get; init; }
public required uint Color { get; set; }
public required float Radius { get; init; }
public required bool Fill { get; init; }
} }
} }
public sealed class SimpleElement : IRenderElement
{
public bool IsValid { get; set; } = true;
public required MemoryLocation.EType Type { get; init; }
public required Vector3 Position { get; init; }
public required bool Enabled { get; set; }
public required uint Color { get; set; }
public required float Radius { get; init; }
public required bool Fill { get; init; }
}
} }

View File

@ -4,8 +4,8 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Reflection; using System.Reflection;
using Dalamud.Game.ClientState;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using ECommons; using ECommons;
using ECommons.Reflection; using ECommons.Reflection;
using ECommons.Schedulers; using ECommons.Schedulers;
@ -15,182 +15,182 @@ using Pal.Client.Configuration;
using Pal.Client.DependencyInjection; using Pal.Client.DependencyInjection;
using Pal.Client.Floors; using Pal.Client.Floors;
namespace Pal.Client.Rendering; namespace Pal.Client.Rendering
internal sealed class SplatoonRenderer : IRenderer, IDisposable
{ {
private const long OnTerritoryChange = -2; internal sealed class SplatoonRenderer : IRenderer, IDisposable
private readonly ILogger<SplatoonRenderer> _logger;
private readonly DebugState _debugState;
private readonly IClientState _clientState;
private readonly Chat _chat;
public SplatoonRenderer(
ILogger<SplatoonRenderer> logger,
IDalamudPluginInterface pluginInterface,
IDalamudPlugin dalamudPlugin,
DebugState debugState,
IClientState clientState,
Chat chat)
{ {
_logger = logger; private const long OnTerritoryChange = -2;
_debugState = debugState;
_clientState = clientState;
_chat = chat;
_logger.LogInformation("Initializing splatoon"); private readonly ILogger<SplatoonRenderer> _logger;
ECommonsMain.Init(pluginInterface, dalamudPlugin, ECommons.Module.SplatoonAPI); private readonly DebugState _debugState;
} private readonly ClientState _clientState;
private readonly Chat _chat;
private bool IsDisposed { get; set; } public SplatoonRenderer(
ILogger<SplatoonRenderer> logger,
DalamudPluginInterface pluginInterface,
IDalamudPlugin dalamudPlugin,
DebugState debugState,
ClientState clientState,
Chat chat)
{
_logger = logger;
_debugState = debugState;
_clientState = clientState;
_chat = chat;
public void SetLayer(ELayer layer, IReadOnlyList<IRenderElement> elements) _logger.LogInformation("Initializing splatoon");
{ ECommonsMain.Init(pluginInterface, dalamudPlugin, ECommons.Module.SplatoonAPI);
// we need to delay this, as the current framework update could be before splatoon's, in which case it would immediately delete the layout }
_ = new TickScheduler(delegate
private bool IsDisposed { get; set; }
public void SetLayer(ELayer layer, IReadOnlyList<IRenderElement> elements)
{
// we need to delay this, as the current framework update could be before splatoon's, in which case it would immediately delete the layout
_ = new TickScheduler(delegate
{
try
{
Splatoon.AddDynamicElements(ToLayerName(layer),
elements.Cast<SplatoonElement>().Select(x => x.Delegate).ToArray(),
new[] { Environment.TickCount64 + 60 * 60 * 1000, OnTerritoryChange });
}
catch (Exception e)
{
_logger.LogError(e, "Could not create splatoon layer {Layer} with {Count} elements", layer,
elements.Count);
_debugState.SetFromException(e);
}
});
}
public void ResetLayer(ELayer layer)
{ {
try try
{ {
Splatoon.AddDynamicElements(ToLayerName(layer), Splatoon.RemoveDynamicElements(ToLayerName(layer));
elements.Cast<SplatoonElement>().Select(x => x.Delegate).ToArray(),
new[] { Environment.TickCount64 + 60 * 60 * 1000, OnTerritoryChange });
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, "Could not create splatoon layer {Layer} with {Count} elements", layer, _logger.LogError(e, "Could not reset splatoon layer {Layer}", layer);
elements.Count);
_debugState.SetFromException(e);
} }
});
}
public void ResetLayer(ELayer layer)
{
try
{
Splatoon.RemoveDynamicElements(ToLayerName(layer));
} }
catch (Exception e)
{
_logger.LogError(e, "Could not reset splatoon layer {Layer}", layer);
}
}
private string ToLayerName(ELayer layer) private string ToLayerName(ELayer layer)
=> $"PalacePal.{layer}"; => $"PalacePal.{layer}";
public IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, bool enabled, uint color, bool fill = false) public IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, uint color, bool fill = false)
{
MarkerConfig config = MarkerConfig.ForType(type);
Element element = new Element(ElementType.CircleAtFixedCoordinates)
{ {
refX = pos.X, MarkerConfig config = MarkerConfig.ForType(type);
refY = pos.Z, // z and y are swapped Element element = new Element(ElementType.CircleAtFixedCoordinates)
refZ = pos.Y,
offX = 0,
offY = 0,
offZ = config.OffsetY,
Filled = fill,
radius = config.Radius,
FillStep = 1,
color = color,
thicc = 2,
Enabled = enabled,
};
return new SplatoonElement(this, element);
}
public void DrawDebugItems(uint trapColor, uint hoardColor)
{
try
{
Vector3? pos = _clientState.LocalPlayer?.Position;
if (pos != null)
{ {
ResetLayer(ELayer.Test); refX = pos.X,
refY = pos.Z, // z and y are swapped
var elements = new List<IRenderElement> refZ = pos.Y,
{ offX = 0,
CreateElement(MemoryLocation.EType.Trap, pos.Value, true, trapColor), offY = 0,
CreateElement(MemoryLocation.EType.Hoard, pos.Value, true, hoardColor), offZ = config.OffsetY,
}; Filled = fill,
radius = config.Radius,
if (!Splatoon.AddDynamicElements(ToLayerName(ELayer.Test), FillStep = 1,
elements.Cast<SplatoonElement>().Select(x => x.Delegate).ToArray(), color = color,
new[] { Environment.TickCount64 + RenderData.TestLayerTimeout })) thicc = 2,
{ };
_chat.Message("Could not draw markers :("); return new SplatoonElement(this, element);
}
}
} }
catch (Exception)
public void DrawDebugItems(uint trapColor, uint hoardColor)
{ {
try try
{ {
var pluginManager = DalamudReflector.GetPluginManager(); Vector3? pos = _clientState.LocalPlayer?.Position;
IList installedPlugins = if (pos != null)
pluginManager.GetType().GetProperty("InstalledPlugins")?.GetValue(pluginManager) as IList ??
new List<object>();
foreach (var t in installedPlugins)
{ {
AssemblyName? assemblyName = ResetLayer(ELayer.Test);
(AssemblyName?)t.GetType().GetProperty("AssemblyName")?.GetValue(t);
string? pluginName = (string?)t.GetType().GetProperty("Name")?.GetValue(t); var elements = new List<IRenderElement>
if (assemblyName?.Name == "Splatoon" && pluginName != "Splatoon")
{ {
_chat.Error( CreateElement(MemoryLocation.EType.Trap, pos.Value, trapColor),
$"Splatoon is installed under the plugin name '{pluginName}', which is incompatible with the Splatoon API."); CreateElement(MemoryLocation.EType.Hoard, pos.Value, hoardColor),
_chat.Message( };
"You need to install Splatoon from the official repository at https://github.com/NightmareXIV/MyDalamudPlugins.");
return; if (!Splatoon.AddDynamicElements(ToLayerName(ELayer.Test),
elements.Cast<SplatoonElement>().Select(x => x.Delegate).ToArray(),
new[] { Environment.TickCount64 + RenderData.TestLayerTimeout }))
{
_chat.Message("Could not draw markers :(");
} }
} }
} }
catch (Exception) catch (Exception)
{ {
// not relevant try
{
var pluginManager = DalamudReflector.GetPluginManager();
IList installedPlugins =
pluginManager.GetType().GetProperty("InstalledPlugins")?.GetValue(pluginManager) as IList ??
new List<object>();
foreach (var t in installedPlugins)
{
AssemblyName? assemblyName =
(AssemblyName?)t.GetType().GetProperty("AssemblyName")?.GetValue(t);
string? pluginName = (string?)t.GetType().GetProperty("Name")?.GetValue(t);
if (assemblyName?.Name == "Splatoon" && pluginName != "Splatoon")
{
_chat.Error(
$"Splatoon is installed under the plugin name '{pluginName}', which is incompatible with the Splatoon API.");
_chat.Message(
"You need to install Splatoon from the official repository at https://github.com/NightmareXIV/MyDalamudPlugins.");
return;
}
}
}
catch (Exception)
{
// not relevant
}
_chat.Error("Could not draw markers, is Splatoon installed and enabled?");
}
}
public ERenderer GetConfigValue()
=> ERenderer.Splatoon;
public void Dispose()
{
_logger.LogInformation("Disposing splatoon");
IsDisposed = true;
ResetLayer(ELayer.TrapHoard);
ResetLayer(ELayer.RegularCoffers);
ResetLayer(ELayer.Test);
ECommonsMain.Dispose();
}
private sealed class SplatoonElement : IRenderElement
{
private readonly SplatoonRenderer _renderer;
public SplatoonElement(SplatoonRenderer renderer, Element element)
{
_renderer = renderer;
Delegate = element;
} }
_chat.Error("Could not draw markers, is Splatoon installed and enabled?"); public Element Delegate { get; }
}
}
public ERenderer GetConfigValue() public bool IsValid => !_renderer.IsDisposed && Delegate.IsValid();
=> ERenderer.Splatoon;
public void Dispose() public uint Color
{ {
_logger.LogInformation("Disposing splatoon"); get => Delegate.color;
set => Delegate.color = value;
IsDisposed = true; }
ResetLayer(ELayer.TrapHoard);
ResetLayer(ELayer.RegularCoffers);
ResetLayer(ELayer.Test);
ECommonsMain.Dispose();
}
private sealed class SplatoonElement : IRenderElement
{
private readonly SplatoonRenderer _renderer;
public SplatoonElement(SplatoonRenderer renderer, Element element)
{
_renderer = renderer;
Delegate = element;
}
public Element Delegate { get; }
public bool IsValid => !_renderer.IsDisposed && Delegate.IsValid();
public bool Enabled
{
get => Delegate.Enabled;
set => Delegate.Enabled = value;
} }
} }
} }

View File

@ -2,37 +2,38 @@
using Dalamud.Logging; using Dalamud.Logging;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Pal.Client.Scheduled; namespace Pal.Client.Scheduled
internal interface IQueueOnFrameworkThread
{ {
internal interface IHandler internal interface IQueueOnFrameworkThread
{ {
void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout); internal interface IHandler
}
internal abstract class Handler<T> : IHandler
where T : IQueueOnFrameworkThread
{
protected readonly ILogger<Handler<T>> _logger;
protected Handler(ILogger<Handler<T>> logger)
{ {
_logger = logger; void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout);
} }
protected abstract void Run(T queued, ref bool recreateLayout); internal abstract class Handler<T> : IHandler
where T : IQueueOnFrameworkThread
public void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout)
{ {
if (queued is T t) protected readonly ILogger<Handler<T>> _logger;
protected Handler(ILogger<Handler<T>> logger)
{ {
_logger.LogDebug("Handling {QueuedType}", queued.GetType()); _logger = logger;
Run(t, ref recreateLayout);
} }
else
protected abstract void Run(T queued, ref bool recreateLayout);
public void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout)
{ {
_logger.LogError("Could not use queue handler {QueuedType}", queued.GetType()); if (queued is T t)
{
_logger.LogDebug("Handling {QueuedType}", queued.GetType());
Run(t, ref recreateLayout);
}
else
{
_logger.LogError("Could not use queue handler {QueuedType}", queued.GetType());
}
} }
} }
} }

View File

@ -4,25 +4,26 @@ using Pal.Client.DependencyInjection;
using Pal.Client.Floors; using Pal.Client.Floors;
using Pal.Client.Rendering; using Pal.Client.Rendering;
namespace Pal.Client.Scheduled; namespace Pal.Client.Scheduled
internal sealed class QueuedConfigUpdate : IQueueOnFrameworkThread
{ {
internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedConfigUpdate> internal sealed class QueuedConfigUpdate : IQueueOnFrameworkThread
{ {
private readonly RenderAdapter _renderAdapter; internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedConfigUpdate>
public Handler(
ILogger<Handler> logger,
RenderAdapter renderAdapter)
: base(logger)
{ {
_renderAdapter = renderAdapter; private readonly RenderAdapter _renderAdapter;
}
protected override void Run(QueuedConfigUpdate queued, ref bool recreateLayout) public Handler(
{ ILogger<Handler> logger,
_renderAdapter.ConfigUpdated(); RenderAdapter renderAdapter)
: base(logger)
{
_renderAdapter = renderAdapter;
}
protected override void Run(QueuedConfigUpdate queued, ref bool recreateLayout)
{
_renderAdapter.ConfigUpdated();
}
} }
} }
} }

View File

@ -10,113 +10,114 @@ using Pal.Client.Properties;
using Pal.Client.Windows; using Pal.Client.Windows;
using Pal.Common; using Pal.Common;
namespace Pal.Client.Scheduled; namespace Pal.Client.Scheduled
internal sealed class QueuedImport : IQueueOnFrameworkThread
{ {
private ExportRoot Export { get; } internal sealed class QueuedImport : IQueueOnFrameworkThread
private Guid ExportId { get; set; }
private int ImportedTraps { get; set; }
private int ImportedHoardCoffers { get; set; }
public QueuedImport(string sourcePath)
{ {
using var input = File.OpenRead(sourcePath); private ExportRoot Export { get; }
Export = ExportRoot.Parser.ParseFrom(input); private Guid ExportId { get; set; }
} private int ImportedTraps { get; set; }
private int ImportedHoardCoffers { get; set; }
internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedImport> public QueuedImport(string sourcePath)
{
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly Chat _chat;
private readonly ImportService _importService;
private readonly ConfigWindow _configWindow;
public Handler(
ILogger<Handler> logger,
IServiceScopeFactory serviceScopeFactory,
Chat chat,
ImportService importService,
ConfigWindow configWindow)
: base(logger)
{ {
_serviceScopeFactory = serviceScopeFactory; using var input = File.OpenRead(sourcePath);
_chat = chat; Export = ExportRoot.Parser.ParseFrom(input);
_importService = importService;
_configWindow = configWindow;
} }
protected override void Run(QueuedImport import, ref bool recreateLayout) internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedImport>
{ {
recreateLayout = true; private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly Chat _chat;
private readonly ImportService _importService;
private readonly ConfigWindow _configWindow;
try public Handler(
ILogger<Handler> logger,
IServiceScopeFactory serviceScopeFactory,
Chat chat,
ImportService importService,
ConfigWindow configWindow)
: base(logger)
{ {
if (!Validate(import)) _serviceScopeFactory = serviceScopeFactory;
return; _chat = chat;
_importService = importService;
_configWindow = configWindow;
}
Task.Run(() => protected override void Run(QueuedImport import, ref bool recreateLayout)
{
recreateLayout = true;
try
{ {
try if (!Validate(import))
return;
Task.Run(() =>
{ {
using (var scope = _serviceScopeFactory.CreateScope()) try
{ {
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>(); using (var scope = _serviceScopeFactory.CreateScope())
(import.ImportedTraps, import.ImportedHoardCoffers) = {
_importService.Import(import.Export); using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
(import.ImportedTraps, import.ImportedHoardCoffers) =
_importService.Import(import.Export);
}
_configWindow.UpdateLastImport();
_logger.LogInformation(
"Imported {ExportId} for {Traps} traps, {Hoard} hoard coffers", import.ExportId,
import.ImportedTraps, import.ImportedHoardCoffers);
_chat.Message(string.Format(Localization.ImportCompleteStatistics, import.ImportedTraps,
import.ImportedHoardCoffers));
} }
catch (Exception e)
_configWindow.UpdateLastImport(); {
_logger.LogError(e, "Import failed in inner task");
_logger.LogInformation( _chat.Error(string.Format(Localization.Error_ImportFailed, e));
"Imported {ExportId} for {Traps} traps, {Hoard} hoard coffers", import.ExportId, }
import.ImportedTraps, import.ImportedHoardCoffers); });
_chat.Message(string.Format(Localization.ImportCompleteStatistics, import.ImportedTraps, }
import.ImportedHoardCoffers)); catch (Exception e)
} {
catch (Exception e) _logger.LogError(e, "Import failed");
{ _chat.Error(string.Format(Localization.Error_ImportFailed, e));
_logger.LogError(e, "Import failed in inner task"); }
_chat.Error(string.Format(Localization.Error_ImportFailed, e));
}
});
} }
catch (Exception e)
private bool Validate(QueuedImport import)
{ {
_logger.LogError(e, "Import failed"); if (import.Export.ExportVersion != ExportConfig.ExportVersion)
_chat.Error(string.Format(Localization.Error_ImportFailed, e)); {
_logger.LogError(
"Import: Different version in export file, {ExportVersion} != {ConfiguredVersion}",
import.Export.ExportVersion, ExportConfig.ExportVersion);
_chat.Error(Localization.Error_ImportFailed_IncompatibleVersion);
return false;
}
if (!Guid.TryParse(import.Export.ExportId, out Guid exportId) || exportId == Guid.Empty)
{
_logger.LogError("Import: Invalid export id '{Id}'", import.Export.ExportId);
_chat.Error(Localization.Error_ImportFailed_InvalidFile);
return false;
}
import.ExportId = exportId;
if (string.IsNullOrEmpty(import.Export.ServerUrl))
{
// If we allow for backups as import/export, this should be removed
_logger.LogError("Import: No server URL");
_chat.Error(Localization.Error_ImportFailed_InvalidFile);
return false;
}
return true;
} }
} }
private bool Validate(QueuedImport import)
{
if (import.Export.ExportVersion != ExportConfig.ExportVersion)
{
_logger.LogError(
"Import: Different version in export file, {ExportVersion} != {ConfiguredVersion}",
import.Export.ExportVersion, ExportConfig.ExportVersion);
_chat.Error(Localization.Error_ImportFailed_IncompatibleVersion);
return false;
}
if (!Guid.TryParse(import.Export.ExportId, out Guid exportId) || exportId == Guid.Empty)
{
_logger.LogError("Import: Invalid export id '{Id}'", import.Export.ExportId);
_chat.Error(Localization.Error_ImportFailed_InvalidFile);
return false;
}
import.ExportId = exportId;
if (string.IsNullOrEmpty(import.Export.ServerUrl))
{
// If we allow for backups as import/export, this should be removed
_logger.LogError("Import: No server URL");
_chat.Error(Localization.Error_ImportFailed_InvalidFile);
return false;
}
return true;
}
} }
} }

View File

@ -11,150 +11,151 @@ using Pal.Client.Floors.Tasks;
using Pal.Client.Net; using Pal.Client.Net;
using Pal.Common; using Pal.Common;
namespace Pal.Client.Scheduled; namespace Pal.Client.Scheduled
internal sealed class QueuedSyncResponse : IQueueOnFrameworkThread
{ {
public required SyncType Type { get; init; } internal sealed class QueuedSyncResponse : IQueueOnFrameworkThread
public required ushort TerritoryType { get; init; }
public required bool Success { get; init; }
public required IReadOnlyList<PersistentLocation> Locations { get; init; }
internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedSyncResponse>
{ {
private readonly IServiceScopeFactory _serviceScopeFactory; public required SyncType Type { get; init; }
private readonly IPalacePalConfiguration _configuration; public required ushort TerritoryType { get; init; }
private readonly FloorService _floorService; public required bool Success { get; init; }
private readonly TerritoryState _territoryState; public required IReadOnlyList<PersistentLocation> Locations { get; init; }
private readonly DebugState _debugState;
public Handler( internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedSyncResponse>
ILogger<Handler> logger,
IServiceScopeFactory serviceScopeFactory,
IPalacePalConfiguration configuration,
FloorService floorService,
TerritoryState territoryState,
DebugState debugState)
: base(logger)
{ {
_serviceScopeFactory = serviceScopeFactory; private readonly IServiceScopeFactory _serviceScopeFactory;
_configuration = configuration; private readonly IPalacePalConfiguration _configuration;
_floorService = floorService; private readonly FloorService _floorService;
_territoryState = territoryState; private readonly TerritoryState _territoryState;
_debugState = debugState; private readonly DebugState _debugState;
}
protected override void Run(QueuedSyncResponse queued, ref bool recreateLayout) public Handler(
{ ILogger<Handler> logger,
recreateLayout = true; IServiceScopeFactory serviceScopeFactory,
IPalacePalConfiguration configuration,
_logger.LogDebug( FloorService floorService,
"Sync response for territory {Territory} of type {Type}, success = {Success}, response objects = {Count}", TerritoryState territoryState,
(ETerritoryType)queued.TerritoryType, queued.Type, queued.Success, queued.Locations.Count); DebugState debugState)
var memoryTerritory = _floorService.GetTerritoryIfReady(queued.TerritoryType); : base(logger)
if (memoryTerritory == null)
{ {
_logger.LogWarning("Discarding sync response for territory {Territory} as it isn't ready", _serviceScopeFactory = serviceScopeFactory;
(ETerritoryType)queued.TerritoryType); _configuration = configuration;
return; _floorService = floorService;
_territoryState = territoryState;
_debugState = debugState;
} }
try protected override void Run(QueuedSyncResponse queued, ref bool recreateLayout)
{ {
var remoteMarkers = queued.Locations; recreateLayout = true;
if (_configuration.Mode == EMode.Online && queued.Success && remoteMarkers.Count > 0)
_logger.LogDebug(
"Sync response for territory {Territory} of type {Type}, success = {Success}, response objects = {Count}",
(ETerritoryType)queued.TerritoryType, queued.Type, queued.Success, queued.Locations.Count);
var memoryTerritory = _floorService.GetTerritoryIfReady(queued.TerritoryType);
if (memoryTerritory == null)
{ {
switch (queued.Type) _logger.LogWarning("Discarding sync response for territory {Territory} as it isn't ready",
{ (ETerritoryType)queued.TerritoryType);
case SyncType.Download: return;
case SyncType.Upload:
List<PersistentLocation> newLocations = new();
foreach (var remoteMarker in remoteMarkers)
{
// Both uploads and downloads return the network id to be set, but only the downloaded marker is new as in to-be-saved.
PersistentLocation? localLocation =
memoryTerritory.Locations.SingleOrDefault(x => x == remoteMarker);
if (localLocation != null)
{
localLocation.NetworkId = remoteMarker.NetworkId;
continue;
}
if (queued.Type == SyncType.Download)
{
memoryTerritory.Locations.Add(remoteMarker);
newLocations.Add(remoteMarker);
}
}
if (newLocations.Count > 0)
new SaveNewLocations(_serviceScopeFactory, memoryTerritory, newLocations).Start();
break;
case SyncType.MarkSeen:
var partialAccountId =
_configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId();
if (partialAccountId == null)
break;
List<PersistentLocation> locationsToUpdate = new();
foreach (var remoteMarker in remoteMarkers)
{
PersistentLocation? localLocation =
memoryTerritory.Locations.SingleOrDefault(x => x == remoteMarker);
if (localLocation != null)
{
localLocation.RemoteSeenOn.Add(partialAccountId);
locationsToUpdate.Add(localLocation);
}
}
if (locationsToUpdate.Count > 0)
{
new MarkRemoteSeen(_serviceScopeFactory, memoryTerritory, locationsToUpdate,
partialAccountId).Start();
}
break;
}
} }
// don't modify state for outdated floors try
if (_territoryState.LastTerritory != queued.TerritoryType)
return;
if (queued.Type == SyncType.Download)
{ {
if (queued.Success) var remoteMarkers = queued.Locations;
memoryTerritory.SyncState = ESyncState.Complete; if (_configuration.Mode == EMode.Online && queued.Success && remoteMarkers.Count > 0)
else {
switch (queued.Type)
{
case SyncType.Download:
case SyncType.Upload:
List<PersistentLocation> newLocations = new();
foreach (var remoteMarker in remoteMarkers)
{
// Both uploads and downloads return the network id to be set, but only the downloaded marker is new as in to-be-saved.
PersistentLocation? localLocation =
memoryTerritory.Locations.SingleOrDefault(x => x == remoteMarker);
if (localLocation != null)
{
localLocation.NetworkId = remoteMarker.NetworkId;
continue;
}
if (queued.Type == SyncType.Download)
{
memoryTerritory.Locations.Add(remoteMarker);
newLocations.Add(remoteMarker);
}
}
if (newLocations.Count > 0)
new SaveNewLocations(_serviceScopeFactory, memoryTerritory, newLocations).Start();
break;
case SyncType.MarkSeen:
var partialAccountId =
_configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId();
if (partialAccountId == null)
break;
List<PersistentLocation> locationsToUpdate = new();
foreach (var remoteMarker in remoteMarkers)
{
PersistentLocation? localLocation =
memoryTerritory.Locations.SingleOrDefault(x => x == remoteMarker);
if (localLocation != null)
{
localLocation.RemoteSeenOn.Add(partialAccountId);
locationsToUpdate.Add(localLocation);
}
}
if (locationsToUpdate.Count > 0)
{
new MarkRemoteSeen(_serviceScopeFactory, memoryTerritory, locationsToUpdate,
partialAccountId).Start();
}
break;
}
}
// don't modify state for outdated floors
if (_territoryState.LastTerritory != queued.TerritoryType)
return;
if (queued.Type == SyncType.Download)
{
if (queued.Success)
memoryTerritory.SyncState = ESyncState.Complete;
else
memoryTerritory.SyncState = ESyncState.Failed;
}
}
catch (Exception e)
{
_logger.LogError(e, "Sync failed for territory {Territory}", (ETerritoryType)queued.TerritoryType);
_debugState.SetFromException(e);
if (queued.Type == SyncType.Download)
memoryTerritory.SyncState = ESyncState.Failed; memoryTerritory.SyncState = ESyncState.Failed;
} }
} }
catch (Exception e)
{
_logger.LogError(e, "Sync failed for territory {Territory}", (ETerritoryType)queued.TerritoryType);
_debugState.SetFromException(e);
if (queued.Type == SyncType.Download)
memoryTerritory.SyncState = ESyncState.Failed;
}
} }
} }
}
public enum ESyncState public enum ESyncState
{ {
NotAttempted, NotAttempted,
NotNeeded, NotNeeded,
Started, Started,
Complete, Complete,
Failed, Failed,
} }
public enum SyncType public enum SyncType
{ {
Upload, Upload,
Download, Download,
MarkSeen, MarkSeen,
}
} }

View File

@ -7,35 +7,36 @@ using Pal.Client.Floors;
using Pal.Client.Windows; using Pal.Client.Windows;
using Pal.Common; using Pal.Common;
namespace Pal.Client.Scheduled; namespace Pal.Client.Scheduled
internal sealed class QueuedUndoImport : IQueueOnFrameworkThread
{ {
public QueuedUndoImport(Guid exportId) internal sealed class QueuedUndoImport : IQueueOnFrameworkThread
{ {
ExportId = exportId; public QueuedUndoImport(Guid exportId)
}
private Guid ExportId { get; }
internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedUndoImport>
{
private readonly ImportService _importService;
private readonly ConfigWindow _configWindow;
public Handler(ILogger<Handler> logger, ImportService importService, ConfigWindow configWindow)
: base(logger)
{ {
_importService = importService; ExportId = exportId;
_configWindow = configWindow;
} }
protected override void Run(QueuedUndoImport queued, ref bool recreateLayout) private Guid ExportId { get; }
{
recreateLayout = true;
_importService.RemoveById(queued.ExportId); internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedUndoImport>
_configWindow.UpdateLastImport(); {
private readonly ImportService _importService;
private readonly ConfigWindow _configWindow;
public Handler(ILogger<Handler> logger, ImportService importService, ConfigWindow configWindow)
: base(logger)
{
_importService = importService;
_configWindow = configWindow;
}
protected override void Run(QueuedUndoImport queued, ref bool recreateLayout)
{
recreateLayout = true;
_importService.RemoveById(queued.ExportId);
_configWindow.UpdateLastImport();
}
} }
} }
} }

View File

@ -8,97 +8,98 @@ using Pal.Client.Configuration;
using Pal.Client.Extensions; using Pal.Client.Extensions;
using Pal.Client.Properties; using Pal.Client.Properties;
namespace Pal.Client.Windows; namespace Pal.Client.Windows
internal sealed class AgreementWindow : Window, IDisposable, ILanguageChanged
{ {
private const string WindowId = "###PalPalaceAgreement"; internal sealed class AgreementWindow : Window, IDisposable, ILanguageChanged
private readonly WindowSystem _windowSystem;
private readonly ConfigurationManager _configurationManager;
private readonly IPalacePalConfiguration _configuration;
private int _choice;
public AgreementWindow(
WindowSystem windowSystem,
ConfigurationManager configurationManager,
IPalacePalConfiguration configuration)
: base(WindowId)
{ {
_windowSystem = windowSystem; private const string WindowId = "###PalPalaceAgreement";
_configurationManager = configurationManager; private readonly WindowSystem _windowSystem;
_configuration = configuration; private readonly ConfigurationManager _configurationManager;
private readonly IPalacePalConfiguration _configuration;
private int _choice;
LanguageChanged(); public AgreementWindow(
WindowSystem windowSystem,
Flags = ImGuiWindowFlags.NoCollapse; ConfigurationManager configurationManager,
Size = new Vector2(500, 500); IPalacePalConfiguration configuration)
SizeCondition = ImGuiCond.FirstUseEver; : base(WindowId)
PositionCondition = ImGuiCond.FirstUseEver;
Position = new Vector2(310, 310);
SizeConstraints = new WindowSizeConstraints
{ {
MinimumSize = new Vector2(500, 500), _windowSystem = windowSystem;
MaximumSize = new Vector2(2000, 2000), _configurationManager = configurationManager;
}; _configuration = configuration;
IsOpen = configuration.FirstUse; LanguageChanged();
_windowSystem.AddWindow(this);
}
public void Dispose() Flags = ImGuiWindowFlags.NoCollapse;
=> _windowSystem.RemoveWindow(this); Size = new Vector2(500, 500);
SizeCondition = ImGuiCond.FirstUseEver;
PositionCondition = ImGuiCond.FirstUseEver;
Position = new Vector2(310, 310);
public void LanguageChanged() SizeConstraints = new WindowSizeConstraints
=> WindowName = $"{Localization.Palace_Pal}{WindowId}"; {
MinimumSize = new Vector2(500, 500),
MaximumSize = new Vector2(2000, 2000),
};
public override void OnOpen() IsOpen = configuration.FirstUse;
{ _windowSystem.AddWindow(this);
_choice = -1;
}
public override void Draw()
{
ImGui.TextWrapped(Localization.Explanation_1);
ImGui.TextWrapped(Localization.Explanation_2);
ImGui.Spacing();
ImGui.TextWrapped(Localization.Explanation_3);
ImGui.TextWrapped(Localization.Explanation_4);
PalImGui.RadioButtonWrapped(Localization.Config_UploadMyDiscoveries_ShowOtherTraps, ref _choice,
(int)EMode.Online);
PalImGui.RadioButtonWrapped(Localization.Config_NeverUploadDiscoveries_ShowMyTraps, ref _choice,
(int)EMode.Offline);
ImGui.Separator();
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
ImGui.TextWrapped(Localization.Agreement_Warning1);
ImGui.TextWrapped(Localization.Agreement_Warning2);
ImGui.TextWrapped(Localization.Agreement_Warning3);
ImGui.PopStyleColor();
ImGui.Separator();
if (_choice == -1)
ImGui.TextDisabled(Localization.Agreement_PickOneOption);
ImGui.BeginDisabled(_choice == -1);
if (ImGui.Button(Localization.Agreement_UsingThisOnMyOwnRisk))
{
_configuration.Mode = (EMode)_choice;
_configuration.FirstUse = false;
_configurationManager.Save(_configuration);
IsOpen = false;
} }
ImGui.EndDisabled(); public void Dispose()
=> _windowSystem.RemoveWindow(this);
ImGui.Separator(); public void LanguageChanged()
=> WindowName = $"{Localization.Palace_Pal}{WindowId}";
if (ImGui.Button(Localization.Agreement_ViewPluginAndServerSourceCode)) public override void OnOpen()
GenericHelpers.ShellStart("https://git.carvel.li/liza/PalacePal"); {
_choice = -1;
}
public override void Draw()
{
ImGui.TextWrapped(Localization.Explanation_1);
ImGui.TextWrapped(Localization.Explanation_2);
ImGui.Spacing();
ImGui.TextWrapped(Localization.Explanation_3);
ImGui.TextWrapped(Localization.Explanation_4);
PalImGui.RadioButtonWrapped(Localization.Config_UploadMyDiscoveries_ShowOtherTraps, ref _choice,
(int)EMode.Online);
PalImGui.RadioButtonWrapped(Localization.Config_NeverUploadDiscoveries_ShowMyTraps, ref _choice,
(int)EMode.Offline);
ImGui.Separator();
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
ImGui.TextWrapped(Localization.Agreement_Warning1);
ImGui.TextWrapped(Localization.Agreement_Warning2);
ImGui.TextWrapped(Localization.Agreement_Warning3);
ImGui.PopStyleColor();
ImGui.Separator();
if (_choice == -1)
ImGui.TextDisabled(Localization.Agreement_PickOneOption);
ImGui.BeginDisabled(_choice == -1);
if (ImGui.Button(Localization.Agreement_UsingThisOnMyOwnRisk))
{
_configuration.Mode = (EMode)_choice;
_configuration.FirstUse = false;
_configurationManager.Save(_configuration);
IsOpen = false;
}
ImGui.EndDisabled();
ImGui.Separator();
if (ImGui.Button(Localization.Agreement_ViewPluginAndServerSourceCode))
GenericHelpers.ShellStart("https://github.com/carvelli/PalPalace");
}
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -8,118 +8,119 @@ using Pal.Client.Properties;
using Pal.Common; using Pal.Common;
using Palace; using Palace;
namespace Pal.Client.Windows; namespace Pal.Client.Windows
internal sealed class StatisticsWindow : Window, IDisposable, ILanguageChanged
{ {
private const string WindowId = "###PalacePalStats"; internal sealed class StatisticsWindow : Window, IDisposable, ILanguageChanged
private readonly WindowSystem _windowSystem;
private readonly SortedDictionary<ETerritoryType, TerritoryStatistics> _territoryStatistics = new();
public StatisticsWindow(WindowSystem windowSystem)
: base(WindowId)
{ {
_windowSystem = windowSystem; private const string WindowId = "###PalacePalStats";
private readonly WindowSystem _windowSystem;
private readonly SortedDictionary<ETerritoryType, TerritoryStatistics> _territoryStatistics = new();
LanguageChanged(); public StatisticsWindow(WindowSystem windowSystem)
: base(WindowId)
Size = new Vector2(500, 500);
SizeCondition = ImGuiCond.FirstUseEver;
Flags = ImGuiWindowFlags.AlwaysAutoResize;
foreach (ETerritoryType territory in typeof(ETerritoryType).GetEnumValues())
{ {
_territoryStatistics[territory] = new TerritoryStatistics(territory.ToString()); _windowSystem = windowSystem;
}
_windowSystem.AddWindow(this); LanguageChanged();
}
public void Dispose() Size = new Vector2(500, 500);
=> _windowSystem.RemoveWindow(this); SizeCondition = ImGuiCond.FirstUseEver;
Flags = ImGuiWindowFlags.AlwaysAutoResize;
public void LanguageChanged() foreach (ETerritoryType territory in typeof(ETerritoryType).GetEnumValues())
=> WindowName = $"{Localization.Palace_Pal} - {Localization.Statistics}{WindowId}";
public override void Draw()
{
if (ImGui.BeginTabBar("Tabs"))
{
DrawDungeonStats("Palace of the Dead", Localization.PalaceOfTheDead, ETerritoryType.Palace_1_10,
ETerritoryType.Palace_191_200);
DrawDungeonStats("Heaven on High", Localization.HeavenOnHigh, ETerritoryType.HeavenOnHigh_1_10,
ETerritoryType.HeavenOnHigh_91_100);
DrawDungeonStats("Eureka Orthos", Localization.EurekaOrthos, ETerritoryType.EurekaOrthos_1_10,
ETerritoryType.EurekaOrthos_91_100);
}
}
private void DrawDungeonStats(string id, string name, ETerritoryType minTerritory, ETerritoryType maxTerritory)
{
if (ImGui.BeginTabItem($"{name}###{id}"))
{
if (ImGui.BeginTable($"TrapHoardStatistics{id}", 4,
ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable))
{ {
ImGui.TableSetupColumn(Localization.Statistics_TerritoryId); _territoryStatistics[territory] = new TerritoryStatistics(territory.ToString());
ImGui.TableSetupColumn(Localization.Statistics_InstanceName); }
ImGui.TableSetupColumn(Localization.Statistics_Traps);
ImGui.TableSetupColumn(Localization.Statistics_HoardCoffers);
ImGui.TableHeadersRow();
foreach (var (territoryType, stats) in _territoryStatistics _windowSystem.AddWindow(this);
.Where(x => x.Key >= minTerritory && x.Key <= maxTerritory) }
.OrderBy(x => x.Key.GetOrder() ?? (int)x.Key))
public void Dispose()
=> _windowSystem.RemoveWindow(this);
public void LanguageChanged()
=> WindowName = $"{Localization.Palace_Pal} - {Localization.Statistics}{WindowId}";
public override void Draw()
{
if (ImGui.BeginTabBar("Tabs"))
{
DrawDungeonStats("Palace of the Dead", Localization.PalaceOfTheDead, ETerritoryType.Palace_1_10,
ETerritoryType.Palace_191_200);
DrawDungeonStats("Heaven on High", Localization.HeavenOnHigh, ETerritoryType.HeavenOnHigh_1_10,
ETerritoryType.HeavenOnHigh_91_100);
DrawDungeonStats("Eureka Orthos", Localization.EurekaOrthos, ETerritoryType.EurekaOrthos_1_10,
ETerritoryType.EurekaOrthos_91_100);
}
}
private void DrawDungeonStats(string id, string name, ETerritoryType minTerritory, ETerritoryType maxTerritory)
{
if (ImGui.BeginTabItem($"{name}###{id}"))
{
if (ImGui.BeginTable($"TrapHoardStatistics{id}", 4,
ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable))
{ {
ImGui.TableNextRow(); ImGui.TableSetupColumn(Localization.Statistics_TerritoryId);
if (ImGui.TableNextColumn()) ImGui.TableSetupColumn(Localization.Statistics_InstanceName);
ImGui.Text($"{(uint)territoryType}"); ImGui.TableSetupColumn(Localization.Statistics_Traps);
ImGui.TableSetupColumn(Localization.Statistics_HoardCoffers);
ImGui.TableHeadersRow();
if (ImGui.TableNextColumn()) foreach (var (territoryType, stats) in _territoryStatistics
ImGui.Text(stats.TerritoryName); .Where(x => x.Key >= minTerritory && x.Key <= maxTerritory)
.OrderBy(x => x.Key.GetOrder() ?? (int)x.Key))
{
ImGui.TableNextRow();
if (ImGui.TableNextColumn())
ImGui.Text($"{(uint)territoryType}");
if (ImGui.TableNextColumn()) if (ImGui.TableNextColumn())
ImGui.Text(stats.TrapCount?.ToString() ?? "-"); ImGui.Text(stats.TerritoryName);
if (ImGui.TableNextColumn()) if (ImGui.TableNextColumn())
ImGui.Text(stats.HoardCofferCount?.ToString() ?? "-"); ImGui.Text(stats.TrapCount?.ToString() ?? "-");
if (ImGui.TableNextColumn())
ImGui.Text(stats.HoardCofferCount?.ToString() ?? "-");
}
ImGui.EndTable();
} }
ImGui.EndTable(); ImGui.EndTabItem();
} }
ImGui.EndTabItem();
}
}
internal void SetFloorData(IEnumerable<FloorStatistics> floorStatistics)
{
foreach (var territoryStatistics in _territoryStatistics.Values)
{
territoryStatistics.TrapCount = null;
territoryStatistics.HoardCofferCount = null;
} }
foreach (var floor in floorStatistics) internal void SetFloorData(IEnumerable<FloorStatistics> floorStatistics)
{ {
if (_territoryStatistics.TryGetValue((ETerritoryType)floor.TerritoryType, foreach (var territoryStatistics in _territoryStatistics.Values)
out TerritoryStatistics? territoryStatistics))
{ {
territoryStatistics.TrapCount = floor.TrapCount; territoryStatistics.TrapCount = null;
territoryStatistics.HoardCofferCount = floor.HoardCount; territoryStatistics.HoardCofferCount = null;
}
foreach (var floor in floorStatistics)
{
if (_territoryStatistics.TryGetValue((ETerritoryType)floor.TerritoryType,
out TerritoryStatistics? territoryStatistics))
{
territoryStatistics.TrapCount = floor.TrapCount;
territoryStatistics.HoardCofferCount = floor.HoardCount;
}
} }
} }
}
private sealed class TerritoryStatistics private sealed class TerritoryStatistics
{
public string TerritoryName { get; }
public uint? TrapCount { get; set; }
public uint? HoardCofferCount { get; set; }
public TerritoryStatistics(string territoryName)
{ {
TerritoryName = territoryName; public string TerritoryName { get; }
public uint? TrapCount { get; set; }
public uint? HoardCofferCount { get; set; }
public TerritoryStatistics(string territoryName)
{
TerritoryName = territoryName;
}
} }
} }
} }

View File

@ -1,302 +1,280 @@
{ {
"version": 1, "version": 1,
"dependencies": { "dependencies": {
"net8.0-windows7.0": { "net7.0-windows7.0": {
"Dalamud.Extensions.MicrosoftLogging": { "Dalamud.Extensions.MicrosoftLogging": {
"type": "Direct", "type": "Direct",
"requested": "[4.0.1, )", "requested": "[1.0.0, )",
"resolved": "4.0.1", "resolved": "1.0.0",
"contentHash": "fMEL2ajtF/30SBBku7vMyG0yye5eHN/A9fgT//1CEjUth/Wz2CYco5Ehye21T8KN1IuAPwoqJuu49rB71j+8ug==", "contentHash": "nPjMrT9n9GJ+TYF1lyVhlvhmFyN4ajMX2ccclgyMc8MNpOGZwxrJ4VEtrUUk7UkuX2wAhtnNsjrcf5sER3/CbA==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Logging": "8.0.0" "Microsoft.Extensions.Logging": "7.0.0"
} }
}, },
"DalamudPackager": { "DalamudPackager": {
"type": "Direct", "type": "Direct",
"requested": "[2.1.13, )", "requested": "[2.1.11, )",
"resolved": "2.1.13", "resolved": "2.1.11",
"contentHash": "rMN1omGe8536f4xLMvx9NwfvpAc9YFFfeXJ1t4P4PE6Gu8WCIoFliR1sh07hM+bfODmesk/dvMbji7vNI+B/pQ==" "contentHash": "9qlAWoRRTiL/geAvuwR/g6Bcbrd/bJJgVnB/RurBiyKs6srsP0bvpoo8IK+Eg8EA6jWeM6/YJWs66w4FIAzqPw=="
}, },
"DotNet.ReproducibleBuilds": { "GitInfo": {
"type": "Direct", "type": "Direct",
"requested": "[1.1.1, )", "requested": "[2.3.0, )",
"resolved": "1.1.1", "resolved": "2.3.0",
"contentHash": "+H2t/t34h6mhEoUvHi8yGXyuZ2GjSovcGYehJrS2MDm2XgmPfZL2Sdxg+uL2lKgZ4M6tTwKHIlxOob2bgh0NRQ==", "contentHash": "LdnsKNdwQvdDvpPYQuoGjXML75dY7NybKRe+qlkPPQaTY4dE5Fy8VCrD8YBhXO0fH/5xnmvKeSq4yztzg5KY0Q=="
"dependencies": {
"Microsoft.SourceLink.AzureRepos.Git": "1.1.1",
"Microsoft.SourceLink.Bitbucket.Git": "1.1.1",
"Microsoft.SourceLink.GitHub": "1.1.1",
"Microsoft.SourceLink.GitLab": "1.1.1"
}
}, },
"Google.Protobuf": { "Google.Protobuf": {
"type": "Direct", "type": "Direct",
"requested": "[3.27.2, )", "requested": "[3.22.1, )",
"resolved": "3.27.2", "resolved": "3.22.1",
"contentHash": "0wdgA3LO9mBS477jieBFs4pU1sWhVtwv/P+i9nAEiFDQyUA7PPHDBbJL1CeqYtV18jLiq9og4n7wSVCO171OBg==" "contentHash": "Ul4gVJWLya83Z8/n3+O4QKhD8ukCCwNLDyoWpUdJSnmzxRe8o3pWiuCzzvN2z/LVH60nozlKpTzhJo3ctI+G4Q=="
}, },
"Grpc.Net.Client": { "Grpc.Net.Client": {
"type": "Direct", "type": "Direct",
"requested": "[2.63.0, )", "requested": "[2.52.0, )",
"resolved": "2.63.0", "resolved": "2.52.0",
"contentHash": "847zG24daOP1242OpbnjhbKtplH/EfV/76QReQA3cbS5SL78uIXsWMe9IN9JlIb4+kT3eE4fjMCXTn8BAQ91Ng==", "contentHash": "hWVH9g/Nnjz40ni//2S8UIOyEmhueQREoZIkD0zKHEPqLxXcNlbp4eebXIOicZtkwDSx0TFz9NpkbecEDn6rBw==",
"dependencies": { "dependencies": {
"Grpc.Net.Common": "2.63.0", "Grpc.Net.Common": "2.52.0",
"Microsoft.Extensions.Logging.Abstractions": "6.0.0" "Microsoft.Extensions.Logging.Abstractions": "3.0.3"
} }
}, },
"Grpc.Tools": { "Grpc.Tools": {
"type": "Direct", "type": "Direct",
"requested": "[2.64.0, )", "requested": "[2.53.0, )",
"resolved": "2.64.0", "resolved": "2.53.0",
"contentHash": "W5RrhDFHUhioASktxfuDs5fTjWUxwegljZAig9zFL8nWNskeyQA6OXN2choWKYxGrljer25VqCJCMbWz7XHvqg==" "contentHash": "vm8iRSAF/4PN9g555iYZwhCQptSE4cZ8xk5W1TQ+JcHwaHSrBhD+P6H4l0+SqqfzuX7sGpjjOMQJXHSyrERTgw=="
}, },
"Microsoft.EntityFrameworkCore.Sqlite": { "Microsoft.EntityFrameworkCore.Sqlite": {
"type": "Direct", "type": "Direct",
"requested": "[8.0.6, )", "requested": "[7.0.4, )",
"resolved": "8.0.6", "resolved": "7.0.4",
"contentHash": "nC4cZN4zReTb22qd9WDU0eDmlXvkyf2g2pqQ3VIHJbkpJcdWSY/PDgwGpbpShsVcAjXbkjGiUcv9aGwa61xQPw==", "contentHash": "d1cIR5upwzTZmzycqWEoxfso5b3qD0G43IeECtfeMSPoG8JD4OJHHtbun0wS9RzwAORMa/4Zb3vuogTYY3mtaQ==",
"dependencies": { "dependencies": {
"Microsoft.EntityFrameworkCore.Sqlite.Core": "8.0.6", "Microsoft.EntityFrameworkCore.Sqlite.Core": "7.0.4",
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.6" "SQLitePCLRaw.bundle_e_sqlite3": "2.1.4"
}
},
"Microsoft.EntityFrameworkCore.Tools": {
"type": "Direct",
"requested": "[7.0.4, )",
"resolved": "7.0.4",
"contentHash": "58hDB+ENGisuSjJBl1RBHL9qzFJTukFSQFl/wCU8/3ApcOH/rPrRG4PWThiJTmfHRmh8H8HExdYbtkv7wa7BLg==",
"dependencies": {
"Microsoft.EntityFrameworkCore.Design": "7.0.4"
} }
}, },
"Microsoft.Extensions.Logging": { "Microsoft.Extensions.Logging": {
"type": "Direct", "type": "Direct",
"requested": "[8.0.0, )", "requested": "[7.0.0, )",
"resolved": "8.0.0", "resolved": "7.0.0",
"contentHash": "tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==", "contentHash": "Nw2muoNrOG5U5qa2ZekXwudUn2BJcD41e65zwmDHb1fQegTX66UokLWZkJRpqSSHXDOWZ5V0iqhbxOEky91atA==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.DependencyInjection": "7.0.0",
"Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0",
"Microsoft.Extensions.Options": "8.0.0" "Microsoft.Extensions.Logging.Abstractions": "7.0.0",
} "Microsoft.Extensions.Options": "7.0.0"
},
"Microsoft.SourceLink.Gitea": {
"type": "Direct",
"requested": "[8.0.0, )",
"resolved": "8.0.0",
"contentHash": "KOBodmDnlWGIqZt2hT47Q69TIoGhIApDVLCyyj9TT5ct8ju16AbHYcB4XeknoHX562wO1pMS/1DfBIZK+V+sxg==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "8.0.0",
"Microsoft.SourceLink.Common": "8.0.0"
} }
}, },
"System.Security.Cryptography.ProtectedData": { "System.Security.Cryptography.ProtectedData": {
"type": "Direct", "type": "Direct",
"requested": "[8.0.0, )", "requested": "[7.0.1, )",
"resolved": "8.0.0", "resolved": "7.0.1",
"contentHash": "+TUFINV2q2ifyXauQXRwy4CiBhqvDEDZeVJU7qfxya4aRYOKzVBpN+4acx25VcPB9ywUN6C0n8drWl110PhZEg==" "contentHash": "3evI3sBfKqwYSwuBcYgShbmEgtXcg8N5Qu+jExLdkBXPty2yGDXq5m1/4sx9Exb8dqdeMPUs/d9DQ0wy/9Adwg=="
}, },
"Grpc.Core.Api": { "Grpc.Core.Api": {
"type": "Transitive", "type": "Transitive",
"resolved": "2.63.0", "resolved": "2.52.0",
"contentHash": "t3+/MF8AxIqKq5UmPB9EWAnM9C/+lXOB8TRFfeVMDntf6dekfJmjpKDebaT4t2bbuwVwwvthxxox9BuGr59kYA==" "contentHash": "SQiPyBczG4vKPmI6Fd+O58GcxxDSFr6nfRAJuBDUNj+PgdokhjWJvZE/La1c09AkL2FVm/jrDloG89nkzmVF7A==",
"dependencies": {
"System.Memory": "4.5.3"
}
}, },
"Grpc.Net.Common": { "Grpc.Net.Common": {
"type": "Transitive", "type": "Transitive",
"resolved": "2.63.0", "resolved": "2.52.0",
"contentHash": "RLt6p31ZMsXRcHNeu1dQuIFLYZvnwP6LUzoDPlV3KoR4w9btmwrXIvz9Jbp1SOmxW7nXw9zShAeIt5LsqFAx5w==", "contentHash": "di9qzpdx525IxumZdYmu6sG2y/gXJyYeZ1ruFUzB9BJ1nj4kU1/dTAioNCMt1VLRvNVDqh8S8B1oBdKhHJ4xRg==",
"dependencies": { "dependencies": {
"Grpc.Core.Api": "2.63.0" "Grpc.Core.Api": "2.52.0"
} }
}, },
"Microsoft.Build.Tasks.Git": { "Humanizer.Core": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.0", "resolved": "2.14.1",
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ==" "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw=="
}, },
"Microsoft.Data.Sqlite.Core": { "Microsoft.Data.Sqlite.Core": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.6", "resolved": "7.0.4",
"contentHash": "umhZ0ZF2RI81rGFTnYmCxI+Euj4Aqe/6Y4+8CxN9OVJNGDNIqB5laJ3wxQTU8zXCcm2k9F7FL+/6RVoOT4z1Fw==", "contentHash": "AUBM1KZ7EvmkYhC/ECXL4cjx+q55DJ3lmSf0NwAyRNArubNPRdroGono5uN6aW7Kqp+IUZwEK0Ywd1Gh7FDM2A==",
"dependencies": { "dependencies": {
"SQLitePCLRaw.core": "2.1.6" "SQLitePCLRaw.core": "2.1.4"
} }
}, },
"Microsoft.EntityFrameworkCore": { "Microsoft.EntityFrameworkCore": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.6", "resolved": "7.0.4",
"contentHash": "Ms5e5QuBAjVIuQsGumeLvkgMiOpnj6wxPvwBIoe1NfTkseWK4NZYztnhgDlpkCPkrUmJEXLv69kl349Ours30Q==", "contentHash": "eNcsY3rft5ERJJcen80Jyg57EScjWZmvhwmFLYXmEOTdVqHG+wQZiMOXnO1b5RH3u2qTQq+Tpci7KGfLAG5Gtg==",
"dependencies": { "dependencies": {
"Microsoft.EntityFrameworkCore.Abstractions": "8.0.6", "Microsoft.EntityFrameworkCore.Abstractions": "7.0.4",
"Microsoft.EntityFrameworkCore.Analyzers": "8.0.6", "Microsoft.EntityFrameworkCore.Analyzers": "7.0.4",
"Microsoft.Extensions.Caching.Memory": "8.0.0", "Microsoft.Extensions.Caching.Memory": "7.0.0",
"Microsoft.Extensions.Logging": "8.0.0" "Microsoft.Extensions.DependencyInjection": "7.0.0",
"Microsoft.Extensions.Logging": "7.0.0"
} }
}, },
"Microsoft.EntityFrameworkCore.Abstractions": { "Microsoft.EntityFrameworkCore.Abstractions": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.6", "resolved": "7.0.4",
"contentHash": "X7wSSBNFRuN8j8M9HDYG7rPpEeyhY+PdJZR9rftmgvsZH0eK5+bZ3b3As8iO4rLEpjsBzDnrgSIY6q2F3HQatw==" "contentHash": "6GbYvs4L5oFpYpMzwF05kdDgvX09UmMX7MpDtDlGI5ymijFwquwv+yvdijbtodOuu0yLUpc4n71x6eBdJ8v1xQ=="
}, },
"Microsoft.EntityFrameworkCore.Analyzers": { "Microsoft.EntityFrameworkCore.Analyzers": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.6", "resolved": "7.0.4",
"contentHash": "fDNtuQ4lAaPaCOlsrwUck/GvnF4QLeDpMmE1L5QtxZpMSmWfnL2/vk8sDL9OVTWcfprooI9V5MNpIx3/Tq5ehg==" "contentHash": "YRD4bViuaEPEsaBIL52DzXGzLCt3jYoE3wztYEW1QZYDl89hQ+ca0nvBO2mnMHmCXpU/2wlErrUyDp4x5B/3mg=="
},
"Microsoft.EntityFrameworkCore.Design": {
"type": "Transitive",
"resolved": "7.0.4",
"contentHash": "LI/ML3w17ap5IUmEKOPVnGJYi/XSDJW3Rf42utNF0e1tidmKtSkjwoTqIKLt2hE+jQJrlzeaqu5YiqdoFWVuZw==",
"dependencies": {
"Humanizer.Core": "2.14.1",
"Microsoft.EntityFrameworkCore.Relational": "7.0.4",
"Microsoft.Extensions.DependencyModel": "7.0.0",
"Mono.TextTemplating": "2.2.1"
}
}, },
"Microsoft.EntityFrameworkCore.Relational": { "Microsoft.EntityFrameworkCore.Relational": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.6", "resolved": "7.0.4",
"contentHash": "chhfmLusCGLGvNYtvMji6KGQlduPDnJsStG/LjS8qJhFWJDDzTZpSr2LHowewcxMrMo/Axc6Jwe+WwSi/vlkTg==", "contentHash": "L41+VonK6L0IurFHopoe5yY+m3MD26OMocKLPPR/XKxnazzZUcGPz0IGJpVnwpZyKVPfEIAnD5vmm60meYr1NA==",
"dependencies": { "dependencies": {
"Microsoft.EntityFrameworkCore": "8.0.6", "Microsoft.EntityFrameworkCore": "7.0.4",
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0" "Microsoft.Extensions.Configuration.Abstractions": "7.0.0"
} }
}, },
"Microsoft.EntityFrameworkCore.Sqlite.Core": { "Microsoft.EntityFrameworkCore.Sqlite.Core": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.6", "resolved": "7.0.4",
"contentHash": "87xfPtqSouxWWdynYZv/rubd0rOUeiN9+XeoMWQzpZm/5svH1TuvzFODGIY0zKuXS18NiOFyHl9N6///eaEs/Q==", "contentHash": "FeuV57+U4A4DO018Jy5Wkv0uYNZhyFVUUdwyVYz8TMghsZAj+3i+fOeFtD/jAWWMzDOFOF7eMni3YqLA+ufu9Q==",
"dependencies": { "dependencies": {
"Microsoft.Data.Sqlite.Core": "8.0.6", "Microsoft.Data.Sqlite.Core": "7.0.4",
"Microsoft.EntityFrameworkCore.Relational": "8.0.6", "Microsoft.EntityFrameworkCore.Relational": "7.0.4",
"Microsoft.Extensions.DependencyModel": "8.0.0" "Microsoft.Extensions.DependencyModel": "7.0.0"
} }
}, },
"Microsoft.Extensions.Caching.Abstractions": { "Microsoft.Extensions.Caching.Abstractions": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.0", "resolved": "7.0.0",
"contentHash": "3KuSxeHoNYdxVYfg2IRZCThcrlJ1XJqIXkAWikCsbm5C/bCjv7G0WoKDyuR98Q+T607QT2Zl5GsbGRkENcV2yQ==", "contentHash": "IeimUd0TNbhB4ded3AbgBLQv2SnsiVugDyGV1MvspQFVlA07nDC7Zul7kcwH5jWN3JiTcp/ySE83AIJo8yfKjg==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Primitives": "8.0.0" "Microsoft.Extensions.Primitives": "7.0.0"
} }
}, },
"Microsoft.Extensions.Caching.Memory": { "Microsoft.Extensions.Caching.Memory": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.0", "resolved": "7.0.0",
"contentHash": "7pqivmrZDzo1ADPkRwjy+8jtRKWRCPag9qPI+p7sgu7Q4QreWhcvbiWXsbhP+yY8XSiDvZpu2/LWdBv7PnmOpQ==", "contentHash": "xpidBs2KCE2gw1JrD0quHE72kvCaI3xFql5/Peb2GRtUuZX+dYPoK/NTdVMiM67Svym0M0Df9A3xyU0FbMQhHw==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Caching.Abstractions": "8.0.0", "Microsoft.Extensions.Caching.Abstractions": "7.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0",
"Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "7.0.0",
"Microsoft.Extensions.Options": "8.0.0", "Microsoft.Extensions.Options": "7.0.0",
"Microsoft.Extensions.Primitives": "8.0.0" "Microsoft.Extensions.Primitives": "7.0.0"
} }
}, },
"Microsoft.Extensions.Configuration.Abstractions": { "Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.0", "resolved": "7.0.0",
"contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==", "contentHash": "f34u2eaqIjNO9YLHBz8rozVZ+TcFiFs0F3r7nUJd7FRkVSxk8u4OpoK226mi49MwexHOR2ibP9MFvRUaLilcQQ==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Primitives": "8.0.0" "Microsoft.Extensions.Primitives": "7.0.0"
} }
}, },
"Microsoft.Extensions.DependencyInjection": { "Microsoft.Extensions.DependencyInjection": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.0", "resolved": "7.0.0",
"contentHash": "V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==", "contentHash": "elNeOmkeX3eDVG6pYVeV82p29hr+UKDaBhrZyWvWLw/EVZSYEkZlQdkp0V39k/Xehs2Qa0mvoCvkVj3eQxNQ1Q==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0"
} }
}, },
"Microsoft.Extensions.DependencyInjection.Abstractions": { "Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.0", "resolved": "7.0.0",
"contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" "contentHash": "h3j/QfmFN4S0w4C2A6X7arXij/M/OVw3uQHSOFxnND4DyAzO1F9eMX7Eti7lU/OkSthEE0WzRsfT/Dmx86jzCw=="
}, },
"Microsoft.Extensions.DependencyModel": { "Microsoft.Extensions.DependencyModel": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.0", "resolved": "7.0.0",
"contentHash": "NSmDw3K0ozNDgShSIpsZcbFIzBX4w28nDag+TfaQujkXGazBm+lid5onlWoCBy4VsLxqnnKjEBbGSJVWJMf43g==", "contentHash": "oONNYd71J3LzkWc4fUHl3SvMfiQMYUCo/mDHDEu76hYYxdhdrPYv6fvGv9nnKVyhE9P0h20AU8RZB5OOWQcAXg==",
"dependencies": { "dependencies": {
"System.Text.Encodings.Web": "8.0.0", "System.Text.Encodings.Web": "7.0.0",
"System.Text.Json": "8.0.0" "System.Text.Json": "7.0.0"
} }
}, },
"Microsoft.Extensions.Logging.Abstractions": { "Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.0", "resolved": "7.0.0",
"contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", "contentHash": "kmn78+LPVMOWeITUjIlfxUPDsI0R6G0RkeAMBmQxAJ7vBJn4q2dTva7pWi65ceN5vPGjJ9q/Uae2WKgvfktJAw=="
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
}
}, },
"Microsoft.Extensions.Options": { "Microsoft.Extensions.Options": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.0", "resolved": "7.0.0",
"contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==", "contentHash": "lP1yBnTTU42cKpMozuafbvNtQ7QcBjr/CcK3bYOGEMH55Fjt+iecXjT6chR7vbgCMqy3PG3aNQSZgo/EuY/9qQ==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0",
"Microsoft.Extensions.Primitives": "8.0.0" "Microsoft.Extensions.Primitives": "7.0.0"
} }
}, },
"Microsoft.Extensions.Primitives": { "Microsoft.Extensions.Primitives": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.0", "resolved": "7.0.0",
"contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" "contentHash": "um1KU5kxcRp3CNuI8o/GrZtD4AIOXDk+RLsytjZ9QPok3ttLUelLKpilVPuaFT3TFjOhSibUAso0odbOaCDj3Q=="
}, },
"Microsoft.SourceLink.AzureRepos.Git": { "Mono.TextTemplating": {
"type": "Transitive", "type": "Transitive",
"resolved": "1.1.1", "resolved": "2.2.1",
"contentHash": "qB5urvw9LO2bG3eVAkuL+2ughxz2rR7aYgm2iyrB8Rlk9cp2ndvGRCvehk3rNIhRuNtQaeKwctOl1KvWiklv5w==", "contentHash": "KZYeKBET/2Z0gY1WlTAK7+RHTl7GSbtvTLDXEZZojUdAPqpQNDL6tHv7VUpqfX5VEOh+uRGKaZXkuD253nEOBQ==",
"dependencies": { "dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1", "System.CodeDom": "4.4.0"
"Microsoft.SourceLink.Common": "1.1.1"
}
},
"Microsoft.SourceLink.Bitbucket.Git": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "cDzxXwlyWpLWaH0em4Idj0H3AmVo3L/6xRXKssYemx+7W52iNskj/SQ4FOmfCb8YQt39otTDNMveCZzYtMoucQ==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
}
},
"Microsoft.SourceLink.Common": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
},
"Microsoft.SourceLink.GitHub": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "IaJGnOv/M7UQjRJks7B6p7pbPnOwisYGOIzqCz5ilGFTApZ3ktOR+6zJ12ZRPInulBmdAf1SrGdDG2MU8g6XTw==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
}
},
"Microsoft.SourceLink.GitLab": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "tvsg47DDLqqedlPeYVE2lmiTpND8F0hkrealQ5hYltSmvruy/Gr5nHAKSsjyw5L3NeM/HLMI5ORv7on/M4qyZw==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
} }
}, },
"SQLitePCLRaw.bundle_e_sqlite3": { "SQLitePCLRaw.bundle_e_sqlite3": {
"type": "Transitive", "type": "Transitive",
"resolved": "2.1.6", "resolved": "2.1.4",
"contentHash": "BmAf6XWt4TqtowmiWe4/5rRot6GerAeklmOPfviOvwLoF5WwgxcJHAxZtySuyW9r9w+HLILnm8VfJFLCUJYW8A==", "contentHash": "EWI1olKDjFEBMJu0+3wuxwziIAdWDVMYLhuZ3Qs84rrz+DHwD00RzWPZCa+bLnHCf3oJwuFZIRsHT5p236QXww==",
"dependencies": { "dependencies": {
"SQLitePCLRaw.lib.e_sqlite3": "2.1.6", "SQLitePCLRaw.lib.e_sqlite3": "2.1.4",
"SQLitePCLRaw.provider.e_sqlite3": "2.1.6" "SQLitePCLRaw.provider.e_sqlite3": "2.1.4"
} }
}, },
"SQLitePCLRaw.core": { "SQLitePCLRaw.core": {
"type": "Transitive", "type": "Transitive",
"resolved": "2.1.6", "resolved": "2.1.4",
"contentHash": "wO6v9GeMx9CUngAet8hbO7xdm+M42p1XeJq47ogyRoYSvNSp0NGLI+MgC0bhrMk9C17MTVFlLiN6ylyExLCc5w==", "contentHash": "inBjvSHo9UDKneGNzfUfDjK08JzlcIhn1+SP5Y3m6cgXpCxXKCJDy6Mka7LpgSV+UZmKSnC8rTwB0SQ0xKu5pA==",
"dependencies": { "dependencies": {
"System.Memory": "4.5.3" "System.Memory": "4.5.3"
} }
}, },
"SQLitePCLRaw.lib.e_sqlite3": { "SQLitePCLRaw.lib.e_sqlite3": {
"type": "Transitive", "type": "Transitive",
"resolved": "2.1.6", "resolved": "2.1.4",
"contentHash": "2ObJJLkIUIxRpOUlZNGuD4rICpBnrBR5anjyfUFQep4hMOIeqW+XGQYzrNmHSVz5xSWZ3klSbh7sFR6UyDj68Q==" "contentHash": "2C9Q9eX7CPLveJA0rIhf9RXAvu+7nWZu1A2MdG6SD/NOu26TakGgL1nsbc0JAspGijFOo3HoN79xrx8a368fBg=="
}, },
"SQLitePCLRaw.provider.e_sqlite3": { "SQLitePCLRaw.provider.e_sqlite3": {
"type": "Transitive", "type": "Transitive",
"resolved": "2.1.6", "resolved": "2.1.4",
"contentHash": "PQ2Oq3yepLY4P7ll145P3xtx2bX8xF4PzaKPRpw9jZlKvfe4LE/saAV82inND9usn1XRpmxXk7Lal3MTI+6CNg==", "contentHash": "CSlb5dUp1FMIkez9Iv5EXzpeq7rHryVNqwJMWnpq87j9zWZexaEMdisDktMsnnrzKM6ahNrsTkjqNodTBPBxtQ==",
"dependencies": { "dependencies": {
"SQLitePCLRaw.core": "2.1.6" "SQLitePCLRaw.core": "2.1.4"
} }
}, },
"System.CodeDom": {
"type": "Transitive",
"resolved": "4.4.0",
"contentHash": "2sCCb7doXEwtYAbqzbF/8UAeDRMNmPaQbU2q50Psg1J9KzumyVVCgKQY8s53WIPTufNT0DpSe9QRvVjOzfDWBA=="
},
"System.Memory": { "System.Memory": {
"type": "Transitive", "type": "Transitive",
"resolved": "4.5.3", "resolved": "4.5.3",
@ -304,40 +282,34 @@
}, },
"System.Text.Encodings.Web": { "System.Text.Encodings.Web": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.0", "resolved": "7.0.0",
"contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==" "contentHash": "OP6umVGxc0Z0MvZQBVigj4/U31Pw72ITihDWP9WiWDm+q5aoe0GaJivsfYGq53o6dxH7DcXWiCTl7+0o2CGdmg=="
}, },
"System.Text.Json": { "System.Text.Json": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.0", "resolved": "7.0.0",
"contentHash": "OdrZO2WjkiEG6ajEFRABTRCi/wuXQPxeV6g8xvUJqdxMvvuCCEk86zPla8UiIQJz3durtUEbNyY/3lIhS0yZvQ==", "contentHash": "DaGSsVqKsn/ia6RG8frjwmJonfos0srquhw09TlT8KRw5I43E+4gs+/bZj4K0vShJ5H9imCuXupb4RmS+dBy3w==",
"dependencies": { "dependencies": {
"System.Text.Encodings.Web": "8.0.0" "System.Text.Encodings.Web": "7.0.0"
} }
}, },
"ecommons": { "ecommons": {
"type": "Project" "type": "Project"
}, },
"llib": {
"type": "Project",
"dependencies": {
"DalamudPackager": "[2.1.13, )"
}
},
"pal.common": { "pal.common": {
"type": "Project" "type": "Project"
} }
}, },
"net8.0-windows7.0/win-x64": { "net7.0-windows7.0/win-x64": {
"SQLitePCLRaw.lib.e_sqlite3": { "SQLitePCLRaw.lib.e_sqlite3": {
"type": "Transitive", "type": "Transitive",
"resolved": "2.1.6", "resolved": "2.1.4",
"contentHash": "2ObJJLkIUIxRpOUlZNGuD4rICpBnrBR5anjyfUFQep4hMOIeqW+XGQYzrNmHSVz5xSWZ3klSbh7sFR6UyDj68Q==" "contentHash": "2C9Q9eX7CPLveJA0rIhf9RXAvu+7nWZu1A2MdG6SD/NOu26TakGgL1nsbc0JAspGijFOo3HoN79xrx8a368fBg=="
}, },
"System.Text.Encodings.Web": { "System.Text.Encodings.Web": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.0", "resolved": "7.0.0",
"contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==" "contentHash": "OP6umVGxc0Z0MvZQBVigj4/U31Pw72ITihDWP9WiWDm+q5aoe0GaJivsfYGq53o6dxH7DcXWiCTl7+0o2CGdmg=="
} }
} }
} }

View File

@ -1,62 +1,63 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
namespace Pal.Common; namespace Pal.Common
[SuppressMessage("ReSharper", "UnusedMember.Global")]
[SuppressMessage("ReSharper", "InconsistentNaming")]
public enum ETerritoryType : ushort
{ {
Palace_1_10 = 561, [SuppressMessage("ReSharper", "UnusedMember.Global")]
Palace_11_20, [SuppressMessage("ReSharper", "InconsistentNaming")]
Palace_21_30, public enum ETerritoryType : ushort
Palace_31_40, {
Palace_41_50, Palace_1_10 = 561,
Palace_51_60 = 593, Palace_11_20,
Palace_61_70, Palace_21_30,
Palace_71_80, Palace_31_40,
Palace_81_90, Palace_41_50,
Palace_91_100, Palace_51_60 = 593,
Palace_101_110, Palace_61_70,
Palace_111_120, Palace_71_80,
Palace_121_130, Palace_81_90,
Palace_131_140, Palace_91_100,
Palace_141_150, Palace_101_110,
Palace_151_160, Palace_111_120,
Palace_161_170, Palace_121_130,
Palace_171_180, Palace_131_140,
Palace_181_190, Palace_141_150,
Palace_191_200, Palace_151_160,
Palace_161_170,
Palace_171_180,
Palace_181_190,
Palace_191_200,
[Display(Order = 1)] [Display(Order = 1)]
HeavenOnHigh_1_10 = 770, HeavenOnHigh_1_10 = 770,
[Display(Order = 2)] [Display(Order = 2)]
HeavenOnHigh_11_20 = 771, HeavenOnHigh_11_20 = 771,
[Display(Order = 3)] [Display(Order = 3)]
HeavenOnHigh_21_30 = 772, HeavenOnHigh_21_30 = 772,
[Display(Order = 4)] [Display(Order = 4)]
HeavenOnHigh_31_40 = 782, HeavenOnHigh_31_40 = 782,
[Display(Order = 5)] [Display(Order = 5)]
HeavenOnHigh_41_50 = 773, HeavenOnHigh_41_50 = 773,
[Display(Order = 6)] [Display(Order = 6)]
HeavenOnHigh_51_60 = 783, HeavenOnHigh_51_60 = 783,
[Display(Order = 7)] [Display(Order = 7)]
HeavenOnHigh_61_70 = 774, HeavenOnHigh_61_70 = 774,
[Display(Order = 8)] [Display(Order = 8)]
HeavenOnHigh_71_80 = 784, HeavenOnHigh_71_80 = 784,
[Display(Order = 9)] [Display(Order = 9)]
HeavenOnHigh_81_90 = 775, HeavenOnHigh_81_90 = 775,
[Display(Order = 10)] [Display(Order = 10)]
HeavenOnHigh_91_100 = 785, HeavenOnHigh_91_100 = 785,
EurekaOrthos_1_10 = 1099, EurekaOrthos_1_10 = 1099,
EurekaOrthos_11_20, EurekaOrthos_11_20,
EurekaOrthos_21_30, EurekaOrthos_21_30,
EurekaOrthos_31_40, EurekaOrthos_31_40,
EurekaOrthos_41_50, EurekaOrthos_41_50,
EurekaOrthos_51_60, EurekaOrthos_51_60,
EurekaOrthos_61_70, EurekaOrthos_61_70,
EurekaOrthos_71_80, EurekaOrthos_71_80,
EurekaOrthos_81_90, EurekaOrthos_81_90,
EurekaOrthos_91_100 EurekaOrthos_91_100
}
} }

View File

@ -1,17 +1,16 @@
using System; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection; using System.Reflection;
namespace Pal.Common; namespace Pal.Common
public static class EnumExtensions
{ {
public static int? GetOrder(this Enum e) public static class EnumExtensions
{ {
Type type = e.GetType(); public static int? GetOrder(this Enum e)
MemberInfo field = type.GetMember(e.ToString()).Single(); {
DisplayAttribute? attribute = field.GetCustomAttributes(typeof(DisplayAttribute), false).Cast<DisplayAttribute>().FirstOrDefault(); Type type = e.GetType();
return attribute?.Order; MemberInfo field = type.GetMember(e.ToString()).Single();
DisplayAttribute? attribute = field.GetCustomAttributes(typeof(DisplayAttribute), false).Cast<DisplayAttribute>().FirstOrDefault();
return attribute?.Order;
}
} }
} }

View File

@ -4,9 +4,10 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Pal.Common; namespace Pal.Common
public static class ExportConfig
{ {
public static int ExportVersion => 2; public static class ExportConfig
{
public static int ExportVersion => 2;
}
} }

View File

@ -1,8 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<LangVersion>12.0</LangVersion> <LangVersion>11.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<DebugType>portable</DebugType> <DebugType>portable</DebugType>
<PathMap Condition="$(SolutionDir) != ''">$(SolutionDir)=X:\</PathMap> <PathMap Condition="$(SolutionDir) != ''">$(SolutionDir)=X:\</PathMap>

View File

@ -1,22 +1,22 @@
using System; using System.Numerics;
using System.Numerics;
namespace Pal.Common; namespace Pal.Common
public class PalaceMath
{ {
private static readonly Vector3 ScaleFactor = new(5); public class PalaceMath
public static bool IsNearlySamePosition(Vector3 a, Vector3 b)
{ {
a *= ScaleFactor; private static readonly Vector3 ScaleFactor = new(5);
b *= ScaleFactor;
return (int)a.X == (int)b.X && (int)a.Y == (int)b.Y && (int)a.Z == (int)b.Z;
}
public static int GetHashCode(Vector3 v) public static bool IsNearlySamePosition(Vector3 a, Vector3 b)
{ {
v *= ScaleFactor; a *= ScaleFactor;
return HashCode.Combine((int)v.X, (int)v.Y, (int)v.Z); b *= ScaleFactor;
return (int)a.X == (int)b.X && (int)a.Y == (int)b.Y && (int)a.Z == (int)b.Z;
}
public static int GetHashCode(Vector3 v)
{
v *= ScaleFactor;
return HashCode.Combine((int)v.X, (int)v.Y, (int)v.Z);
}
} }
} }

View File

@ -18,7 +18,6 @@ service AccountService {
} }
message CreateAccountRequest { message CreateAccountRequest {
Version version = 1;
} }
message CreateAccountReply { message CreateAccountReply {
@ -36,7 +35,6 @@ enum CreateAccountError {
message LoginRequest { message LoginRequest {
string accountId = 1; string accountId = 1;
Version version = 2;
} }
message LoginReply { message LoginReply {
@ -57,9 +55,4 @@ message VerifyRequest {
} }
message VerifyReply { message VerifyReply {
} }
message Version {
int32 major = 1;
int32 minor = 2;
}

View File

@ -1,6 +1,6 @@
{ {
"version": 1, "version": 1,
"dependencies": { "dependencies": {
"net8.0": {} "net7.0": {}
} }
} }

View File

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.22.1"/>
<PackageReference Include="Grpc.Net.Client" Version="2.52.0"/>
<PackageReference Include="Grpc.Tools" Version="2.53.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Pal.Common\Pal.Common.csproj"/>
</ItemGroup>
<ItemGroup>
<Protobuf Include="..\Pal.Common\Protos\account.proto" Link="Protos\account.proto" GrpcServices="Client" Access="Internal"/>
<Protobuf Include="..\Pal.Common\Protos\palace.proto" Link="Protos\palace.proto" GrpcServices="Client" Access="Internal"/>
</ItemGroup>
</Project>

View File

@ -0,0 +1,34 @@
using Grpc.Core;
using Grpc.Net.Client;
using Palace;
namespace Pal.StandaloneClient
{
internal class Program
{
private const string remoteUrl = "http://localhost:5415";
private static readonly Guid accountId = Guid.Parse("ce7b109a-5e29-4b63-ab3e-b6f89eb5e19e"); // manually created account id
static async Task Main(string[] args)
{
GrpcChannel channel = GrpcChannel.ForAddress(remoteUrl);
var accountClient = new Account.AccountService.AccountServiceClient(channel);
var loginReply = await accountClient.LoginAsync(new Account.LoginRequest
{
AccountId = accountId.ToString()
});
if (loginReply == null || !loginReply.Success)
throw new Exception($"Login failed: {loginReply?.Error}");
var headers = new Metadata()
{
{ "Authorization", $"Bearer {loginReply.AuthToken}" }
};
var palaceClient = new Palace.PalaceService.PalaceServiceClient(channel);
var markAsSeenRequest = new MarkObjectsSeenRequest { TerritoryType = 772 };
markAsSeenRequest.NetworkIds.Add("0c635960-0e2e-4ec6-9fb5-443d0e7a3315"); // this is an already existing entry
var markAsSeenReply = await palaceClient.MarkObjectsSeenAsync(markAsSeenRequest, headers: headers);
Console.WriteLine($"Reply = {markAsSeenReply.Success}");
}
}
}

View File

@ -0,0 +1,6 @@
# Palace Pal - Test Client
This is a very simple prototype for a local test client, which is more helpful
in troubleshooting some specific edge cases with the server implementation.
This should eventually be refactored into a test suite.

View File

@ -0,0 +1,58 @@
{
"version": 1,
"dependencies": {
"net7.0": {
"Google.Protobuf": {
"type": "Direct",
"requested": "[3.22.1, )",
"resolved": "3.22.1",
"contentHash": "Ul4gVJWLya83Z8/n3+O4QKhD8ukCCwNLDyoWpUdJSnmzxRe8o3pWiuCzzvN2z/LVH60nozlKpTzhJo3ctI+G4Q=="
},
"Grpc.Net.Client": {
"type": "Direct",
"requested": "[2.52.0, )",
"resolved": "2.52.0",
"contentHash": "hWVH9g/Nnjz40ni//2S8UIOyEmhueQREoZIkD0zKHEPqLxXcNlbp4eebXIOicZtkwDSx0TFz9NpkbecEDn6rBw==",
"dependencies": {
"Grpc.Net.Common": "2.52.0",
"Microsoft.Extensions.Logging.Abstractions": "3.0.3"
}
},
"Grpc.Tools": {
"type": "Direct",
"requested": "[2.53.0, )",
"resolved": "2.53.0",
"contentHash": "vm8iRSAF/4PN9g555iYZwhCQptSE4cZ8xk5W1TQ+JcHwaHSrBhD+P6H4l0+SqqfzuX7sGpjjOMQJXHSyrERTgw=="
},
"Grpc.Core.Api": {
"type": "Transitive",
"resolved": "2.52.0",
"contentHash": "SQiPyBczG4vKPmI6Fd+O58GcxxDSFr6nfRAJuBDUNj+PgdokhjWJvZE/La1c09AkL2FVm/jrDloG89nkzmVF7A==",
"dependencies": {
"System.Memory": "4.5.3"
}
},
"Grpc.Net.Common": {
"type": "Transitive",
"resolved": "2.52.0",
"contentHash": "di9qzpdx525IxumZdYmu6sG2y/gXJyYeZ1ruFUzB9BJ1nj4kU1/dTAioNCMt1VLRvNVDqh8S8B1oBdKhHJ4xRg==",
"dependencies": {
"Grpc.Core.Api": "2.52.0"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
"resolved": "3.0.3",
"contentHash": "m2Jyi/MEn043WMI1I6J1ALuCThktZ93rd7eqzYeLmMcA0bdZC+TBVl0LuEbEWM01dWeeBjOoagjNwQTzOi2r6A=="
},
"System.Memory": {
"type": "Transitive",
"resolved": "4.5.3",
"contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA=="
},
"pal.common": {
"type": "Project"
}
}
}
}

22
Pal.sln
View File

@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.3.32929.385 VisualStudioVersion = 17.3.32929.385
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pal.Server", "Server\Server\Pal.Server.csproj", "{AB3E2849-DB06-46F6-8457-9AC1096B4125}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pal.Server", "Pal.Server\Pal.Server.csproj", "{AB3E2849-DB06-46F6-8457-9AC1096B4125}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pal.Client", "Pal.Client\Pal.Client.csproj", "{7F1985B3-D376-4A91-BC9B-46C2A860F9EF}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pal.Client", "Pal.Client\Pal.Client.csproj", "{7F1985B3-D376-4A91-BC9B-46C2A860F9EF}"
EndProject EndProject
@ -25,9 +25,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "github-workflows", "github-
.github\workflows\upload-crowdin.yml = .github\workflows\upload-crowdin.yml .github\workflows\upload-crowdin.yml = .github\workflows\upload-crowdin.yml
EndProjectSection EndProjectSection
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pal.Server.Tests", "Server\Tests\Pal.Server.Tests.csproj", "{AEC052FA-F178-492C-9A09-ED28DBE1EF5E}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pal.StandaloneClient", "Pal.StandaloneClient\Pal.StandaloneClient.csproj", "{EDC1C408-D832-4C09-97A2-61B223A84166}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LLib", "vendor\LLib\LLib.csproj", "{B1321FD5-7BBF-4C9D-83C1-F8D7C394F32A}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pal.Server.Tests", "Pal.Server.Tests\Pal.Server.Tests.csproj", "{AEC052FA-F178-492C-9A09-ED28DBE1EF5E}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -69,6 +69,14 @@ Global
{D0B37096-5BC3-41B0-8D81-203CBA3932B0}.Release|Any CPU.Build.0 = Release|x64 {D0B37096-5BC3-41B0-8D81-203CBA3932B0}.Release|Any CPU.Build.0 = Release|x64
{D0B37096-5BC3-41B0-8D81-203CBA3932B0}.Release|x64.ActiveCfg = Release|x64 {D0B37096-5BC3-41B0-8D81-203CBA3932B0}.Release|x64.ActiveCfg = Release|x64
{D0B37096-5BC3-41B0-8D81-203CBA3932B0}.Release|x64.Build.0 = Release|x64 {D0B37096-5BC3-41B0-8D81-203CBA3932B0}.Release|x64.Build.0 = Release|x64
{EDC1C408-D832-4C09-97A2-61B223A84166}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EDC1C408-D832-4C09-97A2-61B223A84166}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EDC1C408-D832-4C09-97A2-61B223A84166}.Debug|x64.ActiveCfg = Debug|Any CPU
{EDC1C408-D832-4C09-97A2-61B223A84166}.Debug|x64.Build.0 = Debug|Any CPU
{EDC1C408-D832-4C09-97A2-61B223A84166}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EDC1C408-D832-4C09-97A2-61B223A84166}.Release|Any CPU.Build.0 = Release|Any CPU
{EDC1C408-D832-4C09-97A2-61B223A84166}.Release|x64.ActiveCfg = Release|Any CPU
{EDC1C408-D832-4C09-97A2-61B223A84166}.Release|x64.Build.0 = Release|Any CPU
{AEC052FA-F178-492C-9A09-ED28DBE1EF5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AEC052FA-F178-492C-9A09-ED28DBE1EF5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AEC052FA-F178-492C-9A09-ED28DBE1EF5E}.Debug|Any CPU.Build.0 = Debug|Any CPU {AEC052FA-F178-492C-9A09-ED28DBE1EF5E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AEC052FA-F178-492C-9A09-ED28DBE1EF5E}.Debug|x64.ActiveCfg = Debug|Any CPU {AEC052FA-F178-492C-9A09-ED28DBE1EF5E}.Debug|x64.ActiveCfg = Debug|Any CPU
@ -77,14 +85,6 @@ Global
{AEC052FA-F178-492C-9A09-ED28DBE1EF5E}.Release|Any CPU.Build.0 = Release|Any CPU {AEC052FA-F178-492C-9A09-ED28DBE1EF5E}.Release|Any CPU.Build.0 = Release|Any CPU
{AEC052FA-F178-492C-9A09-ED28DBE1EF5E}.Release|x64.ActiveCfg = Release|Any CPU {AEC052FA-F178-492C-9A09-ED28DBE1EF5E}.Release|x64.ActiveCfg = Release|Any CPU
{AEC052FA-F178-492C-9A09-ED28DBE1EF5E}.Release|x64.Build.0 = Release|Any CPU {AEC052FA-F178-492C-9A09-ED28DBE1EF5E}.Release|x64.Build.0 = Release|Any CPU
{B1321FD5-7BBF-4C9D-83C1-F8D7C394F32A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B1321FD5-7BBF-4C9D-83C1-F8D7C394F32A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B1321FD5-7BBF-4C9D-83C1-F8D7C394F32A}.Debug|x64.ActiveCfg = Debug|Any CPU
{B1321FD5-7BBF-4C9D-83C1-F8D7C394F32A}.Debug|x64.Build.0 = Debug|Any CPU
{B1321FD5-7BBF-4C9D-83C1-F8D7C394F32A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B1321FD5-7BBF-4C9D-83C1-F8D7C394F32A}.Release|Any CPU.Build.0 = Release|Any CPU
{B1321FD5-7BBF-4C9D-83C1-F8D7C394F32A}.Release|x64.ActiveCfg = Release|Any CPU
{B1321FD5-7BBF-4C9D-83C1-F8D7C394F32A}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@ -5,7 +5,7 @@ Shows possible trap & hoard coffer locations in Palace of the Dead & Heaven on H
## Installation ## Installation
To install this plugin from my plugin repository, please check the To install this plugin from my plugin repository, please check the
[Installation Instructions](https://git.carvel.li/liza/plugin-repo/src/branch/master/README.md). [Installation Instructions](https://github.com/carvelli/Dalamud-Plugins#installation).
Additionally, you **need to install Splatoon**, which is used to render the visible overlays. Additionally, you **need to install Splatoon**, which is used to render the visible overlays.
Please check [Splatoon's Installation Instructions](https://github.com/NightmareXIV/MyDalamudPlugins#installation). Please check [Splatoon's Installation Instructions](https://github.com/NightmareXIV/MyDalamudPlugins#installation).
@ -15,4 +15,4 @@ Please check [Splatoon's Installation Instructions](https://github.com/Nightmare
Please feel free to help by [translating this plugin into your language](https://crowdin.com/project/palace-pal). Please feel free to help by [translating this plugin into your language](https://crowdin.com/project/palace-pal).
If you want to translate the plugin into a language that is currently not enabled, If you want to translate the plugin into a language that is currently not enabled,
[please create a new issue](https://git.carvel.li/liza/PalacePal/issues/new). [please create a new issue](https://github.com/carvelli/PalacePal/issues/new).

1
Server

@ -1 +0,0 @@
Subproject commit e59583fac353fdd960556ed2fa65220d66db5637

View File

@ -1,4 +0,0 @@
git fetch origin master
git reset --hard origin/master
git submodule update --checkout
docker buildx build -t palacepal-server --platform linux/amd64,linux/arm64 .

2
vendor/ECommons vendored

@ -1 +1 @@
Subproject commit dcd85f8cca504360eda4b2cb5327632cc500a22c Subproject commit d9dc8c1c6e914cf37ad47703579d85094246f2e5

1
vendor/LLib vendored

@ -1 +0,0 @@
Subproject commit e206782c1106e1a5292a06af61783faef1ac0c42