Compare commits
32 Commits
Author | SHA1 | Date | |
---|---|---|---|
d239a6e4b0 | |||
9d5e4a797b | |||
3e2f917c14 | |||
2fdf9b1e4d | |||
18ffd66086 | |||
64e576a004 | |||
0d1882d97f | |||
964119cfd2 | |||
309edfcd17 | |||
8fbd3fbc0d | |||
ce9be45e8f | |||
8def5e28a4 | |||
ba06576c2d | |||
f259f03534 | |||
cf3a24afe8 | |||
2a180345eb | |||
345c9c59a3 | |||
5c4e9f30e0 | |||
d337413b82 | |||
5a5d7fecfd | |||
1ee86d378f | |||
cd52192d15 | |||
6260f35b61 | |||
79ad7fbc39 | |||
1bdcc7179c | |||
5ba18477b4 | |||
5f506bda8a | |||
bbec57c3ad | |||
58404b9728 | |||
8042aee951 | |||
0b07e8b8f6 | |||
7e73430179 |
@ -4,4 +4,6 @@
|
|||||||
.gitignore
|
.gitignore
|
||||||
Dockerfile
|
Dockerfile
|
||||||
.dockerignore
|
.dockerignore
|
||||||
|
docker-build.sh
|
||||||
.vs/
|
.vs/
|
||||||
|
.idea/
|
||||||
|
@ -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 = block_scoped
|
csharp_style_namespace_declarations = file_scoped
|
||||||
|
|
||||||
# braces
|
# braces
|
||||||
csharp_prefer_braces = when_multiline
|
csharp_prefer_braces = when_multiline
|
||||||
|
45
.github/workflows/build.yml
vendored
45
.github/workflows/build.yml
vendored
@ -1,45 +0,0 @@
|
|||||||
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
31
.github/workflows/server.yml
vendored
@ -1,31 +0,0 @@
|
|||||||
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
24
.github/workflows/upload-crowdin.yml
vendored
@ -1,24 +0,0 @@
|
|||||||
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
6
.gitmodules
vendored
@ -1,3 +1,9 @@
|
|||||||
[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
|
||||||
|
16
Dockerfile
16
Dockerfile
@ -1,13 +1,19 @@
|
|||||||
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build-env
|
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.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 Pal.Server/Pal.Server.csproj Pal.Server/
|
COPY Server/Server/Pal.Server.csproj Server/Server/
|
||||||
RUN dotnet restore Pal.Server/Pal.Server.csproj
|
RUN dotnet restore Server/Server/Pal.Server.csproj -a $TARGETARCH
|
||||||
|
|
||||||
COPY . ./
|
COPY . ./
|
||||||
RUN dotnet publish Pal.Server/Pal.Server.csproj --configuration Release --no-restore -o /dist
|
RUN dotnet publish Server/Server/Pal.Server.csproj -a $TARGETARCH --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=
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Pal.Client.Commands
|
namespace Pal.Client.Commands;
|
||||||
|
|
||||||
|
public interface ISubCommand
|
||||||
{
|
{
|
||||||
public interface ISubCommand
|
IReadOnlyDictionary<string, Action<string>> GetHandlers();
|
||||||
{
|
|
||||||
IReadOnlyDictionary<string, Action<string>> GetHandlers();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -3,38 +3,37 @@ 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
|
||||||
{
|
{
|
||||||
internal class PalConfigCommand : ISubCommand
|
private readonly IPalacePalConfiguration _configuration;
|
||||||
|
private readonly AgreementWindow _agreementWindow;
|
||||||
|
private readonly ConfigWindow _configWindow;
|
||||||
|
|
||||||
|
public PalConfigCommand(
|
||||||
|
IPalacePalConfiguration configuration,
|
||||||
|
AgreementWindow agreementWindow,
|
||||||
|
ConfigWindow configWindow)
|
||||||
{
|
{
|
||||||
private readonly IPalacePalConfiguration _configuration;
|
_configuration = configuration;
|
||||||
private readonly AgreementWindow _agreementWindow;
|
_agreementWindow = agreementWindow;
|
||||||
private readonly ConfigWindow _configWindow;
|
_configWindow = configWindow;
|
||||||
|
}
|
||||||
|
|
||||||
public PalConfigCommand(
|
|
||||||
IPalacePalConfiguration configuration,
|
public IReadOnlyDictionary<string, Action<string>> GetHandlers()
|
||||||
AgreementWindow agreementWindow,
|
=> new Dictionary<string, Action<string>>
|
||||||
ConfigWindow configWindow)
|
|
||||||
{
|
{
|
||||||
_configuration = configuration;
|
{ "config", _ => Execute() },
|
||||||
_agreementWindow = agreementWindow;
|
{ "", _ => Execute() }
|
||||||
_configWindow = configWindow;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
public void Execute()
|
||||||
public IReadOnlyDictionary<string, Action<string>> GetHandlers()
|
{
|
||||||
=> new Dictionary<string, Action<string>>
|
if (_configuration.FirstUse)
|
||||||
{
|
_agreementWindow.IsOpen = true;
|
||||||
{ "config", _ => Execute() },
|
else
|
||||||
{ "", _ => Execute() }
|
_configWindow.Toggle();
|
||||||
};
|
|
||||||
|
|
||||||
public void Execute()
|
|
||||||
{
|
|
||||||
if (_configuration.FirstUse)
|
|
||||||
_agreementWindow.IsOpen = true;
|
|
||||||
else
|
|
||||||
_configWindow.Toggle();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,63 +1,62 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Dalamud.Game.ClientState;
|
using Dalamud.Plugin.Services;
|
||||||
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
|
||||||
{
|
{
|
||||||
internal sealed class PalNearCommand : ISubCommand
|
private readonly Chat _chat;
|
||||||
|
private readonly IClientState _clientState;
|
||||||
|
private readonly TerritoryState _territoryState;
|
||||||
|
private readonly FloorService _floorService;
|
||||||
|
|
||||||
|
public PalNearCommand(Chat chat, IClientState clientState, TerritoryState territoryState,
|
||||||
|
FloorService floorService)
|
||||||
{
|
{
|
||||||
private readonly Chat _chat;
|
_chat = chat;
|
||||||
private readonly ClientState _clientState;
|
_clientState = clientState;
|
||||||
private readonly TerritoryState _territoryState;
|
_territoryState = territoryState;
|
||||||
private readonly FloorService _floorService;
|
_floorService = floorService;
|
||||||
|
}
|
||||||
|
|
||||||
public PalNearCommand(Chat chat, ClientState clientState, TerritoryState territoryState,
|
|
||||||
FloorService floorService)
|
public IReadOnlyDictionary<string, Action<string>> GetHandlers()
|
||||||
|
=> new Dictionary<string, Action<string>>
|
||||||
{
|
{
|
||||||
_chat = chat;
|
{ "near", _ => DebugNearest(_ => true) },
|
||||||
_clientState = clientState;
|
{ "tnear", _ => DebugNearest(m => m.Type == MemoryLocation.EType.Trap) },
|
||||||
_territoryState = territoryState;
|
{ "hnear", _ => DebugNearest(m => m.Type == MemoryLocation.EType.Hoard) },
|
||||||
_floorService = floorService;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
private void DebugNearest(Predicate<PersistentLocation> predicate)
|
||||||
|
{
|
||||||
|
if (!_territoryState.IsInDeepDungeon())
|
||||||
|
return;
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, Action<string>> GetHandlers()
|
var state = _floorService.GetTerritoryIfReady(_clientState.TerritoryType);
|
||||||
=> new Dictionary<string, Action<string>>
|
if (state == null)
|
||||||
{
|
return;
|
||||||
{ "near", _ => DebugNearest(_ => true) },
|
|
||||||
{ "tnear", _ => DebugNearest(m => m.Type == MemoryLocation.EType.Trap) },
|
|
||||||
{ "hnear", _ => DebugNearest(m => m.Type == MemoryLocation.EType.Hoard) },
|
|
||||||
};
|
|
||||||
|
|
||||||
private void DebugNearest(Predicate<PersistentLocation> predicate)
|
var playerPosition = _clientState.LocalPlayer?.Position;
|
||||||
{
|
if (playerPosition == null)
|
||||||
if (!_territoryState.IsInDeepDungeon())
|
return;
|
||||||
return;
|
_chat.Message($"Your position: {playerPosition}");
|
||||||
|
|
||||||
var state = _floorService.GetTerritoryIfReady(_clientState.TerritoryType);
|
var nearbyMarkers = state.Locations
|
||||||
if (state == null)
|
.Where(m => predicate(m))
|
||||||
return;
|
.Where(m => m.RenderElement != null && m.RenderElement.Enabled)
|
||||||
|
.Select(m => new { m, distance = (playerPosition.Value - m.Position).Length() })
|
||||||
var playerPosition = _clientState.LocalPlayer?.Position;
|
.OrderBy(m => m.distance)
|
||||||
if (playerPosition == null)
|
.Take(5)
|
||||||
return;
|
.ToList();
|
||||||
_chat.Message($"Your position: {playerPosition}");
|
foreach (var nearbyMarker in nearbyMarkers)
|
||||||
|
_chat.UnformattedMessage(
|
||||||
var nearbyMarkers = state.Locations
|
$"{nearbyMarker.distance:F2} - {nearbyMarker.m.Type} {nearbyMarker.m.NetworkId?.ToPartialId(length: 8)} - {nearbyMarker.m.Position}");
|
||||||
.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}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,24 +2,23 @@
|
|||||||
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
|
||||||
{
|
{
|
||||||
internal sealed class PalStatsCommand : ISubCommand
|
private readonly StatisticsService _statisticsService;
|
||||||
|
|
||||||
|
public PalStatsCommand(StatisticsService statisticsService)
|
||||||
{
|
{
|
||||||
private readonly StatisticsService _statisticsService;
|
_statisticsService = statisticsService;
|
||||||
|
|
||||||
public PalStatsCommand(StatisticsService statisticsService)
|
|
||||||
{
|
|
||||||
_statisticsService = statisticsService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, Action<string>> GetHandlers()
|
|
||||||
=> new Dictionary<string, Action<string>>
|
|
||||||
{
|
|
||||||
{ "stats", _ => Execute() },
|
|
||||||
};
|
|
||||||
|
|
||||||
private void Execute()
|
|
||||||
=> _statisticsService.ShowGlobalStatistics();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, Action<string>> GetHandlers()
|
||||||
|
=> new Dictionary<string, Action<string>>
|
||||||
|
{
|
||||||
|
{ "stats", _ => Execute() },
|
||||||
|
};
|
||||||
|
|
||||||
|
private void Execute()
|
||||||
|
=> _statisticsService.ShowGlobalStatistics();
|
||||||
}
|
}
|
||||||
|
@ -3,28 +3,27 @@ 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
|
||||||
{
|
{
|
||||||
internal sealed class PalTestConnectionCommand : ISubCommand
|
private readonly ConfigWindow _configWindow;
|
||||||
|
|
||||||
|
public PalTestConnectionCommand(ConfigWindow configWindow)
|
||||||
{
|
{
|
||||||
private readonly ConfigWindow _configWindow;
|
_configWindow = configWindow;
|
||||||
|
}
|
||||||
|
|
||||||
public PalTestConnectionCommand(ConfigWindow configWindow)
|
public IReadOnlyDictionary<string, Action<string>> GetHandlers()
|
||||||
|
=> new Dictionary<string, Action<string>>
|
||||||
{
|
{
|
||||||
_configWindow = configWindow;
|
{ "test-connection", _ => Execute() },
|
||||||
}
|
{ "tc", _ => Execute() },
|
||||||
|
};
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, Action<string>> GetHandlers()
|
private void Execute()
|
||||||
=> new Dictionary<string, Action<string>>
|
{
|
||||||
{
|
_configWindow.IsOpen = true;
|
||||||
{ "test-connection", _ => Execute() },
|
var _ = new TickScheduler(() => _configWindow.TestConnection());
|
||||||
{ "tc", _ => Execute() },
|
|
||||||
};
|
|
||||||
|
|
||||||
private void Execute()
|
|
||||||
{
|
|
||||||
_configWindow.IsOpen = true;
|
|
||||||
var _ = new TickScheduler(() => _configWindow.TestConnection());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,146 +4,141 @@ 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
|
||||||
{
|
{
|
||||||
public sealed class AccountConfigurationV7 : IAccountConfiguration
|
private const int DefaultEntropyLength = 16;
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
public AccountConfigurationV7()
|
||||||
{
|
{
|
||||||
private const int DefaultEntropyLength = 16;
|
}
|
||||||
|
|
||||||
private static readonly ILogger _logger =
|
public AccountConfigurationV7(string server, Guid accountId)
|
||||||
DependencyInjectionContext.LoggerProvider.CreateLogger<AccountConfigurationV7>();
|
{
|
||||||
|
Server = server;
|
||||||
|
(EncryptedId, Entropy, Format) = EncryptAccountId(accountId);
|
||||||
|
}
|
||||||
|
|
||||||
[JsonConstructor]
|
[Obsolete("for V1 import")]
|
||||||
public AccountConfigurationV7()
|
public AccountConfigurationV7(string server, string accountId)
|
||||||
|
{
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
|
||||||
public AccountConfigurationV7(string server, Guid accountId)
|
[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)
|
||||||
{
|
{
|
||||||
Server = server;
|
try
|
||||||
(EncryptedId, Entropy, Format) = EncryptAccountId(accountId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Obsolete("for V1 import")]
|
|
||||||
public AccountConfigurationV7(string server, string accountId)
|
|
||||||
{
|
|
||||||
Server = server;
|
|
||||||
|
|
||||||
if (accountId.StartsWith("s:"))
|
|
||||||
{
|
{
|
||||||
EncryptedId = accountId.Substring(2);
|
byte[] guidBytes = ProtectedData.Unprotect(Convert.FromBase64String(EncryptedId), Entropy, DataProtectionScope.CurrentUser);
|
||||||
Entropy = ConfigurationData.FixedV1Entropy;
|
return new Guid(guidBytes);
|
||||||
Format = EFormat.UseProtectedData;
|
|
||||||
EncryptIfNeeded();
|
|
||||||
}
|
}
|
||||||
else if (Guid.TryParse(accountId, out Guid guid))
|
catch (Exception)
|
||||||
(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 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;
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (Format == EFormat.Unencrypted)
|
||||||
|
return Guid.Parse(EncryptedId);
|
||||||
|
else if (Format == EFormat.ProtectedDataUnsupported && !ConfigurationData.SupportsDpapi)
|
||||||
|
return Guid.Parse(EncryptedId);
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public bool EncryptIfNeeded()
|
private (string encryptedId, byte[]? entropy, EFormat format) EncryptAccountId(Guid g)
|
||||||
|
{
|
||||||
|
if (!ConfigurationData.SupportsDpapi)
|
||||||
|
return (g.ToString(), null, EFormat.ProtectedDataUnsupported);
|
||||||
|
else
|
||||||
{
|
{
|
||||||
if (Format == EFormat.Unencrypted)
|
try
|
||||||
{
|
{
|
||||||
var (newId, newEntropy, newFormat) = EncryptAccountId(Guid.Parse(EncryptedId));
|
byte[] entropy = RandomNumberGenerator.GetBytes(DefaultEntropyLength);
|
||||||
if (newFormat != EFormat.Unencrypted)
|
byte[] guidBytes = ProtectedData.Protect(g.ToByteArray(), entropy, DataProtectionScope.CurrentUser);
|
||||||
{
|
return (Convert.ToBase64String(guidBytes), entropy, EFormat.UseProtectedData);
|
||||||
EncryptedId = newId;
|
|
||||||
Entropy = newEntropy;
|
|
||||||
Format = newFormat;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (Format == EFormat.UseProtectedData && Entropy is { Length: < DefaultEntropyLength })
|
catch (Exception)
|
||||||
{
|
{
|
||||||
Guid? g = DecryptAccountId();
|
return (g.ToString(), null, EFormat.Unencrypted);
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,42 +3,38 @@ 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
|
||||||
{
|
{
|
||||||
internal static class ConfigurationData
|
[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;
|
||||||
|
|
||||||
|
public static bool SupportsDpapi
|
||||||
{
|
{
|
||||||
private static readonly ILogger _logger =
|
get
|
||||||
DependencyInjectionContext.LoggerProvider.CreateLogger(typeof(ConfigurationData));
|
|
||||||
|
|
||||||
[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)
|
||||||
{
|
{
|
||||||
if (_supportsDpapi == null)
|
try
|
||||||
{
|
{
|
||||||
try
|
byte[] input = RandomNumberGenerator.GetBytes(32);
|
||||||
{
|
byte[] entropy = RandomNumberGenerator.GetBytes(16);
|
||||||
byte[] input = RandomNumberGenerator.GetBytes(32);
|
byte[] temp = ProtectedData.Protect(input, entropy, DataProtectionScope.CurrentUser);
|
||||||
byte[] entropy = RandomNumberGenerator.GetBytes(16);
|
byte[] output = ProtectedData.Unprotect(temp, entropy, DataProtectionScope.CurrentUser);
|
||||||
byte[] temp = ProtectedData.Protect(input, entropy, DataProtectionScope.CurrentUser);
|
_supportsDpapi = input.SequenceEqual(output);
|
||||||
byte[] output = ProtectedData.Unprotect(temp, entropy, DataProtectionScope.CurrentUser);
|
}
|
||||||
_supportsDpapi = input.SequenceEqual(output);
|
catch (Exception)
|
||||||
}
|
{
|
||||||
catch (Exception)
|
_supportsDpapi = false;
|
||||||
{
|
|
||||||
_supportsDpapi = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogTrace("DPAPI support: {Supported}", _supportsDpapi);
|
|
||||||
}
|
}
|
||||||
return _supportsDpapi.Value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return _supportsDpapi.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,144 +13,158 @@ 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
|
||||||
{
|
{
|
||||||
internal sealed class ConfigurationManager
|
private readonly ILogger<ConfigurationManager> _logger;
|
||||||
|
private readonly IDalamudPluginInterface _pluginInterface;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
|
||||||
|
public event EventHandler<IPalacePalConfiguration>? Saved;
|
||||||
|
|
||||||
|
public ConfigurationManager(ILogger<ConfigurationManager> logger, IDalamudPluginInterface pluginInterface,
|
||||||
|
IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
private readonly ILogger<ConfigurationManager> _logger;
|
_logger = logger;
|
||||||
private readonly DalamudPluginInterface _pluginInterface;
|
_pluginInterface = pluginInterface;
|
||||||
private readonly IServiceProvider _serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
public event EventHandler<IPalacePalConfiguration>? Saved;
|
private string ConfigPath =>
|
||||||
|
Path.Join(_pluginInterface.GetPluginConfigDirectory(), ConfigurationData.ConfigFileName);
|
||||||
|
|
||||||
public ConfigurationManager(ILogger<ConfigurationManager> logger, DalamudPluginInterface pluginInterface,
|
public IPalacePalConfiguration Load()
|
||||||
IServiceProvider serviceProvider)
|
{
|
||||||
|
if (!File.Exists(ConfigPath))
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger.LogInformation("No config file exists, creating one");
|
||||||
_pluginInterface = pluginInterface;
|
Save(new ConfigurationV7(), false);
|
||||||
_serviceProvider = serviceProvider;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ConfigPath =>
|
return JsonSerializer.Deserialize<ConfigurationV7>(File.ReadAllText(ConfigPath, Encoding.UTF8)) ??
|
||||||
Path.Join(_pluginInterface.GetPluginConfigDirectory(), ConfigurationData.ConfigFileName);
|
new ConfigurationV7();
|
||||||
|
}
|
||||||
|
|
||||||
public IPalacePalConfiguration Load()
|
public void Save(IConfigurationInConfigDirectory config, bool queue = true)
|
||||||
{
|
{
|
||||||
if (!File.Exists(ConfigPath))
|
File.WriteAllText(ConfigPath,
|
||||||
{
|
JsonSerializer.Serialize(config, config.GetType(),
|
||||||
_logger.LogInformation("No config file exists, creating one");
|
new JsonSerializerOptions
|
||||||
Save(new ConfigurationV7(), false);
|
{ WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }),
|
||||||
}
|
Encoding.UTF8);
|
||||||
|
|
||||||
return JsonSerializer.Deserialize<ConfigurationV7>(File.ReadAllText(ConfigPath, Encoding.UTF8)) ??
|
if (queue && config is ConfigurationV7 v7)
|
||||||
new ConfigurationV7();
|
Saved?.Invoke(this, v7);
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
{
|
{
|
||||||
if (_pluginInterface.ConfigFile.Exists)
|
_logger.LogInformation("Migrating config file from v1-v6 format");
|
||||||
|
|
||||||
|
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 config file from v1-v6 format");
|
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
|
||||||
|
dbContext.Imports.RemoveRange(dbContext.Imports);
|
||||||
|
|
||||||
ConfigurationV1 configurationV1 =
|
foreach (var importHistory in configurationV1.ImportHistory)
|
||||||
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 import {Id}", importHistory.Id);
|
||||||
dbContext.Imports.RemoveRange(dbContext.Imports);
|
dbContext.Imports.Add(new ImportHistory
|
||||||
|
|
||||||
foreach (var importHistory in configurationV1.ImportHistory)
|
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Migrating import {Id}", importHistory.Id);
|
Id = importHistory.Id,
|
||||||
dbContext.Imports.Add(new ImportHistory
|
RemoteUrl = importHistory.RemoteUrl
|
||||||
{
|
?.Replace(".μ.tv", ".liza.sh")
|
||||||
Id = importHistory.Id,
|
.Replace("pal.liza.sh", "connect.palacepal.com"),
|
||||||
RemoteUrl = importHistory.RemoteUrl?.Replace(".μ.tv", ".liza.sh"),
|
ExportedAt = importHistory.ExportedAt,
|
||||||
ExportedAt = importHistory.ExportedAt,
|
ImportedAt = importHistory.ImportedAt
|
||||||
ImportedAt = importHistory.ImportedAt
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
dbContext.SaveChanges();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
File.Move(_pluginInterface.ConfigFile.FullName, _pluginInterface.ConfigFile.FullName + ".old", true);
|
dbContext.SaveChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
File.Move(_pluginInterface.ConfigFile.FullName, _pluginInterface.ConfigFile.FullName + ".old", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ConfigurationV7 MigrateToV7(ConfigurationV1 v1)
|
IPalacePalConfiguration? currentConfig = Load();
|
||||||
|
IAccountConfiguration? legacyAccount = currentConfig?.FindAccount("https://pal.liza.sh");
|
||||||
|
if (currentConfig != null && legacyAccount != null)
|
||||||
{
|
{
|
||||||
ConfigurationV7 v7 = new()
|
IAccountConfiguration newAccount = currentConfig.CreateAccount("https://connect.palacepal.com", legacyAccount.AccountId);
|
||||||
{
|
newAccount.CachedRoles = legacyAccount.CachedRoles;
|
||||||
Version = 7,
|
|
||||||
FirstUse = v1.FirstUse,
|
|
||||||
Mode = v1.Mode,
|
|
||||||
BetaKey = v1.BetaKey,
|
|
||||||
|
|
||||||
DeepDungeons = new DeepDungeonConfiguration
|
currentConfig.RemoveAccount(legacyAccount.Server);
|
||||||
{
|
Save(currentConfig, false);
|
||||||
Traps = new MarkerConfiguration
|
|
||||||
{
|
|
||||||
Show = v1.ShowTraps,
|
|
||||||
Color = ImGui.ColorConvertFloat4ToU32(v1.TrapColor),
|
|
||||||
OnlyVisibleAfterPomander = v1.OnlyVisibleTrapsAfterPomander,
|
|
||||||
Fill = false
|
|
||||||
},
|
|
||||||
HoardCoffers = new MarkerConfiguration
|
|
||||||
{
|
|
||||||
Show = v1.ShowHoard,
|
|
||||||
Color = ImGui.ColorConvertFloat4ToU32(v1.HoardColor),
|
|
||||||
OnlyVisibleAfterPomander = v1.OnlyVisibleHoardAfterPomander,
|
|
||||||
Fill = false
|
|
||||||
},
|
|
||||||
SilverCoffers = new MarkerConfiguration
|
|
||||||
{
|
|
||||||
Show = v1.ShowSilverCoffers,
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Migrate ImportHistory
|
|
||||||
|
|
||||||
return v7;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
Show = v1.ShowTraps,
|
||||||
|
Color = ImGui.ColorConvertFloat4ToU32(v1.TrapColor),
|
||||||
|
OnlyVisibleAfterPomander = v1.OnlyVisibleTrapsAfterPomander,
|
||||||
|
Fill = false
|
||||||
|
},
|
||||||
|
HoardCoffers = new MarkerConfiguration
|
||||||
|
{
|
||||||
|
Show = v1.ShowHoard,
|
||||||
|
Color = ImGui.ColorConvertFloat4ToU32(v1.HoardColor),
|
||||||
|
OnlyVisibleAfterPomander = v1.OnlyVisibleHoardAfterPomander,
|
||||||
|
Fill = false
|
||||||
|
},
|
||||||
|
SilverCoffers = new MarkerConfiguration
|
||||||
|
{
|
||||||
|
Show = v1.ShowSilverCoffers,
|
||||||
|
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")
|
||||||
|
.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
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,53 +2,52 @@
|
|||||||
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 sealed class ConfigurationV7 : IPalacePalConfiguration, IConfigurationInConfigDirectory
|
public int Version { get; set; } = 7;
|
||||||
|
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
public int Version { get; set; } = 7;
|
var account = new AccountConfigurationV7(server, accountId);
|
||||||
|
Accounts.Add(account);
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
public bool FirstUse { get; set; } = true;
|
[Obsolete("for V1 import")]
|
||||||
public EMode Mode { get; set; }
|
internal IAccountConfiguration CreateAccount(string server, string accountId)
|
||||||
public string BetaKey { get; init; } = "";
|
{
|
||||||
|
var account = new AccountConfigurationV7(server, accountId);
|
||||||
|
Accounts.Add(account);
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
public DeepDungeonConfiguration DeepDungeons { get; set; } = new();
|
public IAccountConfiguration? FindAccount(string server)
|
||||||
public RendererConfiguration Renderer { get; set; } = new();
|
{
|
||||||
public List<AccountConfigurationV7> Accounts { get; set; } = new();
|
return Accounts.FirstOrDefault(a => a.Server == server && a.IsUsable);
|
||||||
public BackupConfiguration Backups { get; set; } = new();
|
}
|
||||||
|
|
||||||
public IAccountConfiguration CreateAccount(string server, Guid accountId)
|
public void RemoveAccount(string server)
|
||||||
{
|
{
|
||||||
var account = new AccountConfigurationV7(server, accountId);
|
Accounts.RemoveAll(a => a.Server == server && a.IsUsable);
|
||||||
Accounts.Add(account);
|
}
|
||||||
return account;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Obsolete("for V1 import")]
|
public bool HasRoleOnCurrentServer(string server, string role)
|
||||||
internal IAccountConfiguration CreateAccount(string server, string accountId)
|
{
|
||||||
{
|
if (Mode != EMode.Online)
|
||||||
var account = new AccountConfigurationV7(server, accountId);
|
return false;
|
||||||
Accounts.Add(account);
|
|
||||||
return account;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IAccountConfiguration? FindAccount(string server)
|
var account = FindAccount(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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
namespace Pal.Client.Configuration
|
namespace Pal.Client.Configuration;
|
||||||
{
|
|
||||||
public enum EMode
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Fetches trap locations from remote server.
|
|
||||||
/// </summary>
|
|
||||||
Online = 1,
|
|
||||||
|
|
||||||
/// <summary>
|
public enum EMode
|
||||||
/// Only shows traps found by yourself using a pomander of sight.
|
{
|
||||||
/// </summary>
|
/// <summary>
|
||||||
Offline = 2,
|
/// Fetches trap locations from remote server.
|
||||||
}
|
/// </summary>
|
||||||
|
Online = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Only shows traps found by yourself using a pomander of sight.
|
||||||
|
/// </summary>
|
||||||
|
Offline = 2,
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
namespace Pal.Client.Configuration
|
namespace Pal.Client.Configuration;
|
||||||
{
|
|
||||||
public enum ERenderer
|
|
||||||
{
|
|
||||||
/// <see cref="Rendering.SimpleRenderer"/>
|
|
||||||
Simple = 0,
|
|
||||||
|
|
||||||
/// <see cref="Rendering.SplatoonRenderer"/>
|
public enum ERenderer
|
||||||
Splatoon = 1,
|
{
|
||||||
}
|
/// <see cref="Rendering.SimpleRenderer"/>
|
||||||
|
Simple = 0,
|
||||||
|
|
||||||
|
/// <see cref="Rendering.SplatoonRenderer"/>
|
||||||
|
Splatoon = 1,
|
||||||
}
|
}
|
||||||
|
@ -4,108 +4,107 @@ using System.Numerics;
|
|||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Pal.Client.Configuration
|
namespace Pal.Client.Configuration;
|
||||||
|
|
||||||
|
public interface IVersioned
|
||||||
{
|
{
|
||||||
public interface IVersioned
|
int Version { get; set; }
|
||||||
{
|
}
|
||||||
int Version { get; set; }
|
public interface IConfigurationInConfigDirectory : IVersioned
|
||||||
}
|
{
|
||||||
public interface IConfigurationInConfigDirectory : IVersioned
|
}
|
||||||
{
|
|
||||||
}
|
public interface IPalacePalConfiguration : IConfigurationInConfigDirectory
|
||||||
|
{
|
||||||
public interface IPalacePalConfiguration : IConfigurationInConfigDirectory
|
bool FirstUse { get; set; }
|
||||||
{
|
EMode Mode { get; set; }
|
||||||
bool FirstUse { get; set; }
|
string BetaKey { get; }
|
||||||
EMode Mode { get; set; }
|
bool HasBetaFeature(string feature) => BetaKey.Contains(feature);
|
||||||
string BetaKey { get; }
|
|
||||||
bool HasBetaFeature(string feature) => BetaKey.Contains(feature);
|
DeepDungeonConfiguration DeepDungeons { get; set; }
|
||||||
|
RendererConfiguration Renderer { get; set; }
|
||||||
DeepDungeonConfiguration DeepDungeons { get; set; }
|
BackupConfiguration Backups { get; set; }
|
||||||
RendererConfiguration Renderer { get; set; }
|
|
||||||
BackupConfiguration Backups { get; set; }
|
IAccountConfiguration CreateAccount(string server, Guid accountId);
|
||||||
|
IAccountConfiguration? FindAccount(string server);
|
||||||
IAccountConfiguration CreateAccount(string server, Guid accountId);
|
void RemoveAccount(string server);
|
||||||
IAccountConfiguration? FindAccount(string server);
|
|
||||||
void RemoveAccount(string server);
|
bool HasRoleOnCurrentServer(string server, string role);
|
||||||
|
}
|
||||||
bool HasRoleOnCurrentServer(string server, string role);
|
|
||||||
}
|
public class DeepDungeonConfiguration
|
||||||
|
{
|
||||||
public class DeepDungeonConfiguration
|
public MarkerConfiguration Traps { get; set; } = new()
|
||||||
{
|
{
|
||||||
public MarkerConfiguration Traps { get; set; } = new()
|
Show = true,
|
||||||
{
|
Color = ImGui.ColorConvertFloat4ToU32(new Vector4(1, 0, 0, 0.4f)),
|
||||||
Show = true,
|
OnlyVisibleAfterPomander = true,
|
||||||
Color = ImGui.ColorConvertFloat4ToU32(new Vector4(1, 0, 0, 0.4f)),
|
Fill = false
|
||||||
OnlyVisibleAfterPomander = true,
|
};
|
||||||
Fill = false
|
|
||||||
};
|
public MarkerConfiguration HoardCoffers { get; set; } = new()
|
||||||
|
{
|
||||||
public MarkerConfiguration HoardCoffers { get; set; } = new()
|
Show = true,
|
||||||
{
|
Color = ImGui.ColorConvertFloat4ToU32(new Vector4(0, 1, 1, 0.4f)),
|
||||||
Show = true,
|
OnlyVisibleAfterPomander = true,
|
||||||
Color = ImGui.ColorConvertFloat4ToU32(new Vector4(0, 1, 1, 0.4f)),
|
Fill = false
|
||||||
OnlyVisibleAfterPomander = true,
|
};
|
||||||
Fill = false
|
|
||||||
};
|
public MarkerConfiguration SilverCoffers { get; set; } = new()
|
||||||
|
{
|
||||||
public MarkerConfiguration SilverCoffers { get; set; } = new()
|
Show = false,
|
||||||
{
|
Color = ImGui.ColorConvertFloat4ToU32(new Vector4(1, 1, 1, 0.4f)),
|
||||||
Show = false,
|
OnlyVisibleAfterPomander = false,
|
||||||
Color = ImGui.ColorConvertFloat4ToU32(new Vector4(1, 1, 1, 0.4f)),
|
Fill = true
|
||||||
OnlyVisibleAfterPomander = false,
|
};
|
||||||
Fill = true
|
|
||||||
};
|
public MarkerConfiguration GoldCoffers { get; set; } = new()
|
||||||
|
{
|
||||||
public MarkerConfiguration GoldCoffers { get; set; } = new()
|
Show = false,
|
||||||
{
|
Color = ImGui.ColorConvertFloat4ToU32(new Vector4(1, 1, 0, 0.4f)),
|
||||||
Show = false,
|
OnlyVisibleAfterPomander = false,
|
||||||
Color = ImGui.ColorConvertFloat4ToU32(new Vector4(1, 1, 0, 0.4f)),
|
Fill = true
|
||||||
OnlyVisibleAfterPomander = false,
|
};
|
||||||
Fill = true
|
}
|
||||||
};
|
|
||||||
}
|
public class MarkerConfiguration
|
||||||
|
{
|
||||||
public class MarkerConfiguration
|
[JsonRequired]
|
||||||
{
|
public bool Show { get; set; }
|
||||||
[JsonRequired]
|
|
||||||
public bool Show { get; set; }
|
[JsonRequired]
|
||||||
|
public uint Color { get; set; }
|
||||||
[JsonRequired]
|
|
||||||
public uint Color { get; set; }
|
public bool OnlyVisibleAfterPomander { get; set; }
|
||||||
|
public bool Fill { get; set; }
|
||||||
public bool OnlyVisibleAfterPomander { get; set; }
|
}
|
||||||
public bool Fill { get; set; }
|
|
||||||
}
|
public class RendererConfiguration
|
||||||
|
{
|
||||||
public class RendererConfiguration
|
public ERenderer SelectedRenderer { get; set; } = ERenderer.Splatoon;
|
||||||
{
|
}
|
||||||
public ERenderer SelectedRenderer { get; set; } = ERenderer.Splatoon;
|
|
||||||
}
|
public interface IAccountConfiguration
|
||||||
|
{
|
||||||
public interface IAccountConfiguration
|
bool IsUsable { get; }
|
||||||
{
|
string Server { get; }
|
||||||
bool IsUsable { get; }
|
Guid AccountId { get; }
|
||||||
string Server { get; }
|
|
||||||
Guid AccountId { get; }
|
/// <summary>
|
||||||
|
/// This is taken from the JWT, and is only refreshed on a successful login.
|
||||||
/// <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.
|
||||||
///
|
///
|
||||||
/// 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.
|
||||||
/// This has no impact on what roles the JWT actually contains, but is just to make it
|
/// </summary>
|
||||||
/// easier to draw a consistent UI. The server will still reject unauthorized calls.
|
List<string> CachedRoles { get; set; }
|
||||||
/// </summary>
|
|
||||||
List<string> CachedRoles { get; set; }
|
bool EncryptIfNeeded();
|
||||||
|
}
|
||||||
bool EncryptIfNeeded();
|
|
||||||
}
|
public class BackupConfiguration
|
||||||
|
{
|
||||||
public class BackupConfiguration
|
public int MinimumBackupsToKeep { get; set; } = 3;
|
||||||
{
|
public int DaysToDeleteAfter { get; set; } = 21;
|
||||||
public int MinimumBackupsToKeep { get; set; } = 3;
|
|
||||||
public int DaysToDeleteAfter { get; set; } = 21;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -8,160 +8,159 @@ 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 sealed class ConfigurationV1
|
public string? DebugAccountId { private get; set; }
|
||||||
|
|
||||||
|
[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)
|
||||||
{
|
{
|
||||||
public int Version { get; set; } = 6;
|
if (Version == 1)
|
||||||
|
|
||||||
#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]
|
|
||||||
public string? DebugAccountId { private get; set; }
|
|
||||||
|
|
||||||
[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(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
|
||||||
{
|
{
|
||||||
logger.LogInformation("Updating config to version 2");
|
Id = x.Value.ToString() // encryption happens in V7 migration at latest
|
||||||
|
});
|
||||||
|
Version = 3;
|
||||||
|
Save(pluginInterface);
|
||||||
|
}
|
||||||
|
|
||||||
if (DebugAccountId != null && Guid.TryParse(DebugAccountId, out Guid debugAccountId))
|
if (Version == 3)
|
||||||
AccountIds["http://localhost:5145"] = debugAccountId;
|
{
|
||||||
|
Version = 4;
|
||||||
|
Save(pluginInterface);
|
||||||
|
}
|
||||||
|
|
||||||
if (AccountId != null && Guid.TryParse(AccountId, out Guid accountId))
|
if (Version == 4)
|
||||||
AccountIds["https://pal.μ.tv"] = accountId;
|
{
|
||||||
|
// 2.2 had a bug that would mark chests as traps, there's no easy way to detect this -- or clean this up.
|
||||||
Version = 2;
|
// Not a problem for online players, but offline players might be fucked.
|
||||||
Save(pluginInterface);
|
//bool changedAnyFile = false;
|
||||||
}
|
JsonFloorState.ForEach(s =>
|
||||||
|
|
||||||
if (Version == 2)
|
|
||||||
{
|
{
|
||||||
logger.LogInformation("Updating config to version 3");
|
foreach (var marker in s.Markers)
|
||||||
|
marker.SinceVersion = "0.0";
|
||||||
|
|
||||||
Accounts = AccountIds.ToDictionary(x => x.Key, x => new AccountInfo
|
var lastModified = File.GetLastWriteTimeUtc(s.GetSaveLocation());
|
||||||
|
if (lastModified >= new DateTime(2023, 2, 3, 0, 0, 0, DateTimeKind.Utc))
|
||||||
{
|
{
|
||||||
Id = x.Value.ToString() // encryption happens in V7 migration at latest
|
s.Backup(suffix: "bak");
|
||||||
});
|
|
||||||
Version = 3;
|
|
||||||
Save(pluginInterface);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Version == 3)
|
s.Markers = new ConcurrentBag<JsonMarker>(s.Markers.Where(m => m.SinceVersion != "0.0" || m.Type == JsonMarker.EType.Hoard || m.WasImported));
|
||||||
{
|
s.Save();
|
||||||
Version = 4;
|
|
||||||
Save(pluginInterface);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Version == 4)
|
//changedAnyFile = true;
|
||||||
{
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
*/
|
else
|
||||||
|
{
|
||||||
|
// just add version information, nothing else
|
||||||
|
s.Save();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Version = 5;
|
/*
|
||||||
Save(pluginInterface);
|
// Only notify offline users - we can just re-download the backup markers from the server seamlessly.
|
||||||
}
|
if (Mode == EMode.Offline && changedAnyFile)
|
||||||
|
|
||||||
if (Version == 5)
|
|
||||||
{
|
{
|
||||||
JsonFloorState.UpdateAll();
|
_ = new TickScheduler(delegate
|
||||||
|
{
|
||||||
Version = 6;
|
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.");
|
||||||
Save(pluginInterface);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Save(DalamudPluginInterface pluginInterface)
|
if (Version == 5)
|
||||||
{
|
{
|
||||||
File.WriteAllText(pluginInterface.ConfigFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented, new JsonSerializerSettings
|
JsonFloorState.UpdateAll();
|
||||||
{
|
|
||||||
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
|
|
||||||
TypeNameHandling = TypeNameHandling.Objects
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class AccountInfo
|
Version = 6;
|
||||||
{
|
Save(pluginInterface);
|
||||||
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; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
/// Set when the file is imported locally.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime ImportedAt { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,164 +7,155 @@ 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
|
||||||
{
|
{
|
||||||
/// <summary>
|
private static readonly JsonSerializerOptions JsonSerializerOptions = new() { IncludeFields = true };
|
||||||
/// Legacy JSON file for marker locations.
|
private const int CurrentVersion = 4;
|
||||||
/// </summary>
|
|
||||||
[Obsolete]
|
private static string _pluginConfigDirectory = null!;
|
||||||
public sealed class JsonFloorState
|
|
||||||
|
internal static void SetContextProperties(string pluginConfigDirectory)
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions JsonSerializerOptions = new() { IncludeFields = true };
|
_pluginConfigDirectory = pluginConfigDirectory;
|
||||||
private const int CurrentVersion = 4;
|
}
|
||||||
|
|
||||||
private static string _pluginConfigDirectory = null!;
|
public ushort TerritoryType { get; set; }
|
||||||
|
public ConcurrentBag<JsonMarker> Markers { get; set; } = new();
|
||||||
|
|
||||||
// might not be true, but this is 'less strict filtering' for migrations
|
public JsonFloorState(ushort territoryType)
|
||||||
private static readonly EMode _mode = EMode.Online;
|
{
|
||||||
|
TerritoryType = territoryType;
|
||||||
|
}
|
||||||
|
|
||||||
internal static void SetContextProperties(string pluginConfigDirectory)
|
private void ApplyFilters()
|
||||||
|
{
|
||||||
|
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] == '[')
|
||||||
{
|
{
|
||||||
_pluginConfigDirectory = pluginConfigDirectory;
|
// v1 only had a list of markers, not a JSON object as root
|
||||||
|
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)
|
|
||||||
{
|
{
|
||||||
TerritoryType = territoryType;
|
var save = JsonSerializer.Deserialize<SaveFile>(content, JsonSerializerOptions);
|
||||||
}
|
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;
|
||||||
|
|
||||||
string content = File.ReadAllText(path);
|
localState = new JsonFloorState(territoryType)
|
||||||
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
|
Markers = new ConcurrentBag<JsonMarker>(save.Markers.Where(o => o.Type == JsonMarker.EType.Trap || o.Type == JsonMarker.EType.Hoard)),
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Save()
|
localState.ApplyFilters();
|
||||||
{
|
|
||||||
string path = GetSaveLocation(TerritoryType);
|
|
||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Backup(string suffix)
|
private void SaveImpl(string path)
|
||||||
|
{
|
||||||
|
foreach (var marker in Markers)
|
||||||
{
|
{
|
||||||
string path = $"{GetSaveLocation(TerritoryType)}.{suffix}";
|
if (string.IsNullOrEmpty(marker.SinceVersion))
|
||||||
if (!File.Exists(path))
|
marker.SinceVersion = typeof(Plugin).Assembly.GetName().Version!.ToString(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Markers.Count == 0)
|
||||||
|
File.Delete(path);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
File.WriteAllText(path, JsonSerializer.Serialize(new SaveFile
|
||||||
{
|
{
|
||||||
SaveImpl(path);
|
Version = CurrentVersion,
|
||||||
}
|
Markers = new HashSet<JsonMarker>(Markers)
|
||||||
}
|
}, JsonSerializerOptions));
|
||||||
|
|
||||||
private void SaveImpl(string path)
|
|
||||||
{
|
|
||||||
foreach (var marker in Markers)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(marker.SinceVersion))
|
|
||||||
marker.SinceVersion = typeof(Plugin).Assembly.GetName().Version!.ToString(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Markers.Count == 0)
|
|
||||||
File.Delete(path);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
File.WriteAllText(path, JsonSerializer.Serialize(new SaveFile
|
|
||||||
{
|
|
||||||
Version = CurrentVersion,
|
|
||||||
Markers = new HashSet<JsonMarker>(Markers)
|
|
||||||
}, JsonSerializerOptions));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetSaveLocation() => GetSaveLocation(TerritoryType);
|
|
||||||
|
|
||||||
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
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string GetSaveLocation() => GetSaveLocation(TerritoryType);
|
||||||
|
|
||||||
|
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
|
||||||
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,25 +2,24 @@
|
|||||||
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;
|
|
||||||
public Vector3 Position { get; set; }
|
|
||||||
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
|
[Obsolete]
|
||||||
{
|
public class JsonMarker
|
||||||
Unknown = 0,
|
{
|
||||||
Trap = 1,
|
public EType Type { get; set; } = EType.Unknown;
|
||||||
Hoard = 2,
|
public Vector3 Position { get; set; }
|
||||||
Debug = 3,
|
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,
|
||||||
|
Trap = 1,
|
||||||
|
Hoard = 2,
|
||||||
|
Debug = 3,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,136 +12,135 @@ 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;
|
|
||||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
|
||||||
private readonly DalamudPluginInterface _pluginInterface;
|
|
||||||
|
|
||||||
public JsonMigration(ILogger<JsonMigration> logger, IServiceScopeFactory serviceScopeFactory,
|
/// <summary>
|
||||||
DalamudPluginInterface pluginInterface)
|
/// Imports legacy territoryType.json files into the database if it exists, and no markers for that territory exist.
|
||||||
{
|
/// </summary>
|
||||||
_logger = logger;
|
internal sealed class JsonMigration
|
||||||
_serviceScopeFactory = serviceScopeFactory;
|
{
|
||||||
_pluginInterface = pluginInterface;
|
private readonly ILogger<JsonMigration> _logger;
|
||||||
}
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
|
private readonly IDalamudPluginInterface _pluginInterface;
|
||||||
|
|
||||||
|
public JsonMigration(ILogger<JsonMigration> logger, IServiceScopeFactory serviceScopeFactory,
|
||||||
|
IDalamudPluginInterface 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)
|
||||||
{
|
{
|
||||||
List<JsonFloorState> floorsToMigrate = new();
|
_logger.LogInformation("Found no floors to migrate");
|
||||||
JsonFloorState.ForEach(floorsToMigrate.Add);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (floorsToMigrate.Count == 0)
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
{
|
|
||||||
_logger.LogInformation("Found no floors to migrate");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
await using var scope = _serviceScopeFactory.CreateAsyncScope();
|
||||||
|
await using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
|
||||||
|
|
||||||
await using var scope = _serviceScopeFactory.CreateAsyncScope();
|
var fileStream = new FileStream(
|
||||||
await using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
|
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);
|
||||||
|
|
||||||
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(),
|
|
||||||
Path.GetFileName(floorToMigrate.GetSaveLocation()), CompressionLevel.SmallestSize);
|
|
||||||
await MigrateFloor(dbContext, floorToMigrate, imports, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Removing {Count} old json files", floorsToMigrate.Count);
|
|
||||||
foreach (var floorToMigrate in floorsToMigrate)
|
foreach (var floorToMigrate in floorsToMigrate)
|
||||||
File.Delete(floorToMigrate.GetSaveLocation());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <returns>Whether to archive this file once complete</returns>
|
|
||||||
private async Task MigrateFloor(
|
|
||||||
PalClientContext dbContext,
|
|
||||||
JsonFloorState floorToMigrate,
|
|
||||||
IReadOnlyDictionary<Guid, ImportHistory> imports,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
using var logScope = _logger.BeginScope($"Import {(ETerritoryType)floorToMigrate.TerritoryType}");
|
|
||||||
if (floorToMigrate.Markers.Count == 0)
|
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Skipping migration, floor has no markers");
|
backup.CreateEntryFromFile(floorToMigrate.GetSaveLocation(),
|
||||||
|
Path.GetFileName(floorToMigrate.GetSaveLocation()), CompressionLevel.SmallestSize);
|
||||||
|
await MigrateFloor(dbContext, floorToMigrate, imports, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await dbContext.Locations.AnyAsync(o => o.TerritoryType == floorToMigrate.TerritoryType,
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
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)
|
_logger.LogInformation("Removing {Count} old json files", floorsToMigrate.Count);
|
||||||
{
|
foreach (var floorToMigrate in floorsToMigrate)
|
||||||
return type switch
|
File.Delete(floorToMigrate.GetSaveLocation());
|
||||||
{
|
|
||||||
JsonMarker.EType.Trap => ClientLocation.EType.Trap,
|
|
||||||
JsonMarker.EType.Hoard => ClientLocation.EType.Hoard,
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
#pragma warning restore CS0612
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <returns>Whether to archive this file once complete</returns>
|
||||||
|
private async Task MigrateFloor(
|
||||||
|
PalClientContext dbContext,
|
||||||
|
JsonFloorState floorToMigrate,
|
||||||
|
IReadOnlyDictionary<Guid, ImportHistory> imports,
|
||||||
|
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
|
||||||
|
{
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
@ -6,62 +6,61 @@ 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
|
||||||
{
|
{
|
||||||
internal sealed class Cleanup
|
private readonly ILogger<Cleanup> _logger;
|
||||||
|
private readonly IPalacePalConfiguration _configuration;
|
||||||
|
|
||||||
|
public Cleanup(ILogger<Cleanup> logger, IPalacePalConfiguration configuration)
|
||||||
{
|
{
|
||||||
private readonly ILogger<Cleanup> _logger;
|
_logger = logger;
|
||||||
private readonly IPalacePalConfiguration _configuration;
|
_configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
public Cleanup(ILogger<Cleanup> logger, IPalacePalConfiguration configuration)
|
public void Purge(PalClientContext dbContext)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
var toDelete = dbContext.Locations
|
||||||
_configuration = configuration;
|
.Include(o => o.ImportedBy)
|
||||||
}
|
.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)
|
public void Purge(PalClientContext dbContext, ETerritoryType territoryType)
|
||||||
{
|
{
|
||||||
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(DefaultPredicate())
|
.Where(o => o.TerritoryType == (ushort)territoryType)
|
||||||
.Where(AnyRemoteEncounter())
|
.Where(DefaultPredicate())
|
||||||
.ToList();
|
.Where(AnyRemoteEncounter())
|
||||||
_logger.LogInformation("Cleaning up {Count} outdated locations", toDelete.Count);
|
.ToList();
|
||||||
dbContext.Locations.RemoveRange(toDelete);
|
_logger.LogInformation("Cleaning up {Count} outdated locations for territory {Territory}", toDelete.Count,
|
||||||
}
|
territoryType);
|
||||||
|
dbContext.Locations.RemoveRange(toDelete);
|
||||||
|
}
|
||||||
|
|
||||||
public void Purge(PalClientContext dbContext, ETerritoryType territoryType)
|
private Expression<Func<ClientLocation, bool>> DefaultPredicate()
|
||||||
{
|
{
|
||||||
var toDelete = dbContext.Locations
|
return o => !o.Seen &&
|
||||||
.Include(o => o.ImportedBy)
|
o.ImportedBy.Count == 0 &&
|
||||||
.Include(o => o.RemoteEncounters)
|
o.Source != ClientLocation.ESource.SeenLocally &&
|
||||||
.AsSplitQuery()
|
o.Source != ClientLocation.ESource.ExplodedLocally;
|
||||||
.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>> DefaultPredicate()
|
private Expression<Func<ClientLocation, bool>> AnyRemoteEncounter()
|
||||||
{
|
{
|
||||||
return o => !o.Seen &&
|
if (_configuration.Mode == EMode.Offline)
|
||||||
o.ImportedBy.Count == 0 &&
|
return o => true;
|
||||||
o.Source != ClientLocation.ESource.SeenLocally &&
|
else
|
||||||
o.Source != ClientLocation.ESource.ExplodedLocally;
|
// keep downloaded markers
|
||||||
}
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,59 +1,58 @@
|
|||||||
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
|
||||||
{
|
{
|
||||||
internal sealed class ClientLocation
|
[Key] public int LocalId { get; set; }
|
||||||
|
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
|
||||||
{
|
{
|
||||||
[Key] public int LocalId { get; set; }
|
Trap = 1,
|
||||||
public ushort TerritoryType { get; set; }
|
Hoard = 2,
|
||||||
public EType Type { get; set; }
|
}
|
||||||
public float X { get; set; }
|
|
||||||
public float Y { get; set; }
|
|
||||||
public float Z { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
public enum ESource
|
||||||
/// Whether we have encountered the trap/coffer at this location in-game.
|
{
|
||||||
/// </summary>
|
Unknown = 0,
|
||||||
public bool Seen { get; set; }
|
SeenLocally = 1,
|
||||||
|
ExplodedLocally = 2,
|
||||||
/// <summary>
|
Import = 3,
|
||||||
/// Which account ids this marker was seen. This is a list merely to support different remote endpoints
|
Download = 4,
|
||||||
/// (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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
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; }
|
|
||||||
public string? RemoteUrl { get; set; }
|
|
||||||
public DateTime ExportedAt { get; set; }
|
|
||||||
public DateTime ImportedAt { get; set; }
|
|
||||||
|
|
||||||
public List<ClientLocation> ImportedLocations { get; set; } = new();
|
internal sealed class ImportHistory
|
||||||
}
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string? RemoteUrl { get; set; }
|
||||||
|
public DateTime ExportedAt { get; set; }
|
||||||
|
public DateTime ImportedAt { get; set; }
|
||||||
|
|
||||||
|
public List<ClientLocation> ImportedLocations { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,23 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Pal.Client.Database
|
namespace Pal.Client.Database;
|
||||||
|
|
||||||
|
internal class PalClientContext : DbContext
|
||||||
{
|
{
|
||||||
internal class PalClientContext : DbContext
|
public DbSet<ClientLocation> Locations { get; set; } = null!;
|
||||||
|
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!;
|
|
||||||
|
|
||||||
public PalClientContext(DbContextOptions<PalClientContext> options)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
: base(options)
|
{
|
||||||
{
|
modelBuilder.Entity<ClientLocation>()
|
||||||
}
|
.HasMany(o => o.ImportedBy)
|
||||||
|
.WithMany(o => o.ImportedLocations)
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
.UsingEntity(o => o.ToTable("LocationImports"));
|
||||||
{
|
|
||||||
modelBuilder.Entity<ClientLocation>()
|
|
||||||
.HasMany(o => o.ImportedBy)
|
|
||||||
.WithMany(o => o.ImportedLocations)
|
|
||||||
.UsingEntity(o => o.ToTable("LocationImports"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,40 +2,39 @@
|
|||||||
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>
|
||||||
/// To avoid sending too many requests to the server, we cache which locations have been seen
|
/// Partial account id. This is partially unique - however problems would (in theory)
|
||||||
/// locally. These never expire, and locations which have been seen with a specific account
|
/// only occur once you have two account-ids where the first 13 characters are equal.
|
||||||
/// 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>
|
||||||
internal sealed class RemoteEncounter
|
[MaxLength(13)]
|
||||||
|
public string AccountId { get; private set; }
|
||||||
|
|
||||||
|
private RemoteEncounter(int clientLocationId, string accountId)
|
||||||
{
|
{
|
||||||
[Key]
|
ClientLocationId = clientLocationId;
|
||||||
public int Id { get; private set; }
|
AccountId = accountId;
|
||||||
|
}
|
||||||
|
|
||||||
public int ClientLocationId { get; private set; }
|
public RemoteEncounter(ClientLocation clientLocation, string accountId)
|
||||||
public ClientLocation ClientLocation { get; private set; } = null!;
|
{
|
||||||
|
ClientLocation = clientLocation;
|
||||||
/// <summary>
|
AccountId = accountId.ToPartialId();
|
||||||
/// 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,178 +19,177 @@ 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
|
||||||
{
|
{
|
||||||
/// <summary>
|
private readonly ILogger<DependencyContextInitializer> _logger;
|
||||||
/// Takes care of async plugin init - this is mostly everything that requires either the config or the database to
|
private readonly IServiceProvider _serviceProvider;
|
||||||
/// be available.
|
|
||||||
/// </summary>
|
public DependencyContextInitializer(ILogger<DependencyContextInitializer> logger,
|
||||||
internal sealed class DependencyContextInitializer
|
IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
private readonly ILogger<DependencyContextInitializer> _logger;
|
_logger = logger;
|
||||||
private readonly IServiceProvider _serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
public DependencyContextInitializer(ILogger<DependencyContextInitializer> logger,
|
public async Task InitializeAsync(CancellationToken cancellationToken)
|
||||||
IServiceProvider serviceProvider)
|
{
|
||||||
{
|
using IDisposable? logScope = _logger.BeginScope("AsyncInit");
|
||||||
_logger = logger;
|
|
||||||
_serviceProvider = serviceProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task InitializeAsync(CancellationToken cancellationToken)
|
_logger.LogInformation("Starting async init");
|
||||||
{
|
|
||||||
using IDisposable? logScope = _logger.BeginScope("AsyncInit");
|
|
||||||
|
|
||||||
_logger.LogInformation("Starting async init");
|
await CreateBackup();
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
await CreateBackup();
|
await RunMigrations(cancellationToken);
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
await RunMigrations(cancellationToken);
|
// v1 migration: config migration for import history, json migration for markers
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
_serviceProvider.GetRequiredService<ConfigurationManager>().Migrate();
|
||||||
|
await _serviceProvider.GetRequiredService<JsonMigration>().MigrateAsync(cancellationToken);
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
// v1 migration: config migration for import history, json migration for markers
|
await RunCleanup();
|
||||||
_serviceProvider.GetRequiredService<ConfigurationManager>().Migrate();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
await _serviceProvider.GetRequiredService<JsonMigration>().MigrateAsync(cancellationToken);
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
await RunCleanup();
|
await RemoveOldBackups();
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
await RemoveOldBackups();
|
// windows that have logic to open on startup
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
_serviceProvider.GetRequiredService<AgreementWindow>();
|
||||||
|
|
||||||
// windows that have logic to open on startup
|
// initialize components that are mostly self-contained/self-registered
|
||||||
_serviceProvider.GetRequiredService<AgreementWindow>();
|
_serviceProvider.GetRequiredService<GameHooks>();
|
||||||
|
_serviceProvider.GetRequiredService<FrameworkService>();
|
||||||
|
_serviceProvider.GetRequiredService<ChatService>();
|
||||||
|
|
||||||
// initialize components that are mostly self-contained/self-registered
|
// eager load any commands to find errors now, not when running them
|
||||||
_serviceProvider.GetRequiredService<GameHooks>();
|
_serviceProvider.GetRequiredService<IEnumerable<ISubCommand>>();
|
||||||
_serviceProvider.GetRequiredService<FrameworkService>();
|
|
||||||
_serviceProvider.GetRequiredService<ChatService>();
|
|
||||||
|
|
||||||
// eager load any commands to find errors now, not when running them
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
_serviceProvider.GetRequiredService<IEnumerable<ISubCommand>>();
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
if (_serviceProvider.GetRequiredService<IPalacePalConfiguration>().HasBetaFeature(ObjectTableDebug.FeatureName))
|
||||||
|
_serviceProvider.GetRequiredService<ObjectTableDebug>();
|
||||||
|
|
||||||
if (_serviceProvider.GetRequiredService<IPalacePalConfiguration>().HasBetaFeature(ObjectTableDebug.FeatureName))
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
_serviceProvider.GetRequiredService<ObjectTableDebug>();
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
_logger.LogInformation("Async init complete");
|
||||||
|
}
|
||||||
|
|
||||||
_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>();
|
||||||
|
|
||||||
private async Task RemoveOldBackups()
|
var paths = Directory.GetFiles(pluginInterface.GetPluginConfigDirectory(), "backup-*.data.sqlite3",
|
||||||
{
|
new EnumerationOptions
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
var match = backupRegex.Match(Path.GetFileName(path));
|
IgnoreInaccessible = true,
|
||||||
if (!match.Success)
|
RecurseSubdirectories = false,
|
||||||
continue;
|
MatchCasing = MatchCasing.CaseSensitive,
|
||||||
|
AttributesToSkip = FileAttributes.ReadOnly | FileAttributes.Hidden | FileAttributes.System,
|
||||||
|
ReturnSpecialDirectories = false,
|
||||||
|
});
|
||||||
|
if (paths.Length == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
if (DateTime.TryParseExact(match.Groups[1].Value, "yyyy-MM-dd", CultureInfo.InvariantCulture,
|
Regex backupRegex = new Regex(@"backup-([\d\-]{10})\.data\.sqlite3", RegexOptions.Compiled);
|
||||||
DateTimeStyles.AssumeUniversal, out DateTime backupDate))
|
List<(DateTime Date, string Path)> backupFiles = new();
|
||||||
{
|
foreach (string path in paths)
|
||||||
backupFiles.Add((backupDate, path));
|
{
|
||||||
}
|
var match = backupRegex.Match(Path.GetFileName(path));
|
||||||
}
|
if (!match.Success)
|
||||||
|
continue;
|
||||||
|
|
||||||
var toDelete = backupFiles.OrderByDescending(x => x.Date)
|
if (DateTime.TryParseExact(match.Groups[1].Value, "yyyy-MM-dd", CultureInfo.InvariantCulture,
|
||||||
.Skip(configuration.Backups.MinimumBackupsToKeep)
|
DateTimeStyles.AssumeUniversal, out DateTime backupDate))
|
||||||
.Where(x => (DateTime.Now.ToUniversalTime() - x.Date).Days > configuration.Backups.DaysToDeleteAfter)
|
|
||||||
.Select(x => x.Path);
|
|
||||||
foreach (var path in toDelete)
|
|
||||||
{
|
{
|
||||||
try
|
backupFiles.Add((backupDate, path));
|
||||||
{
|
|
||||||
File.Delete(path);
|
|
||||||
_logger.LogInformation("Deleted old backup file '{Path}'", path);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(e, "Could not delete backup file '{Path}'", path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CreateBackup()
|
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)
|
||||||
{
|
{
|
||||||
await using var scope = _serviceProvider.CreateAsyncScope();
|
try
|
||||||
|
|
||||||
var pluginInterface = scope.ServiceProvider.GetRequiredService<DalamudPluginInterface>();
|
|
||||||
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
|
File.Delete(path);
|
||||||
{
|
_logger.LogInformation("Deleted old backup file '{Path}'", path);
|
||||||
if (File.Exists(sourcePath + "-shm") || File.Exists(sourcePath + "-wal"))
|
}
|
||||||
{
|
catch (Exception e)
|
||||||
_logger.LogInformation("Creating database backup '{Path}' (open db)", backupPath);
|
{
|
||||||
await using var db = scope.ServiceProvider.GetRequiredService<PalClientContext>();
|
_logger.LogWarning(e, "Could not delete backup file '{Path}'", path);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RunMigrations(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
await using var scope = _serviceProvider.CreateAsyncScope();
|
|
||||||
|
|
||||||
_logger.LogInformation("Loading database & running migrations");
|
|
||||||
await using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
|
|
||||||
|
|
||||||
// takes 2-3 seconds with initializing connections, loading driver etc.
|
|
||||||
await dbContext.Database.MigrateAsync(cancellationToken);
|
|
||||||
_logger.LogInformation("Completed database migrations");
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task CreateBackup()
|
||||||
|
{
|
||||||
|
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
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunMigrations(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using var scope = _serviceProvider.CreateAsyncScope();
|
||||||
|
|
||||||
|
_logger.LogInformation("Loading database & running migrations");
|
||||||
|
await using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
|
||||||
|
|
||||||
|
// takes 2-3 seconds with initializing connections, loading driver etc.
|
||||||
|
await dbContext.Database.MigrateAsync(cancellationToken);
|
||||||
|
_logger.LogInformation("Completed database migrations");
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,38 +1,38 @@
|
|||||||
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 ECommons.DalamudServices.Legacy;
|
||||||
using Pal.Client.Properties;
|
using Pal.Client.Properties;
|
||||||
|
|
||||||
namespace Pal.Client.DependencyInjection
|
namespace Pal.Client.DependencyInjection;
|
||||||
|
|
||||||
|
internal sealed class Chat
|
||||||
{
|
{
|
||||||
internal sealed class Chat
|
private readonly IChatGui _chatGui;
|
||||||
|
|
||||||
|
public Chat(IChatGui chatGui)
|
||||||
{
|
{
|
||||||
private readonly ChatGui _chatGui;
|
_chatGui = chatGui;
|
||||||
|
|
||||||
public Chat(ChatGui chatGui)
|
|
||||||
{
|
|
||||||
_chatGui = chatGui;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Error(string e)
|
|
||||||
{
|
|
||||||
_chatGui.PrintChat(new XivChatEntry
|
|
||||||
{
|
|
||||||
Message = new SeStringBuilder()
|
|
||||||
.AddUiForeground($"[{Localization.Palace_Pal}] ", 16)
|
|
||||||
.AddText(e).Build(),
|
|
||||||
Type = XivChatType.Urgent
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Message(string message)
|
|
||||||
{
|
|
||||||
_chatGui.Print(new SeStringBuilder()
|
|
||||||
.AddUiForeground($"[{Localization.Palace_Pal}] ", 57)
|
|
||||||
.AddText(message).Build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UnformattedMessage(string message)
|
|
||||||
=> _chatGui.Print(message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Error(string e)
|
||||||
|
{
|
||||||
|
_chatGui.PrintChat(new XivChatEntry
|
||||||
|
{
|
||||||
|
Message = new SeStringBuilder()
|
||||||
|
.AddUiForeground($"[{Localization.Palace_Pal}] ", 16)
|
||||||
|
.AddText(e).Build(),
|
||||||
|
Type = XivChatType.Urgent
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Message(string message)
|
||||||
|
{
|
||||||
|
_chatGui.Print(new SeStringBuilder()
|
||||||
|
.AddUiForeground($"[{Localization.Palace_Pal}] ", 57)
|
||||||
|
.AddText(message).Build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UnformattedMessage(string message)
|
||||||
|
=> _chatGui.Print(message);
|
||||||
}
|
}
|
||||||
|
@ -1,110 +1,116 @@
|
|||||||
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
|
||||||
{
|
{
|
||||||
internal sealed class ChatService : IDisposable
|
private readonly IChatGui _chatGui;
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
private readonly ChatGui _chatGui;
|
_chatGui = chatGui;
|
||||||
private readonly TerritoryState _territoryState;
|
_territoryState = territoryState;
|
||||||
private readonly IPalacePalConfiguration _configuration;
|
_configuration = configuration;
|
||||||
private readonly DataManager _dataManager;
|
_dataManager = dataManager;
|
||||||
private readonly LocalizedChatMessages _localizedChatMessages;
|
|
||||||
|
|
||||||
public ChatService(ChatGui chatGui, TerritoryState territoryState, IPalacePalConfiguration configuration,
|
_localizedChatMessages = LoadLanguageStrings();
|
||||||
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))
|
||||||
{
|
{
|
||||||
_chatGui = chatGui;
|
_territoryState.PomanderOfSight = PomanderState.Inactive;
|
||||||
_territoryState = territoryState;
|
|
||||||
_configuration = configuration;
|
|
||||||
_dataManager = dataManager;
|
|
||||||
|
|
||||||
_localizedChatMessages = LoadLanguageStrings();
|
if (_territoryState.PomanderOfIntuition == PomanderState.FoundOnCurrentFloor)
|
||||||
|
_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)
|
|
||||||
{
|
{
|
||||||
if (_configuration.FirstUse)
|
_territoryState.PomanderOfSight = PomanderState.Active;
|
||||||
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()
|
|
||||||
{
|
{
|
||||||
return new LocalizedChatMessages
|
_territoryState.PomanderOfSight = PomanderState.PomanderOfSafetyUsed;
|
||||||
{
|
|
||||||
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) ||
|
||||||
private string GetLocalizedString(uint id)
|
message.EndsWith(_localizedChatMessages.HoardOnCurrentFloor))
|
||||||
{
|
{
|
||||||
return _dataManager.GetExcelSheet<LogMessage>()?.GetRow(id)?.Text?.ToString() ?? "Unknown";
|
// 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))
|
||||||
private sealed class LocalizedChatMessages
|
|
||||||
{
|
{
|
||||||
public string MapRevealed { get; init; } = "???"; //"The map for this floor has been revealed!";
|
_territoryState.PomanderOfIntuition = PomanderState.FoundOnCurrentFloor;
|
||||||
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+)$");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Pal.Client.DependencyInjection
|
namespace Pal.Client.DependencyInjection;
|
||||||
|
|
||||||
|
internal sealed class DebugState
|
||||||
{
|
{
|
||||||
internal sealed class DebugState
|
public string? DebugMessage { get; set; }
|
||||||
{
|
|
||||||
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;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
internal sealed unsafe class GameHooks : IDisposable
|
private readonly ILogger<GameHooks> _logger;
|
||||||
{
|
private readonly IObjectTable _objectTable;
|
||||||
private readonly ILogger<GameHooks> _logger;
|
private readonly TerritoryState _territoryState;
|
||||||
private readonly ObjectTable _objectTable;
|
private readonly FrameworkService _frameworkService;
|
||||||
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, ObjectTable objectTable, TerritoryState territoryState, FrameworkService frameworkService)
|
public GameHooks(ILogger<GameHooks> logger, IObjectTable objectTable, TerritoryState territoryState, FrameworkService frameworkService, IGameInteropProvider gameInteropProvider)
|
||||||
|
{
|
||||||
|
_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
|
||||||
{
|
{
|
||||||
_logger = logger;
|
if (_territoryState.IsInDeepDungeon())
|
||||||
_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
|
|
||||||
{
|
{
|
||||||
if (_territoryState.IsInDeepDungeon())
|
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 IBattleChara bc && (bc.NameId == /* potd */ 5042 || bc.NameId == /* hoh */ 7395))
|
||||||
{
|
{
|
||||||
var vfxPath = MemoryHelper.ReadString(new nint(a1), Encoding.ASCII, 256);
|
if (vfxPath == "vfx/common/eff/dk05th_stdn0t.avfx" || vfxPath == "vfx/common/eff/dk05ht_ipws0t.avfx")
|
||||||
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))
|
|
||||||
{
|
{
|
||||||
if (vfxPath == "vfx/common/eff/dk05th_stdn0t.avfx" || vfxPath == "vfx/common/eff/dk05ht_ipws0t.avfx")
|
_logger.LogDebug("VFX '{Path}' playing at {Location}", vfxPath, obj.Position);
|
||||||
{
|
_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)
|
||||||
public void Dispose()
|
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Disposing game hooks");
|
_logger.LogError(e, "VFX Create Hook failed");
|
||||||
ActorVfxCreateHook.Dispose();
|
|
||||||
}
|
}
|
||||||
|
return ActorVfxCreateHook.Original(a1, a2, a3, a4, a5, a6, a7);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Disposing game hooks");
|
||||||
|
ActorVfxCreateHook.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,155 +12,154 @@ 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
|
||||||
{
|
{
|
||||||
internal sealed class ImportService
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly FloorService _floorService;
|
||||||
|
private readonly Cleanup _cleanup;
|
||||||
|
|
||||||
|
public ImportService(
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
|
FloorService floorService,
|
||||||
|
Cleanup cleanup)
|
||||||
{
|
{
|
||||||
private readonly IServiceProvider _serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
private readonly FloorService _floorService;
|
_floorService = floorService;
|
||||||
private readonly Cleanup _cleanup;
|
_cleanup = cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
public ImportService(
|
public async Task<ImportHistory?> FindLast(CancellationToken token = default)
|
||||||
IServiceProvider serviceProvider,
|
{
|
||||||
FloorService floorService,
|
await using var scope = _serviceProvider.CreateAsyncScope();
|
||||||
Cleanup cleanup)
|
await using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
|
||||||
|
|
||||||
|
return await dbContext.Imports.OrderByDescending(x => x.ImportedAt).ThenBy(x => x.Id)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken: token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public (int traps, int hoard) Import(ExportRoot import)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider;
|
_floorService.SetToImportState();
|
||||||
_floorService = floorService;
|
|
||||||
_cleanup = cleanup;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ImportHistory?> FindLast(CancellationToken token = default)
|
using var scope = _serviceProvider.CreateScope();
|
||||||
{
|
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
|
||||||
await using var scope = _serviceProvider.CreateAsyncScope();
|
|
||||||
await using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
|
|
||||||
|
|
||||||
return await dbContext.Imports.OrderByDescending(x => x.ImportedAt).ThenBy(x => x.Id)
|
dbContext.Imports.RemoveRange(dbContext.Imports.Where(x => x.RemoteUrl == import.ServerUrl).ToList());
|
||||||
.FirstOrDefaultAsync(cancellationToken: token);
|
dbContext.SaveChanges();
|
||||||
}
|
|
||||||
|
|
||||||
public (int traps, int hoard) Import(ExportRoot import)
|
ImportHistory importHistory = new ImportHistory
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
_floorService.SetToImportState();
|
Id = Guid.Parse(import.ExportId),
|
||||||
|
RemoteUrl = import.ServerUrl,
|
||||||
|
ExportedAt = import.CreatedAt.ToDateTime(),
|
||||||
|
ImportedAt = DateTime.UtcNow,
|
||||||
|
};
|
||||||
|
dbContext.Imports.Add(importHistory);
|
||||||
|
|
||||||
using var scope = _serviceProvider.CreateScope();
|
int traps = 0;
|
||||||
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
|
int hoard = 0;
|
||||||
|
foreach (var floor in import.Floors)
|
||||||
|
{
|
||||||
|
ETerritoryType territoryType = (ETerritoryType)floor.TerritoryType;
|
||||||
|
|
||||||
dbContext.Imports.RemoveRange(dbContext.Imports.Where(x => x.RemoteUrl == import.ServerUrl).ToList());
|
List<PersistentLocation> existingLocations = dbContext.Locations
|
||||||
dbContext.SaveChanges();
|
.Where(loc => loc.TerritoryType == floor.TerritoryType)
|
||||||
|
.ToList()
|
||||||
ImportHistory importHistory = new ImportHistory
|
.Select(LoadTerritory.ToMemoryLocation)
|
||||||
|
.ToList();
|
||||||
|
foreach (var exportLocation in floor.Objects)
|
||||||
{
|
{
|
||||||
Id = Guid.Parse(import.ExportId),
|
PersistentLocation persistentLocation = new PersistentLocation
|
||||||
RemoteUrl = import.ServerUrl,
|
|
||||||
ExportedAt = import.CreatedAt.ToDateTime(),
|
|
||||||
ImportedAt = DateTime.UtcNow,
|
|
||||||
};
|
|
||||||
dbContext.Imports.Add(importHistory);
|
|
||||||
|
|
||||||
int traps = 0;
|
|
||||||
int hoard = 0;
|
|
||||||
foreach (var floor in import.Floors)
|
|
||||||
{
|
|
||||||
ETerritoryType territoryType = (ETerritoryType)floor.TerritoryType;
|
|
||||||
|
|
||||||
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),
|
||||||
Type = ToMemoryType(exportLocation.Type),
|
Source = ClientLocation.ESource.Unknown,
|
||||||
Position = new Vector3(exportLocation.X, exportLocation.Y, exportLocation.Z),
|
};
|
||||||
Source = ClientLocation.ESource.Unknown,
|
|
||||||
};
|
|
||||||
|
|
||||||
var existingLocation = existingLocations.FirstOrDefault(x => x == persistentLocation);
|
var existingLocation = existingLocations.FirstOrDefault(x => x == persistentLocation);
|
||||||
if (existingLocation != null)
|
if (existingLocation != null)
|
||||||
{
|
{
|
||||||
var clientLoc = dbContext.Locations.FirstOrDefault(o => o.LocalId == existingLocation.LocalId);
|
var clientLoc = dbContext.Locations.FirstOrDefault(o => o.LocalId == existingLocation.LocalId);
|
||||||
clientLoc?.ImportedBy.Add(importHistory);
|
clientLoc?.ImportedBy.Add(importHistory);
|
||||||
|
|
||||||
continue;
|
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)
|
|
||||||
{
|
{
|
||||||
return exportLocationType switch
|
_floorService.ResetAll();
|
||||||
{
|
|
||||||
ExportObjectType.Trap => MemoryLocation.EType.Trap,
|
|
||||||
ExportObjectType.Hoard => MemoryLocation.EType.Hoard,
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(exportLocationType), exportLocationType, null)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private ClientLocation.EType ToClientLocationType(ExportObjectType exportLocationType)
|
private MemoryLocation.EType ToMemoryType(ExportObjectType exportLocationType)
|
||||||
|
{
|
||||||
|
return exportLocationType switch
|
||||||
{
|
{
|
||||||
return exportLocationType switch
|
ExportObjectType.Trap => MemoryLocation.EType.Trap,
|
||||||
{
|
ExportObjectType.Hoard => MemoryLocation.EType.Hoard,
|
||||||
ExportObjectType.Trap => ClientLocation.EType.Trap,
|
_ => throw new ArgumentOutOfRangeException(nameof(exportLocationType), exportLocationType, null)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
try
|
_floorService.ResetAll();
|
||||||
{
|
|
||||||
_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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,25 +6,22 @@ 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, DalamudPluginInterface pluginInterface, Chat chat)
|
|
||||||
{
|
|
||||||
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
|
internal sealed class RepoVerification
|
||||||
|
{
|
||||||
|
public RepoVerification(ILogger<RepoVerification> logger, IDalamudPluginInterface pluginInterface, Chat chat)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Install source: {Repo}", pluginInterface.SourceRepository);
|
||||||
|
if (!pluginInterface.IsDev && pluginInterface.SourceRepository.TrimEnd('/') != "https://plugins.carvel.li")
|
||||||
{
|
{
|
||||||
|
chat.Error(string.Format(Localization.Error_WrongRepository,
|
||||||
|
"https://plugins.carvel.li"));
|
||||||
|
throw new RepoVerificationFailedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal sealed class RepoVerificationFailedException : Exception
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,67 +9,66 @@ 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
|
||||||
{
|
{
|
||||||
internal sealed class StatisticsService
|
private readonly IPalacePalConfiguration _configuration;
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
private readonly IPalacePalConfiguration _configuration;
|
_configuration = configuration;
|
||||||
private readonly ILogger<StatisticsService> _logger;
|
_logger = logger;
|
||||||
private readonly RemoteApi _remoteApi;
|
_remoteApi = remoteApi;
|
||||||
private readonly StatisticsWindow _statisticsWindow;
|
_statisticsWindow = statisticsWindow;
|
||||||
private readonly Chat _chat;
|
_chat = chat;
|
||||||
|
}
|
||||||
|
|
||||||
public StatisticsService(
|
public void ShowGlobalStatistics()
|
||||||
IPalacePalConfiguration configuration,
|
{
|
||||||
ILogger<StatisticsService> logger,
|
Task.Run(async () => await FetchFloorStatistics());
|
||||||
RemoteApi remoteApi,
|
}
|
||||||
StatisticsWindow statisticsWindow,
|
|
||||||
Chat chat)
|
|
||||||
{
|
|
||||||
_configuration = configuration;
|
|
||||||
_logger = logger;
|
|
||||||
_remoteApi = remoteApi;
|
|
||||||
_statisticsWindow = statisticsWindow;
|
|
||||||
_chat = chat;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ShowGlobalStatistics()
|
private async Task FetchFloorStatistics()
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
Task.Run(async () => await FetchFloorStatistics());
|
if (!_configuration.HasRoleOnCurrentServer(RemoteApi.RemoteUrl, "statistics:view"))
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
||||||
{
|
{
|
||||||
_logger.LogError(e, "Could not fetch floor statistics");
|
_statisticsWindow.SetFloorData(floorStatistics);
|
||||||
_chat.Error(string.Format(Localization.Error_CommandFailed,
|
_statisticsWindow.IsOpen = true;
|
||||||
$"{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}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ 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;
|
||||||
@ -25,166 +26,169 @@ 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>
|
||||||
/// DI-aware Plugin.
|
/// Initialized as temporary logger, will be overriden once context is ready with a logger that supports scopes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class DependencyInjectionContext : IDisposable
|
private ILogger _logger;
|
||||||
|
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
public const string DatabaseFileName = "palace-pal.data.sqlite3";
|
var loggerProvider = new DalamudLoggerProvider(pluginLog);
|
||||||
public static DalamudLoggerProvider LoggerProvider { get; } = new(typeof(Plugin).Assembly);
|
_logger = loggerProvider.CreateLogger<DependencyInjectionContext>();
|
||||||
|
_logger.LogInformation("Building dalamud service container for {Assembly}",
|
||||||
|
typeof(DependencyInjectionContext).Assembly.FullName);
|
||||||
|
|
||||||
/// <summary>
|
// set up legacy services
|
||||||
/// 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(plugin));
|
.AddDalamudLogger(pluginLog));
|
||||||
|
|
||||||
// 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(new WindowSystem(typeof(DependencyInjectionContext).AssemblyQualifiedName));
|
_serviceCollection.AddSingleton(gameInteropProvider);
|
||||||
|
_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
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Building async service container for {Assembly}",
|
ValidateOnBuild = true,
|
||||||
typeof(DependencyInjectionContext).Assembly.FullName);
|
ValidateScopes = true,
|
||||||
|
});
|
||||||
// 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);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
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)
|
|
||||||
=> g.ToString().ToPartialId();
|
|
||||||
|
|
||||||
public static string ToPartialId(this string s, int length = 13)
|
public static class GuidExtensions
|
||||||
=> s.PadRight(length + 1).Substring(0, length);
|
{
|
||||||
}
|
public static string ToPartialId(this Guid g, int length = 13)
|
||||||
|
=> g.ToString().ToPartialId();
|
||||||
|
|
||||||
|
public static string ToPartialId(this string s, int length = 13)
|
||||||
|
=> s.PadRight(length + 1).Substring(0, length);
|
||||||
}
|
}
|
||||||
|
@ -3,34 +3,33 @@ 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
|
||||||
{
|
{
|
||||||
internal static class PalImGui
|
/// <summary>
|
||||||
|
/// 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)
|
||||||
{
|
{
|
||||||
/// <summary>
|
int labelLength = Encoding.UTF8.GetByteCount(label);
|
||||||
/// None of the default BeginTabItem methods allow using flags without making the tab have a close button for some reason.
|
byte* labelPtr = stackalloc byte[labelLength + 1];
|
||||||
/// </summary>
|
byte[] labelBytes = Encoding.UTF8.GetBytes(label);
|
||||||
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;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,29 +1,28 @@
|
|||||||
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
|
||||||
{
|
{
|
||||||
/// <summary>
|
public override bool Equals(object? obj) => obj is EphemeralLocation && base.Equals(obj);
|
||||||
/// This is a currently-visible marker.
|
|
||||||
/// </summary>
|
public override int GetHashCode() => base.GetHashCode();
|
||||||
internal sealed class EphemeralLocation : MemoryLocation
|
|
||||||
|
public static bool operator ==(EphemeralLocation? a, object? b)
|
||||||
{
|
{
|
||||||
public override bool Equals(object? obj) => obj is EphemeralLocation && base.Equals(obj);
|
return Equals(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
public override int GetHashCode() => base.GetHashCode();
|
public static bool operator !=(EphemeralLocation? a, object? b)
|
||||||
|
{
|
||||||
|
return !Equals(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
public static bool operator ==(EphemeralLocation? a, object? b)
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return Equals(a, b);
|
return $"EphemeralLocation(Position={Position}, Type={Type})";
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator !=(EphemeralLocation? a, object? b)
|
|
||||||
{
|
|
||||||
return !Equals(a, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"EphemeralLocation(Position={Position}, Type={Type})";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,154 +10,153 @@ 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
|
||||||
{
|
{
|
||||||
internal sealed class FloorService
|
private readonly IPalacePalConfiguration _configuration;
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
private readonly IPalacePalConfiguration _configuration;
|
_configuration = configuration;
|
||||||
private readonly Cleanup _cleanup;
|
_cleanup = cleanup;
|
||||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
_serviceScopeFactory = serviceScopeFactory;
|
||||||
private readonly IReadOnlyDictionary<ETerritoryType, MemoryTerritory> _territories;
|
_territories = Enum.GetValues<ETerritoryType>().ToDictionary(o => o, o => new MemoryTerritory(o));
|
||||||
|
}
|
||||||
|
|
||||||
private ConcurrentBag<EphemeralLocation> _ephemeralLocations = new();
|
public IReadOnlyCollection<EphemeralLocation> EphemeralLocations => _ephemeralLocations;
|
||||||
|
public bool IsImportRunning { get; private set; }
|
||||||
|
|
||||||
public FloorService(IPalacePalConfiguration configuration, Cleanup cleanup,
|
public void ChangeTerritory(ushort territoryType)
|
||||||
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)
|
||||||
{
|
{
|
||||||
_configuration = configuration;
|
territory.ReadyState = MemoryTerritory.EReadyState.Loading;
|
||||||
_cleanup = cleanup;
|
new LoadTerritory(_serviceScopeFactory, _cleanup, territory).Start();
|
||||||
_serviceScopeFactory = serviceScopeFactory;
|
|
||||||
_territories = Enum.GetValues<ETerritoryType>().ToDictionary(o => o, o => new MemoryTerritory(o));
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public IReadOnlyCollection<EphemeralLocation> EphemeralLocations => _ephemeralLocations;
|
public MemoryTerritory? GetTerritoryIfReady(ushort territoryType)
|
||||||
public bool IsImportRunning { get; private set; }
|
{
|
||||||
|
if (typeof(ETerritoryType).IsEnumDefined(territoryType))
|
||||||
|
return GetTerritoryIfReady((ETerritoryType)territoryType);
|
||||||
|
|
||||||
public void ChangeTerritory(ushort territoryType)
|
return null;
|
||||||
{
|
}
|
||||||
_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;
|
|
||||||
new LoadTerritory(_serviceScopeFactory, _cleanup, territory).Start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public MemoryTerritory? GetTerritoryIfReady(ushort territoryType)
|
|
||||||
{
|
|
||||||
if (typeof(ETerritoryType).IsEnumDefined(territoryType))
|
|
||||||
return GetTerritoryIfReady((ETerritoryType)territoryType);
|
|
||||||
|
|
||||||
|
public MemoryTerritory? GetTerritoryIfReady(ETerritoryType territoryType)
|
||||||
|
{
|
||||||
|
var territory = _territories[territoryType];
|
||||||
|
if (territory.ReadyState != MemoryTerritory.EReadyState.Ready)
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
public MemoryTerritory? GetTerritoryIfReady(ETerritoryType territoryType)
|
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)
|
||||||
{
|
{
|
||||||
var territory = _territories[territoryType];
|
PersistentLocation? existingLocation = persistentLocations.SingleOrDefault(x => x == visibleLocation);
|
||||||
if (territory.ReadyState != MemoryTerritory.EReadyState.Ready)
|
if (existingLocation != null)
|
||||||
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 is { Seen: false, LocalId: { } })
|
||||||
if (existingLocation != null)
|
|
||||||
{
|
{
|
||||||
if (existingLocation is { Seen: false, LocalId: { } })
|
existingLocation.Seen = true;
|
||||||
{
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
territory.Locations.Add(visibleLocation);
|
// This requires you to have seen a trap/hoard marker once per floor to synchronize this for older local states,
|
||||||
newLocations.Add(visibleLocation);
|
// markers discovered afterwards are automatically marked seen.
|
||||||
recreateLayout = true;
|
if (partialAccountId != null &&
|
||||||
|
existingLocation is { LocalId: { }, NetworkId: { }, RemoteSeenRequested: false } &&
|
||||||
|
!existingLocation.RemoteSeenOn.Contains(partialAccountId))
|
||||||
|
{
|
||||||
|
existingLocation.RemoteSeenRequested = true;
|
||||||
|
locationsToSync.Add(existingLocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (markAsSeen.Count > 0)
|
territory.Locations.Add(visibleLocation);
|
||||||
new MarkLocalSeen(_serviceScopeFactory, territory, markAsSeen).Start();
|
newLocations.Add(visibleLocation);
|
||||||
|
recreateLayout = true;
|
||||||
if (newLocations.Count > 0)
|
|
||||||
new SaveNewLocations(_serviceScopeFactory, territory, newLocations).Start();
|
|
||||||
|
|
||||||
return recreateLayout;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <returns>Whether the locations have changed</returns>
|
if (markAsSeen.Count > 0)
|
||||||
public bool MergeEphemeralLocations(IReadOnlyList<EphemeralLocation> visibleLocations, bool recreate)
|
new MarkLocalSeen(_serviceScopeFactory, territory, markAsSeen).Start();
|
||||||
|
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
recreate |= _ephemeralLocations.Any(loc => visibleLocations.All(x => x != loc));
|
lock (memoryTerritory.LockObj)
|
||||||
recreate |= visibleLocations.Any(loc => _ephemeralLocations.All(x => x != loc));
|
memoryTerritory.Reset();
|
||||||
|
|
||||||
if (!recreate)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
_ephemeralLocations.Clear();
|
|
||||||
foreach (var visibleLocation in visibleLocations)
|
|
||||||
_ephemeralLocations.Add(visibleLocation);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void ResetAll()
|
public void SetToImportState()
|
||||||
|
{
|
||||||
|
IsImportRunning = true;
|
||||||
|
foreach (var memoryTerritory in _territories.Values)
|
||||||
{
|
{
|
||||||
IsImportRunning = false;
|
lock (memoryTerritory.LockObj)
|
||||||
foreach (var memoryTerritory in _territories.Values)
|
memoryTerritory.ReadyState = MemoryTerritory.EReadyState.Importing;
|
||||||
{
|
|
||||||
lock (memoryTerritory.LockObj)
|
|
||||||
memoryTerritory.Reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetToImportState()
|
|
||||||
{
|
|
||||||
IsImportRunning = true;
|
|
||||||
foreach (var memoryTerritory in _territories.Values)
|
|
||||||
{
|
|
||||||
lock (memoryTerritory.LockObj)
|
|
||||||
memoryTerritory.ReadyState = MemoryTerritory.EReadyState.Importing;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,8 @@ 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;
|
||||||
@ -18,451 +16,449 @@ 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
|
||||||
{
|
{
|
||||||
internal sealed class FrameworkService : IDisposable
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
private readonly IServiceProvider _serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
private readonly ILogger<FrameworkService> _logger;
|
_logger = logger;
|
||||||
private readonly Framework _framework;
|
_framework = framework;
|
||||||
private readonly ConfigurationManager _configurationManager;
|
_configurationManager = configurationManager;
|
||||||
private readonly IPalacePalConfiguration _configuration;
|
_configuration = configuration;
|
||||||
private readonly ClientState _clientState;
|
_clientState = clientState;
|
||||||
private readonly TerritoryState _territoryState;
|
_territoryState = territoryState;
|
||||||
private readonly FloorService _floorService;
|
_floorService = floorService;
|
||||||
private readonly DebugState _debugState;
|
_debugState = debugState;
|
||||||
private readonly RenderAdapter _renderAdapter;
|
_renderAdapter = renderAdapter;
|
||||||
private readonly ObjectTable _objectTable;
|
_objectTable = objectTable;
|
||||||
private readonly RemoteApi _remoteApi;
|
_remoteApi = remoteApi;
|
||||||
|
|
||||||
internal Queue<IQueueOnFrameworkThread> EarlyEventQueue { get; } = new();
|
_framework.Update += OnUpdate;
|
||||||
internal Queue<IQueueOnFrameworkThread> LateEventQueue { get; } = new();
|
_configurationManager.Saved += OnSaved;
|
||||||
internal ConcurrentQueue<nint> NextUpdateObjects { get; } = new();
|
}
|
||||||
|
|
||||||
public FrameworkService(
|
public void Dispose()
|
||||||
IServiceProvider serviceProvider,
|
{
|
||||||
ILogger<FrameworkService> logger,
|
_framework.Update -= OnUpdate;
|
||||||
Framework framework,
|
_configurationManager.Saved -= OnSaved;
|
||||||
ConfigurationManager configurationManager,
|
}
|
||||||
IPalacePalConfiguration configuration,
|
|
||||||
ClientState clientState,
|
private void OnSaved(object? sender, IPalacePalConfiguration? config)
|
||||||
TerritoryState territoryState,
|
=> EarlyEventQueue.Enqueue(new QueuedConfigUpdate());
|
||||||
FloorService floorService,
|
|
||||||
DebugState debugState,
|
private void OnUpdate(IFramework framework)
|
||||||
RenderAdapter renderAdapter,
|
{
|
||||||
ObjectTable objectTable,
|
if (_configuration.FirstUse)
|
||||||
RemoteApi remoteApi)
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider;
|
bool recreateLayout = false;
|
||||||
_logger = logger;
|
|
||||||
_framework = framework;
|
|
||||||
_configurationManager = configurationManager;
|
|
||||||
_configuration = configuration;
|
|
||||||
_clientState = clientState;
|
|
||||||
_territoryState = territoryState;
|
|
||||||
_floorService = floorService;
|
|
||||||
_debugState = debugState;
|
|
||||||
_renderAdapter = renderAdapter;
|
|
||||||
_objectTable = objectTable;
|
|
||||||
_remoteApi = remoteApi;
|
|
||||||
|
|
||||||
_framework.Update += OnUpdate;
|
while (EarlyEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued))
|
||||||
_configurationManager.Saved += OnSaved;
|
HandleQueued(queued, ref recreateLayout);
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
if (_territoryState.LastTerritory != _clientState.TerritoryType)
|
||||||
{
|
{
|
||||||
_framework.Update -= OnUpdate;
|
MemoryTerritory? oldTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory);
|
||||||
_configurationManager.Saved -= OnSaved;
|
if (oldTerritory != null)
|
||||||
}
|
oldTerritory.SyncState = ESyncState.NotAttempted;
|
||||||
|
|
||||||
private void OnSaved(object? sender, IPalacePalConfiguration? config)
|
_territoryState.LastTerritory = _clientState.TerritoryType;
|
||||||
=> EarlyEventQueue.Enqueue(new QueuedConfigUpdate());
|
NextUpdateObjects.Clear();
|
||||||
|
|
||||||
private void OnUpdate(Framework framework)
|
_floorService.ChangeTerritory(_territoryState.LastTerritory);
|
||||||
{
|
_territoryState.PomanderOfSight = PomanderState.Inactive;
|
||||||
if (_configuration.FirstUse)
|
_territoryState.PomanderOfIntuition = PomanderState.Inactive;
|
||||||
|
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
|
||||||
{
|
{
|
||||||
bool recreateLayout = false;
|
foreach (var location in memoryTerritory.Locations)
|
||||||
|
|
||||||
while (EarlyEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued))
|
|
||||||
HandleQueued(queued, ref recreateLayout);
|
|
||||||
|
|
||||||
if (_territoryState.LastTerritory != _clientState.TerritoryType)
|
|
||||||
{
|
{
|
||||||
MemoryTerritory? oldTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory);
|
bool isEnabled = DetermineVisibility(location, visibleLocations);
|
||||||
if (oldTerritory != null)
|
if (location.RenderElement == null)
|
||||||
oldTerritory.SyncState = ESyncState.NotAttempted;
|
|
||||||
|
|
||||||
_territoryState.LastTerritory = _clientState.TerritoryType;
|
|
||||||
NextUpdateObjects.Clear();
|
|
||||||
|
|
||||||
_floorService.ChangeTerritory(_territoryState.LastTerritory);
|
|
||||||
_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)
|
|
||||||
{
|
|
||||||
_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
|
|
||||||
{
|
|
||||||
foreach (var location in memoryTerritory.Locations)
|
|
||||||
{
|
{
|
||||||
uint desiredColor = DetermineColor(location, visibleLocations);
|
if (isEnabled)
|
||||||
if (location.RenderElement == null || !location.RenderElement.IsValid)
|
|
||||||
return true;
|
return true;
|
||||||
|
else
|
||||||
if (location.RenderElement.Color != desiredColor)
|
continue;
|
||||||
location.RenderElement.Color = desiredColor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!location.RenderElement.IsValid)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (location.RenderElement.Enabled != isEnabled)
|
||||||
|
location.RenderElement.Enabled = isEnabled;
|
||||||
}
|
}
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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, DetermineColor(location, visibleMarkers),
|
|
||||||
_configuration.DeepDungeons.Traps);
|
|
||||||
}
|
|
||||||
else if (location.Type == MemoryLocation.EType.Hoard)
|
|
||||||
{
|
|
||||||
CreateRenderElement(location, elements, DetermineColor(location, visibleMarkers),
|
|
||||||
_configuration.DeepDungeons.HoardCoffers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private uint DetermineColor(EphemeralLocation location)
|
|
||||||
{
|
|
||||||
return location.Type switch
|
|
||||||
{
|
|
||||||
MemoryLocation.EType.SilverCoffer => _configuration.DeepDungeons.SilverCoffers.Color,
|
|
||||||
MemoryLocation.EType.GoldCoffer => _configuration.DeepDungeons.GoldCoffers.Color,
|
|
||||||
_ => RenderData.ColorInvisible
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CreateRenderElement(MemoryLocation location, List<IRenderElement> elements, uint color,
|
|
||||||
MarkerConfiguration config)
|
|
||||||
{
|
|
||||||
if (!config.Show)
|
|
||||||
return;
|
|
||||||
|
|
||||||
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
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Downloading territory {Territory} from server", (ETerritoryType)territoryId);
|
|
||||||
var (success, downloadedMarkers) = await _remoteApi.DownloadRemoteMarkers(territoryId);
|
|
||||||
LateEventQueue.Enqueue(new QueuedSyncResponse
|
|
||||||
{
|
|
||||||
Type = SyncType.Download,
|
|
||||||
TerritoryType = territoryId,
|
|
||||||
Success = success,
|
|
||||||
Locations = downloadedMarkers
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_debugState.SetFromException(e);
|
_debugState.SetFromException(e);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UploadLocationsForTerritory(ushort territoryId, List<PersistentLocation> locationsToUpload)
|
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)
|
||||||
{
|
{
|
||||||
try
|
foreach (var location in locationsToUpload)
|
||||||
|
location.UploadRequested = true;
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
await UploadLocationsForTerritory(_territoryState.LastTerritory, locationsToUpload));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Uploading {Count} locations for territory {Territory} to server",
|
CreateRenderElement(location, elements, DetermineVisibility(location, visibleMarkers),
|
||||||
locationsToUpload.Count, (ETerritoryType)territoryId);
|
_configuration.DeepDungeons.Traps);
|
||||||
var (success, uploadedLocations) = await _remoteApi.UploadLocations(territoryId, locationsToUpload);
|
|
||||||
LateEventQueue.Enqueue(new QueuedSyncResponse
|
|
||||||
{
|
|
||||||
Type = SyncType.Upload,
|
|
||||||
TerritoryType = territoryId,
|
|
||||||
Success = success,
|
|
||||||
Locations = uploadedLocations
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
else if (location.Type == MemoryLocation.EType.Hoard)
|
||||||
{
|
{
|
||||||
_debugState.SetFromException(e);
|
CreateRenderElement(location, elements, DetermineVisibility(location, visibleMarkers),
|
||||||
|
_configuration.DeepDungeons.HoardCoffers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SyncSeenMarkersForTerritory(ushort territoryId,
|
if (elements.Count == 0)
|
||||||
IReadOnlyList<PersistentLocation> locationsToUpdate)
|
return;
|
||||||
|
|
||||||
|
_renderAdapter.SetLayer(ELayer.TrapHoard, elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecreateEphemeralLayout()
|
||||||
|
{
|
||||||
|
_renderAdapter.ResetLayer(ELayer.RegularCoffers);
|
||||||
|
|
||||||
|
List<IRenderElement> elements = new();
|
||||||
|
foreach (var location in _floorService.EphemeralLocations)
|
||||||
{
|
{
|
||||||
try
|
if (location.Type == MemoryLocation.EType.SilverCoffer &&
|
||||||
|
_configuration.DeepDungeons.SilverCoffers.Show)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Syncing {Count} seen locations for territory {Territory} to server",
|
CreateRenderElement(location, elements, true, _configuration.DeepDungeons.SilverCoffers);
|
||||||
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)
|
else if (location.Type == MemoryLocation.EType.GoldCoffer &&
|
||||||
|
_configuration.DeepDungeons.GoldCoffers.Show)
|
||||||
{
|
{
|
||||||
_debugState.SetFromException(e);
|
CreateRenderElement(location, elements, true, _configuration.DeepDungeons.GoldCoffers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
if (elements.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
private (IReadOnlyList<PersistentLocation>, IReadOnlyList<EphemeralLocation>) GetRelevantGameObjects()
|
_renderAdapter.SetLayer(ELayer.RegularCoffers, elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool DetermineVisibility(PersistentLocation location, IReadOnlyList<PersistentLocation> visibleLocations)
|
||||||
|
{
|
||||||
|
switch (location.Type)
|
||||||
{
|
{
|
||||||
List<PersistentLocation> persistentLocations = new();
|
case MemoryLocation.EType.Trap
|
||||||
List<EphemeralLocation> ephemeralLocations = new();
|
when _territoryState.PomanderOfSight == PomanderState.Inactive ||
|
||||||
for (int i = 246; i < _objectTable.Length; i++)
|
!_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
|
||||||
{
|
{
|
||||||
GameObject? obj = _objectTable[i];
|
Type = SyncType.Download,
|
||||||
if (obj == null)
|
TerritoryType = territoryId,
|
||||||
continue;
|
Success = success,
|
||||||
|
Locations = downloadedMarkers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_debugState.SetFromException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch ((uint)Marshal.ReadInt32(obj.Address + 128))
|
private async Task UploadLocationsForTerritory(ushort territoryId, List<PersistentLocation> locationsToUpload)
|
||||||
{
|
{
|
||||||
case 2007182:
|
try
|
||||||
case 2007183:
|
{
|
||||||
case 2007184:
|
_logger.LogInformation("Uploading {Count} locations for territory {Territory} to server",
|
||||||
case 2007185:
|
locationsToUpload.Count, (ETerritoryType)territoryId);
|
||||||
case 2007186:
|
var (success, uploadedLocations) = await _remoteApi.UploadLocations(territoryId, locationsToUpload);
|
||||||
case 2009504:
|
LateEventQueue.Enqueue(new QueuedSyncResponse
|
||||||
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);
|
Type = SyncType.Upload,
|
||||||
if (obj != null && obj.Position.Length() > 0.1)
|
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++)
|
||||||
|
{
|
||||||
|
IGameObject? 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
|
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.ExplodedLocally,
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (persistentLocations, ephemeralLocations);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleQueued(IQueueOnFrameworkThread queued, ref bool recreateLayout)
|
while (NextUpdateObjects.TryDequeue(out nint address))
|
||||||
{
|
{
|
||||||
Type handlerType = typeof(IQueueOnFrameworkThread.Handler<>).MakeGenericType(queued.GetType());
|
var obj = _objectTable.FirstOrDefault(x => x.Address == address);
|
||||||
var handler = (IQueueOnFrameworkThread.IHandler)_serviceProvider.GetRequiredService(handlerType);
|
if (obj != null && obj.Position.Length() > 0.1)
|
||||||
|
{
|
||||||
handler.RunIfCompatible(queued, ref recreateLayout);
|
persistentLocations.Add(new PersistentLocation
|
||||||
|
{
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,63 +5,62 @@ 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
|
||||||
{
|
{
|
||||||
/// <summary>
|
public required EType Type { get; init; }
|
||||||
/// Base class for <see cref="MemoryLocation"/> and <see cref="EphemeralLocation"/>.
|
public required Vector3 Position { get; init; }
|
||||||
/// </summary>
|
public bool Seen { get; set; }
|
||||||
internal abstract class MemoryLocation
|
|
||||||
|
public IRenderElement? RenderElement { get; set; }
|
||||||
|
|
||||||
|
public enum EType
|
||||||
{
|
{
|
||||||
public required EType Type { get; init; }
|
Unknown,
|
||||||
public required Vector3 Position { get; init; }
|
|
||||||
public bool Seen { get; set; }
|
|
||||||
|
|
||||||
public IRenderElement? RenderElement { get; set; }
|
Trap,
|
||||||
|
Hoard,
|
||||||
|
|
||||||
public enum EType
|
SilverCoffer,
|
||||||
{
|
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static class ETypeExtensions
|
public override bool Equals(object? obj)
|
||||||
{
|
{
|
||||||
public static MemoryLocation.EType ToMemoryType(this ObjectType objectType)
|
return obj is MemoryLocation otherLocation &&
|
||||||
{
|
Type == otherLocation.Type &&
|
||||||
return objectType switch
|
PalaceMath.IsNearlySamePosition(Position, otherLocation.Position);
|
||||||
{
|
}
|
||||||
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)
|
public override int GetHashCode()
|
||||||
{
|
{
|
||||||
return type switch
|
return HashCode.Combine(Type, PalaceMath.GetHashCode(Position));
|
||||||
{
|
}
|
||||||
MemoryLocation.EType.Trap => ObjectType.Trap,
|
}
|
||||||
MemoryLocation.EType.Hoard => ObjectType.Hoard,
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
|
internal static class ETypeExtensions
|
||||||
};
|
{
|
||||||
}
|
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)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,59 +5,58 @@ 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
|
||||||
{
|
{
|
||||||
/// <summary>
|
public MemoryTerritory(ETerritoryType territoryType)
|
||||||
/// 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)
|
TerritoryType = 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,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,101 +1,99 @@
|
|||||||
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
|
||||||
{
|
{
|
||||||
/// <summary>
|
public const string FeatureName = nameof(ObjectTableDebug);
|
||||||
/// This isn't very useful for running deep dungeons normally, but it is for plugin dev.
|
|
||||||
///
|
private readonly IDalamudPluginInterface _pluginInterface;
|
||||||
/// Needs the corresponding beta feature to be enabled.
|
private readonly IObjectTable _objectTable;
|
||||||
/// </summary>
|
private readonly IGameGui _gameGui;
|
||||||
internal sealed class ObjectTableDebug : IDisposable
|
private readonly IClientState _clientState;
|
||||||
|
|
||||||
|
public ObjectTableDebug(IDalamudPluginInterface pluginInterface, IObjectTable objectTable, IGameGui gameGui,
|
||||||
|
IClientState clientState)
|
||||||
{
|
{
|
||||||
public const string FeatureName = nameof(ObjectTableDebug);
|
_pluginInterface = pluginInterface;
|
||||||
|
_objectTable = objectTable;
|
||||||
|
_gameGui = gameGui;
|
||||||
|
_clientState = clientState;
|
||||||
|
|
||||||
private readonly DalamudPluginInterface _pluginInterface;
|
_pluginInterface.UiBuilder.Draw += Draw;
|
||||||
private readonly ObjectTable _objectTable;
|
}
|
||||||
private readonly GameGui _gameGui;
|
|
||||||
private readonly ClientState _clientState;
|
|
||||||
|
|
||||||
public ObjectTableDebug(DalamudPluginInterface pluginInterface, ObjectTable objectTable, GameGui gameGui, ClientState clientState)
|
private void Draw()
|
||||||
|
{
|
||||||
|
int index = 0;
|
||||||
|
foreach (IGameObject obj in _objectTable)
|
||||||
{
|
{
|
||||||
_pluginInterface = pluginInterface;
|
if (obj is IEventObj eventObj && string.IsNullOrEmpty(eventObj.Name.ToString()))
|
||||||
_objectTable = objectTable;
|
|
||||||
_gameGui = gameGui;
|
|
||||||
_clientState = clientState;
|
|
||||||
|
|
||||||
_pluginInterface.UiBuilder.Draw += Draw;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Draw()
|
|
||||||
{
|
|
||||||
int index = 0;
|
|
||||||
foreach (GameObject obj in _objectTable)
|
|
||||||
{
|
{
|
||||||
if (obj is EventObj eventObj && string.IsNullOrEmpty(eventObj.Name.ToString()))
|
++index;
|
||||||
|
int model = Marshal.ReadInt32(obj.Address + 128);
|
||||||
|
|
||||||
|
if (_gameGui.WorldToScreen(obj.Position, out var screenCoords))
|
||||||
{
|
{
|
||||||
++index;
|
// So, while WorldToScreen will return false if the point is off of game client screen, to
|
||||||
int model = Marshal.ReadInt32(obj.Address + 128);
|
// 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.EntityId:X}[{index}]\nkind: {obj.ObjectKind} sub: {obj.SubKind}\nmodel: {model}\nname: {obj.Name}\ndata id: {obj.DataId}";
|
||||||
|
|
||||||
if (_gameGui.WorldToScreen(obj.Position, out var screenCoords))
|
var screenPos = ImGui.GetMainViewport().Pos;
|
||||||
{
|
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 screenPos = ImGui.GetMainViewport().Pos;
|
var windowSize = ImGui.CalcTextSize(objectText);
|
||||||
var screenSize = ImGui.GetMainViewport().Size;
|
|
||||||
|
|
||||||
var windowSize = ImGui.CalcTextSize(objectText);
|
// Add some extra safety padding
|
||||||
|
windowSize.X += ImGui.GetStyle().WindowPadding.X + 10;
|
||||||
|
windowSize.Y += ImGui.GetStyle().WindowPadding.Y + 10;
|
||||||
|
|
||||||
// Add some extra safety padding
|
if (screenCoords.X + windowSize.X > screenPos.X + screenSize.X ||
|
||||||
windowSize.X += ImGui.GetStyle().WindowPadding.X + 10;
|
screenCoords.Y + windowSize.Y > screenPos.Y + screenSize.Y)
|
||||||
windowSize.Y += ImGui.GetStyle().WindowPadding.Y + 10;
|
continue;
|
||||||
|
|
||||||
if (screenCoords.X + windowSize.X > screenPos.X + screenSize.X ||
|
if (distance > 50f)
|
||||||
screenCoords.Y + windowSize.Y > screenPos.Y + screenSize.Y)
|
continue;
|
||||||
continue;
|
|
||||||
|
|
||||||
if (distance > 50f)
|
ImGui.SetNextWindowPos(new Vector2(screenCoords.X, screenCoords.Y));
|
||||||
continue;
|
|
||||||
|
|
||||||
ImGui.SetNextWindowPos(new Vector2(screenCoords.X, screenCoords.Y));
|
ImGui.SetNextWindowBgAlpha(Math.Max(1f - (distance / 50f), 0.2f));
|
||||||
|
if (ImGui.Begin(
|
||||||
ImGui.SetNextWindowBgAlpha(Math.Max(1f - (distance / 50f), 0.2f));
|
$"PalacePal_{nameof(ObjectTableDebug)}_{index}",
|
||||||
if (ImGui.Begin(
|
ImGuiWindowFlags.NoDecoration |
|
||||||
$"PalacePal_{nameof(ObjectTableDebug)}_{index}",
|
ImGuiWindowFlags.AlwaysAutoResize |
|
||||||
ImGuiWindowFlags.NoDecoration |
|
ImGuiWindowFlags.NoSavedSettings |
|
||||||
ImGuiWindowFlags.AlwaysAutoResize |
|
ImGuiWindowFlags.NoMove |
|
||||||
ImGuiWindowFlags.NoSavedSettings |
|
ImGuiWindowFlags.NoMouseInputs |
|
||||||
ImGuiWindowFlags.NoMove |
|
ImGuiWindowFlags.NoDocking |
|
||||||
ImGuiWindowFlags.NoMouseInputs |
|
ImGuiWindowFlags.NoFocusOnAppearing |
|
||||||
ImGuiWindowFlags.NoDocking |
|
ImGuiWindowFlags.NoNav))
|
||||||
ImGuiWindowFlags.NoFocusOnAppearing |
|
ImGui.Text(objectText);
|
||||||
ImGuiWindowFlags.NoNav))
|
ImGui.End();
|
||||||
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;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,54 +2,53 @@
|
|||||||
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>
|
||||||
/// A <see cref="ClientLocation"/> loaded in memory, with certain extra attributes as needed.
|
/// Network id for the server you're currently connected to.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class PersistentLocation : MemoryLocation
|
public Guid? NetworkId { get; set; }
|
||||||
|
|
||||||
|
/// <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)
|
||||||
{
|
{
|
||||||
/// <see cref="ClientLocation.LocalId"/>
|
return Equals(a, b);
|
||||||
public int? LocalId { get; set; }
|
}
|
||||||
|
|
||||||
/// <summary>
|
public static bool operator !=(PersistentLocation? a, object? b)
|
||||||
/// Network id for the server you're currently connected to.
|
{
|
||||||
/// </summary>
|
return !Equals(a, b);
|
||||||
public Guid? NetworkId { get; set; }
|
}
|
||||||
|
|
||||||
/// <summary>
|
public override string ToString()
|
||||||
/// For markers that the server you're connected to doesn't know: Whether this was requested to be uploaded, to avoid duplicate requests.
|
{
|
||||||
/// </summary>
|
return $"PersistentLocation(Position={Position}, Type={Type})";
|
||||||
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})";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,38 +4,43 @@ 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>
|
||||||
{
|
{
|
||||||
internal abstract class DbTask<T>
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
where T : DbTask<T>
|
|
||||||
|
protected DbTask(IServiceScopeFactory serviceScopeFactory)
|
||||||
{
|
{
|
||||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
_serviceScopeFactory = serviceScopeFactory;
|
||||||
|
}
|
||||||
|
|
||||||
protected DbTask(IServiceScopeFactory serviceScopeFactory)
|
public void Start()
|
||||||
|
{
|
||||||
|
Task.Run(() =>
|
||||||
{
|
{
|
||||||
_serviceScopeFactory = serviceScopeFactory;
|
try
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
||||||
{
|
{
|
||||||
DependencyInjectionContext.LoggerProvider.CreateLogger<DbTask<T>>()
|
logger.LogError(e, "Failed to run DbTask");
|
||||||
.LogError(e, "Failed to run DbTask");
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
catch (Exception)
|
||||||
|
{
|
||||||
protected abstract void Run(PalClientContext dbContext, ILogger<T> logger);
|
// 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);
|
||||||
}
|
}
|
||||||
|
@ -7,73 +7,72 @@ 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>
|
||||||
{
|
{
|
||||||
internal sealed class LoadTerritory : DbTask<LoadTerritory>
|
private readonly Cleanup _cleanup;
|
||||||
|
private readonly MemoryTerritory _territory;
|
||||||
|
|
||||||
|
public LoadTerritory(IServiceScopeFactory serviceScopeFactory,
|
||||||
|
Cleanup cleanup,
|
||||||
|
MemoryTerritory territory)
|
||||||
|
: base(serviceScopeFactory)
|
||||||
{
|
{
|
||||||
private readonly Cleanup _cleanup;
|
_cleanup = cleanup;
|
||||||
private readonly MemoryTerritory _territory;
|
_territory = territory;
|
||||||
|
}
|
||||||
|
|
||||||
public LoadTerritory(IServiceScopeFactory serviceScopeFactory,
|
protected override void Run(PalClientContext dbContext, ILogger<LoadTerritory> logger)
|
||||||
Cleanup cleanup,
|
{
|
||||||
MemoryTerritory territory)
|
lock (_territory.LockObj)
|
||||||
: base(serviceScopeFactory)
|
|
||||||
{
|
{
|
||||||
_cleanup = cleanup;
|
if (_territory.ReadyState != MemoryTerritory.EReadyState.Loading)
|
||||||
_territory = territory;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Run(PalClientContext dbContext, ILogger<LoadTerritory> logger)
|
|
||||||
{
|
|
||||||
lock (_territory.LockObj)
|
|
||||||
{
|
{
|
||||||
if (_territory.ReadyState != MemoryTerritory.EReadyState.Loading)
|
logger.LogInformation("Territory {Territory} is in state {State}", _territory.TerritoryType,
|
||||||
{
|
_territory.ReadyState);
|
||||||
logger.LogInformation("Territory {Territory} is in state {State}", _territory.TerritoryType,
|
return;
|
||||||
_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);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public static PersistentLocation ToMemoryLocation(ClientLocation location)
|
logger.LogInformation("Loading territory {Territory}", _territory.TerritoryType);
|
||||||
{
|
|
||||||
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)
|
// purge outdated locations
|
||||||
{
|
_cleanup.Purge(dbContext, _territory.TerritoryType);
|
||||||
return type switch
|
|
||||||
{
|
// load good locations
|
||||||
ClientLocation.EType.Trap => MemoryLocation.EType.Trap,
|
List<ClientLocation> locations = dbContext.Locations
|
||||||
ClientLocation.EType.Hoard => MemoryLocation.EType.Hoard,
|
.Where(o => o.TerritoryType == (ushort)_territory.TerritoryType)
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
|
.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
{
|
{
|
||||||
internal sealed class MarkLocalSeen : DbTask<MarkLocalSeen>
|
private readonly MemoryTerritory _territory;
|
||||||
|
private readonly IReadOnlyList<PersistentLocation> _locations;
|
||||||
|
|
||||||
|
public MarkLocalSeen(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory,
|
||||||
|
IReadOnlyList<PersistentLocation> locations)
|
||||||
|
: base(serviceScopeFactory)
|
||||||
{
|
{
|
||||||
private readonly MemoryTerritory _territory;
|
_territory = territory;
|
||||||
private readonly IReadOnlyList<PersistentLocation> _locations;
|
_locations = locations;
|
||||||
|
}
|
||||||
|
|
||||||
public MarkLocalSeen(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory,
|
protected override void Run(PalClientContext dbContext, ILogger<MarkLocalSeen> logger)
|
||||||
IReadOnlyList<PersistentLocation> locations)
|
{
|
||||||
: base(serviceScopeFactory)
|
lock (_territory.LockObj)
|
||||||
{
|
{
|
||||||
_territory = territory;
|
logger.LogInformation("Marking {Count} locations as seen locally in territory {Territory}",
|
||||||
_locations = locations;
|
_locations.Count,
|
||||||
}
|
_territory.TerritoryType);
|
||||||
|
List<int> localIds = _locations.Select(l => l.LocalId).Where(x => x != null).Cast<int>().ToList();
|
||||||
protected override void Run(PalClientContext dbContext, ILogger<MarkLocalSeen> logger)
|
dbContext.Locations
|
||||||
{
|
.Where(loc => localIds.Contains(loc.LocalId))
|
||||||
lock (_territory.LockObj)
|
.ExecuteUpdate(loc => loc.SetProperty(l => l.Seen, true));
|
||||||
{
|
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,47 +5,46 @@ 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>
|
||||||
{
|
{
|
||||||
internal sealed class MarkRemoteSeen : DbTask<MarkRemoteSeen>
|
private readonly MemoryTerritory _territory;
|
||||||
|
private readonly IReadOnlyList<PersistentLocation> _locations;
|
||||||
|
private readonly string _accountId;
|
||||||
|
|
||||||
|
public MarkRemoteSeen(IServiceScopeFactory serviceScopeFactory,
|
||||||
|
MemoryTerritory territory,
|
||||||
|
IReadOnlyList<PersistentLocation> locations,
|
||||||
|
string accountId)
|
||||||
|
: base(serviceScopeFactory)
|
||||||
{
|
{
|
||||||
private readonly MemoryTerritory _territory;
|
_territory = territory;
|
||||||
private readonly IReadOnlyList<PersistentLocation> _locations;
|
_locations = locations;
|
||||||
private readonly string _accountId;
|
_accountId = accountId;
|
||||||
|
}
|
||||||
|
|
||||||
public MarkRemoteSeen(IServiceScopeFactory serviceScopeFactory,
|
protected override void Run(PalClientContext dbContext, ILogger<MarkRemoteSeen> logger)
|
||||||
MemoryTerritory territory,
|
{
|
||||||
IReadOnlyList<PersistentLocation> locations,
|
lock (_territory.LockObj)
|
||||||
string accountId)
|
|
||||||
: base(serviceScopeFactory)
|
|
||||||
{
|
{
|
||||||
_territory = territory;
|
logger.LogInformation("Marking {Count} locations as seen remotely on {Account} in territory {Territory}",
|
||||||
_locations = locations;
|
_locations.Count, _accountId, _territory.TerritoryType);
|
||||||
_accountId = accountId;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Run(PalClientContext dbContext, ILogger<MarkRemoteSeen> logger)
|
List<int> locationIds = _locations.Select(x => x.LocalId).Where(x => x != null).Cast<int>().ToList();
|
||||||
{
|
List<ClientLocation> locationsToUpdate =
|
||||||
lock (_territory.LockObj)
|
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)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Marking {Count} locations as seen remotely on {Account} in territory {Territory}",
|
clientLocation.RemoteEncounters.Add(new RemoteEncounter(clientLocation, _accountId));
|
||||||
_locations.Count, _accountId, _territory.TerritoryType);
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dbContext.SaveChanges();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,72 +6,71 @@ 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>
|
||||||
{
|
{
|
||||||
internal sealed class SaveNewLocations : DbTask<SaveNewLocations>
|
private readonly MemoryTerritory _territory;
|
||||||
|
private readonly List<PersistentLocation> _newLocations;
|
||||||
|
|
||||||
|
public SaveNewLocations(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory,
|
||||||
|
List<PersistentLocation> newLocations)
|
||||||
|
: base(serviceScopeFactory)
|
||||||
{
|
{
|
||||||
private readonly MemoryTerritory _territory;
|
_territory = territory;
|
||||||
private readonly List<PersistentLocation> _newLocations;
|
_newLocations = newLocations;
|
||||||
|
}
|
||||||
|
|
||||||
public SaveNewLocations(IServiceScopeFactory serviceScopeFactory, MemoryTerritory territory,
|
protected override void Run(PalClientContext dbContext, ILogger<SaveNewLocations> logger)
|
||||||
List<PersistentLocation> newLocations)
|
{
|
||||||
: base(serviceScopeFactory)
|
Run(_territory, dbContext, logger, _newLocations);
|
||||||
{
|
}
|
||||||
_territory = territory;
|
|
||||||
_newLocations = newLocations;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Run(PalClientContext dbContext, ILogger<SaveNewLocations> logger)
|
public static void Run<T>(
|
||||||
|
MemoryTerritory territory,
|
||||||
|
PalClientContext dbContext,
|
||||||
|
ILogger<T> logger,
|
||||||
|
List<PersistentLocation> locations)
|
||||||
|
{
|
||||||
|
lock (territory.LockObj)
|
||||||
{
|
{
|
||||||
Run(_territory, dbContext, logger, _newLocations);
|
logger.LogInformation("Saving {Count} new locations for territory {Territory}", locations.Count,
|
||||||
}
|
territory.TerritoryType);
|
||||||
|
|
||||||
public static void Run<T>(
|
Dictionary<PersistentLocation, ClientLocation> mapping =
|
||||||
MemoryTerritory territory,
|
locations.ToDictionary(x => x, x => ToDatabaseLocation(x, territory.TerritoryType));
|
||||||
PalClientContext dbContext,
|
dbContext.Locations.AddRange(mapping.Values);
|
||||||
ILogger<T> logger,
|
dbContext.SaveChanges();
|
||||||
List<PersistentLocation> locations)
|
|
||||||
{
|
foreach ((PersistentLocation persistentLocation, ClientLocation clientLocation) in mapping)
|
||||||
lock (territory.LockObj)
|
|
||||||
{
|
{
|
||||||
logger.LogInformation("Saving {Count} new locations for territory {Territory}", locations.Count,
|
persistentLocation.LocalId = clientLocation.LocalId;
|
||||||
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
|
||||||
{
|
{
|
||||||
return new ClientLocation
|
TerritoryType = (ushort)territoryType,
|
||||||
{
|
Type = ToDatabaseType(location.Type),
|
||||||
TerritoryType = (ushort)territoryType,
|
X = location.Position.X,
|
||||||
Type = ToDatabaseType(location.Type),
|
Y = location.Position.Y,
|
||||||
X = location.Position.X,
|
Z = location.Position.Z,
|
||||||
Y = location.Position.Y,
|
Seen = location.Seen,
|
||||||
Z = location.Position.Z,
|
Source = location.Source,
|
||||||
Seen = location.Seen,
|
SinceVersion = typeof(Plugin).Assembly.GetName().Version!.ToString(2),
|
||||||
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
|
||||||
{
|
{
|
||||||
return type switch
|
MemoryLocation.EType.Trap => ClientLocation.EType.Trap,
|
||||||
{
|
MemoryLocation.EType.Hoard => ClientLocation.EType.Hoard,
|
||||||
MemoryLocation.EType.Trap => ClientLocation.EType.Trap,
|
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
|
||||||
MemoryLocation.EType.Hoard => ClientLocation.EType.Hoard,
|
};
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,36 +1,34 @@
|
|||||||
using Dalamud.Game.ClientState;
|
using Dalamud.Game.ClientState.Conditions;
|
||||||
using Dalamud.Game.ClientState.Conditions;
|
using Dalamud.Plugin.Services;
|
||||||
using Pal.Common;
|
using Pal.Common;
|
||||||
|
|
||||||
namespace Pal.Client.Floors
|
namespace Pal.Client.Floors;
|
||||||
|
|
||||||
|
public sealed class TerritoryState
|
||||||
{
|
{
|
||||||
public sealed class TerritoryState
|
private readonly IClientState _clientState;
|
||||||
|
private readonly ICondition _condition;
|
||||||
|
|
||||||
|
public TerritoryState(IClientState clientState, ICondition condition)
|
||||||
{
|
{
|
||||||
private readonly ClientState _clientState;
|
_clientState = clientState;
|
||||||
private readonly Condition _condition;
|
_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 enum PomanderState
|
public ushort LastTerritory { get; set; }
|
||||||
{
|
public PomanderState PomanderOfSight { get; set; } = PomanderState.Inactive;
|
||||||
Inactive,
|
public PomanderState PomanderOfIntuition { get; set; } = PomanderState.Inactive;
|
||||||
Active,
|
|
||||||
FoundOnCurrentFloor,
|
public bool IsInDeepDungeon() =>
|
||||||
PomanderOfSafetyUsed,
|
_clientState.IsLoggedIn
|
||||||
}
|
&& _condition[ConditionFlag.InDeepDungeon]
|
||||||
|
&& typeof(ETerritoryType).IsEnumDefined(_clientState.TerritoryType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum PomanderState
|
||||||
|
{
|
||||||
|
Inactive,
|
||||||
|
Active,
|
||||||
|
FoundOnCurrentFloor,
|
||||||
|
PomanderOfSafetyUsed,
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,9 @@ 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
|
||||||
{
|
{
|
||||||
internal interface ILanguageChanged
|
void LanguageChanged();
|
||||||
{
|
|
||||||
void LanguageChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,95 +1,79 @@
|
|||||||
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
|
||||||
{
|
{
|
||||||
internal sealed class JwtClaims
|
[JsonPropertyName("nameid")]
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
[JsonPropertyName("nameid")]
|
if (string.IsNullOrEmpty(authToken))
|
||||||
public Guid NameId { get; set; }
|
throw new ArgumentException("Server sent no auth token", nameof(authToken));
|
||||||
|
|
||||||
[JsonPropertyName("role")]
|
string[] parts = authToken.Split('.');
|
||||||
[JsonConverter(typeof(JwtRoleConverter))]
|
if (parts.Length != 3)
|
||||||
public List<string> Roles { get; set; } = new();
|
throw new ArgumentException("Unsupported token type", nameof(authToken));
|
||||||
|
|
||||||
[JsonPropertyName("nbf")]
|
// fix padding manually
|
||||||
[JsonConverter(typeof(JwtDateConverter))]
|
string payload = parts[1].Replace(",", "=").Replace("-", "+").Replace("/", "_");
|
||||||
public DateTimeOffset NotBefore { get; set; }
|
if (payload.Length % 4 == 2)
|
||||||
|
payload += "==";
|
||||||
|
else if (payload.Length % 4 == 3)
|
||||||
|
payload += "=";
|
||||||
|
|
||||||
[JsonPropertyName("exp")]
|
string content = Encoding.UTF8.GetString(Convert.FromBase64String(payload));
|
||||||
[JsonConverter(typeof(JwtDateConverter))]
|
return JsonSerializer.Deserialize<JwtClaims>(content) ??
|
||||||
public DateTimeOffset ExpiresAt { get; set; }
|
throw new InvalidOperationException("token deserialization returned null");
|
||||||
|
|
||||||
public static JwtClaims FromAuthToken(string authToken)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(authToken))
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
List<string> result = new();
|
|
||||||
while (reader.Read())
|
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonTokenType.EndArray)
|
|
||||||
{
|
|
||||||
result.Sort();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reader.TokenType != JsonTokenType.String)
|
|
||||||
throw new JsonException("string expected");
|
|
||||||
|
|
||||||
result.Add(reader.GetString() ?? throw new JsonException("no value present"));
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new JsonException("read to end of document");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
throw new JsonException("bad token type");
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, List<string> value, JsonSerializerOptions options) => throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
List<string> result = new();
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.EndArray)
|
||||||
|
{
|
||||||
|
result.Sort();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader.TokenType != JsonTokenType.String)
|
||||||
|
throw new JsonException("string expected");
|
||||||
|
|
||||||
|
result.Add(reader.GetString() ?? throw new JsonException("no value present"));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new JsonException("read to end of document");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
throw new JsonException("bad token type");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, List<string> value, JsonSerializerOptions options) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
21
Pal.Client/Net/JwtDateConverter.cs
Normal file
21
Pal.Client/Net/JwtDateConverter.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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();
|
||||||
|
}
|
@ -11,222 +11,238 @@ 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
|
||||||
{
|
{
|
||||||
internal partial class RemoteApi
|
private static readonly Version PluginVersion = typeof(Plugin).Assembly.GetName().Version!;
|
||||||
|
private readonly SemaphoreSlim _connectLock = new(1, 1);
|
||||||
|
|
||||||
|
private async Task<(bool Success, string Error)> TryConnect(CancellationToken cancellationToken,
|
||||||
|
ILoggerFactory? loggerFactory = null, bool retry = true)
|
||||||
{
|
{
|
||||||
private readonly SemaphoreSlim _connectLock = new(1, 1);
|
using IDisposable? logScope = _logger.BeginScope("TryConnect");
|
||||||
|
|
||||||
private async Task<(bool Success, string Error)> TryConnect(CancellationToken cancellationToken,
|
var result = await TryConnectImpl(cancellationToken, loggerFactory);
|
||||||
ILoggerFactory? loggerFactory = null, bool retry = true)
|
if (retry && result.ShouldRetry)
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
using IDisposable? logScope = _logger.BeginScope("TryConnect");
|
_logger.LogDebug("Not Online, not attempting to establish a connection");
|
||||||
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<(bool Success, string Error, bool ShouldRetry)> TryConnectImpl(
|
if (_channel == null ||
|
||||||
CancellationToken cancellationToken,
|
!(_channel.State == ConnectivityState.Ready || _channel.State == ConnectivityState.Idle))
|
||||||
ILoggerFactory? loggerFactory)
|
|
||||||
{
|
{
|
||||||
if (_configuration.Mode != EMode.Online)
|
Dispose();
|
||||||
{
|
|
||||||
_logger.LogDebug("Not Online, not attempting to establish a connection");
|
|
||||||
return (false, Localization.ConnectionError_NotOnline, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_channel == null ||
|
_logger.LogInformation("Creating new gRPC channel");
|
||||||
!(_channel.State == ConnectivityState.Ready || _channel.State == ConnectivityState.Idle))
|
_channel = GrpcChannel.ForAddress(RemoteUrl, new GrpcChannelOptions
|
||||||
{
|
{
|
||||||
Dispose();
|
HttpHandler = new SocketsHttpHandler
|
||||||
|
|
||||||
_logger.LogInformation("Creating new gRPC channel");
|
|
||||||
_channel = GrpcChannel.ForAddress(RemoteUrl, new GrpcChannelOptions
|
|
||||||
{
|
{
|
||||||
HttpHandler = new SocketsHttpHandler
|
ConnectTimeout = TimeSpan.FromSeconds(5),
|
||||||
{
|
SslOptions = GetSslClientAuthenticationOptions(),
|
||||||
ConnectTimeout = TimeSpan.FromSeconds(5),
|
},
|
||||||
SslOptions = GetSslClientAuthenticationOptions(),
|
LoggerFactory = loggerFactory,
|
||||||
},
|
});
|
||||||
LoggerFactory = loggerFactory,
|
|
||||||
});
|
|
||||||
|
|
||||||
_logger.LogInformation("Connecting to upstream service at {Url}", RemoteUrl);
|
_logger.LogInformation("Connecting to upstream service at {Url}", RemoteUrl);
|
||||||
await _channel.ConnectAsync(cancellationToken);
|
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()
|
||||||
|
{
|
||||||
|
Major = PluginVersion.Major,
|
||||||
|
Minor = PluginVersion.Minor,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
_logger.LogTrace("Acquiring connect lock");
|
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
||||||
await _connectLock.WaitAsync(cancellationToken);
|
if (configuredAccount == null)
|
||||||
_logger.LogTrace("Obtained connect lock");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var accountClient = new AccountService.AccountServiceClient(_channel);
|
_logger.LogWarning("No account to login with");
|
||||||
IAccountConfiguration? configuredAccount = _configuration.FindAccount(RemoteUrl);
|
return (false, Localization.ConnectionError_CreateAccountReturnedNoId, false);
|
||||||
if (configuredAccount == null)
|
}
|
||||||
{
|
|
||||||
_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);
|
if (!_loginInfo.IsValid)
|
||||||
_logger.LogInformation("Account created with id {AccountId}", accountId.ToPartialId());
|
{
|
||||||
|
_logger.LogInformation("Logging in with account id {AccountId}",
|
||||||
_configurationManager.Save(_configuration);
|
configuredAccount.AccountId.ToPartialId());
|
||||||
}
|
LoginReply loginReply = await accountClient.LoginAsync(
|
||||||
else
|
new LoginRequest
|
||||||
{
|
{
|
||||||
_logger.LogError("Account creation failed with error {Error}", createAccountReply.Error);
|
AccountId = configuredAccount.AccountId.ToString(),
|
||||||
if (createAccountReply.Error == CreateAccountError.UpgradeRequired && !_warnedAboutUpgrade)
|
Version = new()
|
||||||
{
|
{
|
||||||
_chat.Error(Localization.ConnectionError_OldVersion);
|
Major = PluginVersion.Major,
|
||||||
_warnedAboutUpgrade = true;
|
Minor = PluginVersion.Minor,
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10),
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
|
||||||
return (false,
|
if (loginReply.Success)
|
||||||
string.Format(Localization.ConnectionError_CreateAccountFailed, createAccountReply.Error),
|
|
||||||
false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
|
||||||
if (configuredAccount == null)
|
|
||||||
{
|
{
|
||||||
_logger.LogWarning("No account to login with");
|
_logger.LogInformation("Login successful with account id: {AccountId}",
|
||||||
return (false, Localization.ConnectionError_CreateAccountReturnedNoId, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_loginInfo.IsValid)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Logging in with account id {AccountId}",
|
|
||||||
configuredAccount.AccountId.ToPartialId());
|
configuredAccount.AccountId.ToPartialId());
|
||||||
LoginReply loginReply = await accountClient.LoginAsync(
|
_loginInfo = new LoginInfo(loginReply.AuthToken);
|
||||||
new LoginRequest { AccountId = configuredAccount.AccountId.ToString() },
|
|
||||||
headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10),
|
|
||||||
cancellationToken: cancellationToken);
|
|
||||||
|
|
||||||
if (loginReply.Success)
|
bool save = configuredAccount.EncryptIfNeeded();
|
||||||
|
|
||||||
|
List<string> newRoles = _loginInfo.Claims?.Roles.ToList() ?? new();
|
||||||
|
if (!newRoles.SequenceEqual(configuredAccount.CachedRoles))
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Login successful with account id: {AccountId}",
|
configuredAccount.CachedRoles = newRoles;
|
||||||
configuredAccount.AccountId.ToPartialId());
|
save = true;
|
||||||
_loginInfo = new LoginInfo(loginReply.AuthToken);
|
|
||||||
|
|
||||||
bool save = configuredAccount.EncryptIfNeeded();
|
|
||||||
|
|
||||||
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");
|
if (save)
|
||||||
return (false, Localization.ConnectionError_InvalidAccountId, true);
|
_configurationManager.Save(_configuration);
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsLoggedIn { get; }
|
if (!_loginInfo.IsValid)
|
||||||
public string? AuthToken { get; }
|
{
|
||||||
public JwtClaims? Claims { get; }
|
_logger.LogError("Login state is loggedIn={LoggedIn}, expired={Expired}", _loginInfo.IsLoggedIn,
|
||||||
|
_loginInfo.IsExpired);
|
||||||
|
return (false, Localization.ConnectionError_LoginReturnedNoToken, false);
|
||||||
|
}
|
||||||
|
|
||||||
private DateTimeOffset ExpiresAt =>
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
Claims?.ExpiresAt.Subtract(TimeSpan.FromMinutes(5)) ?? DateTimeOffset.MinValue;
|
return (true, string.Empty, false);
|
||||||
|
}
|
||||||
public bool IsExpired => ExpiresAt < DateTimeOffset.UtcNow;
|
finally
|
||||||
|
{
|
||||||
public bool IsValid => IsLoggedIn && !IsExpired;
|
_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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
{
|
|
||||||
if (!await Connect(cancellationToken))
|
|
||||||
return new(false, new());
|
|
||||||
|
|
||||||
var exportClient = new ExportService.ExportServiceClient(_channel);
|
internal partial class RemoteApi
|
||||||
var exportReply = await exportClient.ExportAsync(new ExportRequest
|
{
|
||||||
|
public async Task<(bool, ExportRoot)> DoExport(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (!await Connect(cancellationToken))
|
||||||
|
return new(false, new());
|
||||||
|
|
||||||
|
var exportClient = new ExportService.ExportServiceClient(_channel);
|
||||||
|
var exportReply = await exportClient.ExportAsync(new ExportRequest
|
||||||
{
|
{
|
||||||
ServerUrl = RemoteUrl,
|
ServerUrl = RemoteUrl,
|
||||||
}, headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(120), cancellationToken: cancellationToken);
|
}, headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(120),
|
||||||
return (exportReply.Success, exportReply.Data);
|
cancellationToken: cancellationToken);
|
||||||
}
|
return (exportReply.Success, exportReply.Data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,80 +8,88 @@ 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
|
||||||
{
|
{
|
||||||
internal partial class RemoteApi
|
public async Task<(bool, List<PersistentLocation>)> DownloadRemoteMarkers(ushort territoryId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
public async Task<(bool, List<PersistentLocation>)> DownloadRemoteMarkers(ushort territoryId, CancellationToken cancellationToken = default)
|
if (!await Connect(cancellationToken))
|
||||||
|
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
|
||||||
{
|
{
|
||||||
if (!await Connect(cancellationToken))
|
TerritoryType = territoryType,
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
if (locations.Count == 0)
|
Type = m.Type.ToObjectType(),
|
||||||
return (true, new());
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
if (!await Connect(cancellationToken))
|
public async Task<bool> MarkAsSeen(ushort territoryType, IReadOnlyList<PersistentLocation> locations,
|
||||||
return (false, new());
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (locations.Count == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
var palaceClient = new PalaceService.PalaceServiceClient(_channel);
|
if (!await Connect(cancellationToken))
|
||||||
var uploadRequest = new UploadFloorsRequest
|
return false;
|
||||||
{
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> MarkAsSeen(ushort territoryType, IReadOnlyList<PersistentLocation> locations, CancellationToken cancellationToken = default)
|
var palaceClient = new PalaceService.PalaceServiceClient(_channel);
|
||||||
|
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
|
||||||
{
|
{
|
||||||
if (locations.Count == 0)
|
Type = obj.Type.ToMemoryType(),
|
||||||
return true;
|
Position = new Vector3(obj.X, obj.Y, obj.Z),
|
||||||
|
NetworkId = Guid.Parse(obj.NetworkId),
|
||||||
|
Source = ClientLocation.ESource.Download,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!await Connect(cancellationToken))
|
public async Task<(bool, List<FloorStatistics>)> FetchStatistics(CancellationToken cancellationToken = default)
|
||||||
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 seenRequest = new MarkObjectsSeenRequest { TerritoryType = territoryType };
|
var statisticsReply = await palaceClient.FetchStatisticsAsync(new StatisticsRequest(),
|
||||||
foreach (var marker in locations)
|
headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(30),
|
||||||
seenRequest.NetworkIds.Add(marker.NetworkId.ToString());
|
cancellationToken: cancellationToken);
|
||||||
|
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,54 +5,53 @@ 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
|
||||||
{
|
{
|
||||||
internal partial class RemoteApi
|
private Metadata UnauthorizedHeaders() => new()
|
||||||
{
|
{
|
||||||
private Metadata UnauthorizedHeaders() => new()
|
{ "User-Agent", _userAgent },
|
||||||
{
|
};
|
||||||
{ "User-Agent", _userAgent },
|
|
||||||
};
|
|
||||||
|
|
||||||
private Metadata AuthorizedHeaders() => new()
|
private Metadata AuthorizedHeaders() => new()
|
||||||
{
|
{
|
||||||
{ "Authorization", $"Bearer {_loginInfo.AuthToken}" },
|
{ "Authorization", $"Bearer {_loginInfo.AuthToken}" },
|
||||||
{ "User-Agent", _userAgent },
|
{ "User-Agent", _userAgent },
|
||||||
};
|
};
|
||||||
|
|
||||||
private SslClientAuthenticationOptions? GetSslClientAuthenticationOptions()
|
private SslClientAuthenticationOptions? GetSslClientAuthenticationOptions()
|
||||||
{
|
{
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
var secrets = typeof(RemoteApi).Assembly.GetType("Pal.Client.Secrets");
|
var secrets = typeof(RemoteApi).Assembly.GetType("Pal.Client.Secrets");
|
||||||
if (secrets == null)
|
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
|
|
||||||
_logger.LogDebug("Not using client certificate");
|
|
||||||
return 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
|
||||||
|
_logger.LogDebug("Not using client certificate");
|
||||||
|
return null;
|
||||||
#endif
|
#endif
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,47 +6,46 @@ 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://pal.liza.sh";
|
public const string RemoteUrl = "https://connect.palacepal.com";
|
||||||
#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;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,29 +1,15 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Dalamud.NET.Sdk/9.0.2">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0-windows</TargetFramework>
|
<Version>6.0</Version>
|
||||||
<LangVersion>11.0</LangVersion>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
|
|
||||||
<PlatformTarget>x64</PlatformTarget>
|
|
||||||
<AssemblyName>Palace Pal</AssemblyName>
|
<AssemblyName>Palace Pal</AssemblyName>
|
||||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
<PlatformTarget>x64</PlatformTarget>
|
||||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<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>
|
||||||
|
|
||||||
|
<Import Project="..\vendor\LLib\LLib.targets"/>
|
||||||
|
<Import Project="..\vendor\LLib\RenameZip.targets"/>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||||
<OutputPath>dist</OutputPath>
|
<OutputPath>dist</OutputPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@ -37,29 +23,26 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DalamudPackager" Version="2.1.11"/>
|
<PackageReference Include="Dalamud.Extensions.MicrosoftLogging" Version="4.0.1"/>
|
||||||
<PackageReference Include="Dalamud.Extensions.MicrosoftLogging" Version="1.0.0"/>
|
<PackageReference Include="Google.Protobuf" Version="3.27.2" />
|
||||||
<PackageReference Include="Google.Protobuf" Version="3.22.1"/>
|
<PackageReference Include="Grpc.Net.Client" Version="2.63.0"/>
|
||||||
<PackageReference Include="Grpc.Net.Client" Version="2.52.0"/>
|
<PackageReference Include="Grpc.Tools" Version="2.64.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="7.0.4"/>
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.4">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.6" Condition="'$(Configuration)' == 'EF'">
|
||||||
<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="7.0.0"/>
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0"/>
|
||||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="7.0.1"/>
|
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0"/>
|
||||||
</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>
|
||||||
@ -68,42 +51,6 @@
|
|||||||
<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>
|
||||||
@ -116,17 +63,6 @@
|
|||||||
</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 "$(OutDir)$(AssemblyName)\latest.zip" "$(AssemblyName)-$(Version).zip""/>
|
|
||||||
</Target>
|
|
||||||
|
|
||||||
<Target Name="Clean">
|
<Target Name="Clean">
|
||||||
<RemoveDir Directories="dist"/>
|
<RemoveDir Directories="dist"/>
|
||||||
</Target>
|
</Target>
|
||||||
|
@ -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://github.com/carvelli/PalacePal",
|
"RepoUrl": "https://git.carvel.li/liza/PalacePal",
|
||||||
"IconUrl": "https://raw.githubusercontent.com/carvelli/Dalamud-Plugins/master/dist/Palace Pal.png",
|
"IconUrl": "https://plugins.carvel.li/icons/PalacePal.png",
|
||||||
"Tags": [
|
"Tags": [
|
||||||
"potd",
|
"potd",
|
||||||
"palace",
|
"palace",
|
||||||
|
@ -5,12 +5,11 @@ 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.Game;
|
using Dalamud.Extensions.MicrosoftLogging;
|
||||||
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;
|
||||||
@ -21,217 +20,216 @@ 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
|
||||||
{
|
{
|
||||||
/// <summary>
|
private readonly CancellationTokenSource _initCts = new();
|
||||||
/// 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 .
|
private readonly IDalamudPluginInterface _pluginInterface;
|
||||||
/// </summary>
|
private readonly ICommandManager _commandManager;
|
||||||
/// <see cref="DependencyInjectionContext"/>
|
private readonly IClientState _clientState;
|
||||||
internal sealed class Plugin : IDalamudPlugin
|
private readonly IChatGui _chatGui;
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
private readonly CancellationTokenSource _initCts = new();
|
_pluginInterface = pluginInterface;
|
||||||
|
_commandManager = commandManager;
|
||||||
|
_clientState = clientState;
|
||||||
|
_chatGui = chatGui;
|
||||||
|
_framework = framework;
|
||||||
|
_logger = new DalamudLoggerProvider(pluginLog).CreateLogger<Plugin>();
|
||||||
|
|
||||||
private readonly DalamudPluginInterface _pluginInterface;
|
// set up the current UI language before creating anything
|
||||||
private readonly CommandManager _commandManager;
|
Localization.Culture = new CultureInfo(_pluginInterface.UiLanguage);
|
||||||
private readonly ClientState _clientState;
|
|
||||||
private readonly ChatGui _chatGui;
|
|
||||||
private readonly Framework _framework;
|
|
||||||
|
|
||||||
private readonly TaskCompletionSource<IServiceScope> _rootScopeCompletionSource = new();
|
_commandManager.AddHandler("/pal", new CommandInfo(OnCommand)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
_pluginInterface = pluginInterface;
|
HelpMessage = Localization.Command_pal_HelpText
|
||||||
_commandManager = commandManager;
|
});
|
||||||
_clientState = clientState;
|
|
||||||
_chatGui = chatGui;
|
|
||||||
_framework = framework;
|
|
||||||
|
|
||||||
// set up the current UI language before creating anything
|
// Using TickScheduler requires ECommons to at least be partially initialized
|
||||||
Localization.Culture = new CultureInfo(_pluginInterface.UiLanguage);
|
// ECommonsMain.Dispose leaves this untouched.
|
||||||
|
Svc.Init(pluginInterface);
|
||||||
|
|
||||||
_commandManager.AddHandler("/pal", new CommandInfo(OnCommand)
|
Task.Run(async () => await CreateDependencyContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
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(() =>
|
||||||
{
|
{
|
||||||
HelpMessage = Localization.Command_pal_HelpText
|
_pluginInterface.UiBuilder.Draw += Draw;
|
||||||
|
_pluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi;
|
||||||
|
_pluginInterface.LanguageChanged += LanguageChanged;
|
||||||
|
_clientState.Login += Login;
|
||||||
});
|
});
|
||||||
|
_rootScopeCompletionSource.SetResult(_rootScope);
|
||||||
// Using TickScheduler requires ECommons to at least be partially initialized
|
_loadState = ELoadState.Loaded;
|
||||||
// ECommonsMain.Dispose leaves this untouched.
|
|
||||||
Svc.Init(pluginInterface);
|
|
||||||
|
|
||||||
Task.Run(async () => await CreateDependencyContext());
|
|
||||||
}
|
}
|
||||||
|
catch (Exception e) when (e is ObjectDisposedException
|
||||||
public string Name => Localization.Palace_Pal;
|
or OperationCanceledException
|
||||||
|
or RepoVerification.RepoVerificationFailedException
|
||||||
private async Task CreateDependencyContext()
|
|| (e is FileLoadException && _pluginInterface.IsDev))
|
||||||
{
|
{
|
||||||
|
_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
|
||||||
{
|
{
|
||||||
_dependencyInjectionContext = _pluginInterface.Create<DependencyInjectionContext>(this)
|
rootScope = await _rootScopeCompletionSource.Task;
|
||||||
?? throw new Exception("Could not create DI root context class");
|
chat = rootScope.ServiceProvider.GetRequiredService<Chat>();
|
||||||
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)
|
||||||
{
|
{
|
||||||
_rootScopeCompletionSource.SetException(e);
|
_logger.LogError(e, "Could not wait for command root scope");
|
||||||
_logger.LogError(e, "Async load failed");
|
return;
|
||||||
ShowErrorOnLogin(() =>
|
|
||||||
new Chat(_chatGui).Error(string.Format(Localization.Error_LoadFailed,
|
|
||||||
$"{e.GetType()} - {e.Message}")));
|
|
||||||
|
|
||||||
_loadState = ELoadState.Error;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void ShowErrorOnLogin(Action? loginAction)
|
try
|
||||||
{
|
|
||||||
if (_clientState.IsLoggedIn)
|
|
||||||
{
|
{
|
||||||
loginAction?.Invoke();
|
IPalacePalConfiguration configuration =
|
||||||
_loginAction = null;
|
rootScope.ServiceProvider.GetRequiredService<IPalacePalConfiguration>();
|
||||||
}
|
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
|
|
||||||
{
|
{
|
||||||
rootScope = await _rootScopeCompletionSource.Task;
|
chat.Error(Localization.Error_FirstTimeSetupRequired);
|
||||||
chat = rootScope.ServiceProvider.GetRequiredService<Chat>();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_logger.LogError(e, "Could not wait for command root scope");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
Action<string> commandHandler = rootScope.ServiceProvider
|
||||||
{
|
.GetRequiredService<IEnumerable<ISubCommand>>()
|
||||||
IPalacePalConfiguration configuration =
|
.SelectMany(cmd => cmd.GetHandlers())
|
||||||
rootScope.ServiceProvider.GetRequiredService<IPalacePalConfiguration>();
|
.Where(cmd => cmd.Key == arguments.ToLowerInvariant())
|
||||||
if (configuration.FirstUse && arguments != "" && arguments != "config")
|
.Select(cmd => cmd.Value)
|
||||||
|
.SingleOrDefault(missingCommand =>
|
||||||
{
|
{
|
||||||
chat.Error(Localization.Error_FirstTimeSetupRequired);
|
chat.Error(string.Format(Localization.Command_pal_UnknownSubcommand, missingCommand,
|
||||||
return;
|
command));
|
||||||
}
|
});
|
||||||
|
commandHandler.Invoke(arguments);
|
||||||
Action<string> commandHandler = rootScope.ServiceProvider
|
|
||||||
.GetRequiredService<IEnumerable<ISubCommand>>()
|
|
||||||
.SelectMany(cmd => cmd.GetHandlers())
|
|
||||||
.Where(cmd => cmd.Key == arguments.ToLowerInvariant())
|
|
||||||
.Select(cmd => cmd.Value)
|
|
||||||
.SingleOrDefault(missingCommand =>
|
|
||||||
{
|
|
||||||
chat.Error(string.Format(Localization.Command_pal_UnknownSubcommand, missingCommand,
|
|
||||||
command));
|
|
||||||
});
|
|
||||||
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()
|
|
||||||
=> _rootScope!.ServiceProvider.GetRequiredService<PalConfigCommand>().Execute();
|
|
||||||
|
|
||||||
private void LanguageChanged(string languageCode)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Language set to '{Language}'", languageCode);
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
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}"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_initCts.Cancel();
|
private void OpenConfigUi()
|
||||||
_rootScope?.Dispose();
|
=> _rootScope!.ServiceProvider.GetRequiredService<PalConfigCommand>().Execute();
|
||||||
_dependencyInjectionContext?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum ELoadState
|
private void LanguageChanged(string languageCode)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Language set to '{Language}'", languageCode);
|
||||||
|
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
Initializing,
|
_pluginInterface.UiBuilder.Draw -= Draw;
|
||||||
Loaded,
|
_pluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi;
|
||||||
Error
|
_pluginInterface.LanguageChanged -= LanguageChanged;
|
||||||
|
_clientState.Login -= Login;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_initCts.Cancel();
|
||||||
|
_rootScope?.Dispose();
|
||||||
|
_dependencyInjectionContext?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum ELoadState
|
||||||
|
{
|
||||||
|
Initializing,
|
||||||
|
Loaded,
|
||||||
|
Error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
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)]
|
|
@ -15,6 +15,7 @@ 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
|
||||||
```
|
```
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
namespace Pal.Client.Rendering
|
namespace Pal.Client.Rendering;
|
||||||
|
|
||||||
|
internal enum ELayer
|
||||||
{
|
{
|
||||||
internal enum ELayer
|
TrapHoard,
|
||||||
{
|
RegularCoffers,
|
||||||
TrapHoard,
|
Test,
|
||||||
RegularCoffers,
|
|
||||||
Test,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
namespace Pal.Client.Rendering
|
namespace Pal.Client.Rendering;
|
||||||
{
|
|
||||||
public interface IRenderElement
|
|
||||||
{
|
|
||||||
bool IsValid { get; }
|
|
||||||
|
|
||||||
uint Color { get; set; }
|
public interface IRenderElement
|
||||||
}
|
{
|
||||||
|
bool IsValid { get; }
|
||||||
|
|
||||||
|
bool Enabled { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -3,18 +3,17 @@ 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
|
||||||
{
|
{
|
||||||
internal interface IRenderer
|
ERenderer GetConfigValue();
|
||||||
{
|
|
||||||
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, uint color, bool fill = false);
|
IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, bool enabled, uint color, bool fill = false);
|
||||||
|
|
||||||
void DrawDebugItems(uint trapColor, uint hoardColor);
|
void DrawDebugItems(uint trapColor, uint hoardColor);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,23 @@
|
|||||||
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
|
||||||
{
|
{
|
||||||
internal sealed class MarkerConfig
|
private static readonly MarkerConfig EmptyConfig = new();
|
||||||
|
|
||||||
|
private static readonly Dictionary<MemoryLocation.EType, MarkerConfig> MarkerConfigs = new()
|
||||||
{
|
{
|
||||||
private static readonly MarkerConfig EmptyConfig = new();
|
{ 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 } },
|
||||||
|
};
|
||||||
|
|
||||||
private static readonly Dictionary<MemoryLocation.EType, MarkerConfig> MarkerConfigs = new()
|
public float OffsetY { get; private init; }
|
||||||
{
|
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 float OffsetY { get; private init; }
|
public static MarkerConfig ForType(MemoryLocation.EType type) =>
|
||||||
public float Radius { get; private init; } = 0.25f;
|
MarkerConfigs.GetValueOrDefault(type, EmptyConfig);
|
||||||
|
|
||||||
public static MarkerConfig ForType(MemoryLocation.EType type) =>
|
|
||||||
MarkerConfigs.GetValueOrDefault(type, EmptyConfig);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
internal sealed class RenderAdapter : IRenderer, IDisposable
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
|
private readonly ILogger<RenderAdapter> _logger;
|
||||||
|
private readonly IPalacePalConfiguration _configuration;
|
||||||
|
|
||||||
|
private IServiceScope? _renderScope;
|
||||||
|
private IRenderer _implementation;
|
||||||
|
|
||||||
|
public RenderAdapter(IServiceScopeFactory serviceScopeFactory, ILogger<RenderAdapter> logger,
|
||||||
|
IPalacePalConfiguration configuration)
|
||||||
{
|
{
|
||||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
_serviceScopeFactory = serviceScopeFactory;
|
||||||
private readonly ILogger<RenderAdapter> _logger;
|
_logger = logger;
|
||||||
private readonly IPalacePalConfiguration _configuration;
|
_configuration = configuration;
|
||||||
|
|
||||||
private IServiceScope? _renderScope;
|
_implementation = Recreate(null);
|
||||||
private IRenderer _implementation;
|
}
|
||||||
|
|
||||||
public RenderAdapter(IServiceScopeFactory serviceScopeFactory, ILogger<RenderAdapter> logger,
|
public bool RequireRedraw { get; set; }
|
||||||
IPalacePalConfiguration configuration)
|
|
||||||
{
|
|
||||||
_serviceScopeFactory = serviceScopeFactory;
|
|
||||||
_logger = logger;
|
|
||||||
_configuration = configuration;
|
|
||||||
|
|
||||||
_implementation = Recreate(null);
|
private IRenderer Recreate(ERenderer? currentRenderer)
|
||||||
}
|
{
|
||||||
|
ERenderer targetRenderer = _configuration.Renderer.SelectedRenderer;
|
||||||
|
if (targetRenderer == currentRenderer)
|
||||||
|
return _implementation;
|
||||||
|
|
||||||
public bool RequireRedraw { get; set; }
|
_renderScope?.Dispose();
|
||||||
|
|
||||||
private IRenderer Recreate(ERenderer? currentRenderer)
|
_logger.LogInformation("Selected new renderer: {Renderer}", _configuration.Renderer.SelectedRenderer);
|
||||||
{
|
_renderScope = _serviceScopeFactory.CreateScope();
|
||||||
ERenderer targetRenderer = _configuration.Renderer.SelectedRenderer;
|
if (targetRenderer == ERenderer.Splatoon)
|
||||||
if (targetRenderer == currentRenderer)
|
return _renderScope.ServiceProvider.GetRequiredService<SplatoonRenderer>();
|
||||||
return _implementation;
|
else
|
||||||
|
return _renderScope.ServiceProvider.GetRequiredService<SimpleRenderer>();
|
||||||
|
}
|
||||||
|
|
||||||
_renderScope?.Dispose();
|
public void ConfigUpdated()
|
||||||
|
{
|
||||||
|
_implementation = Recreate(_implementation.GetConfigValue());
|
||||||
|
RequireRedraw = true;
|
||||||
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Selected new renderer: {Renderer}", _configuration.Renderer.SelectedRenderer);
|
public void Dispose()
|
||||||
_renderScope = _serviceScopeFactory.CreateScope();
|
=> _renderScope?.Dispose();
|
||||||
if (targetRenderer == ERenderer.Splatoon)
|
|
||||||
return _renderScope.ServiceProvider.GetRequiredService<SplatoonRenderer>();
|
|
||||||
else
|
|
||||||
return _renderScope.ServiceProvider.GetRequiredService<SimpleRenderer>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ConfigUpdated()
|
public void SetLayer(ELayer layer, IReadOnlyList<IRenderElement> elements)
|
||||||
{
|
=> _implementation.SetLayer(layer, elements);
|
||||||
_implementation = Recreate(_implementation.GetConfigValue());
|
|
||||||
RequireRedraw = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
public void ResetLayer(ELayer layer)
|
||||||
=> _renderScope?.Dispose();
|
=> _implementation.ResetLayer(layer);
|
||||||
|
|
||||||
public void SetLayer(ELayer layer, IReadOnlyList<IRenderElement> elements)
|
public IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, bool enabled, uint color,
|
||||||
=> _implementation.SetLayer(layer, elements);
|
bool fill = false)
|
||||||
|
=> _implementation.CreateElement(type, pos, enabled, color, fill);
|
||||||
|
|
||||||
public void ResetLayer(ELayer layer)
|
public ERenderer GetConfigValue()
|
||||||
=> _implementation.ResetLayer(layer);
|
=> throw new NotImplementedException();
|
||||||
|
|
||||||
public IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, uint color, bool fill = false)
|
public void DrawDebugItems(uint trapColor, uint hoardColor)
|
||||||
=> _implementation.CreateElement(type, pos, color, fill);
|
=> _implementation.DrawDebugItems(trapColor, hoardColor);
|
||||||
|
|
||||||
public ERenderer GetConfigValue()
|
public void DrawLayers()
|
||||||
=> throw new NotImplementedException();
|
{
|
||||||
|
if (_implementation is SimpleRenderer sr)
|
||||||
public void DrawDebugItems(uint trapColor, uint hoardColor)
|
sr.DrawLayers();
|
||||||
=> _implementation.DrawDebugItems(trapColor, hoardColor);
|
|
||||||
|
|
||||||
public void DrawLayers()
|
|
||||||
{
|
|
||||||
if (_implementation is SimpleRenderer sr)
|
|
||||||
sr.DrawLayers();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
namespace Pal.Client.Rendering
|
namespace Pal.Client.Rendering;
|
||||||
|
|
||||||
|
internal static class RenderData
|
||||||
{
|
{
|
||||||
internal static class RenderData
|
public static readonly long TestLayerTimeout = 10_000;
|
||||||
{
|
|
||||||
public static readonly uint ColorInvisible = 0;
|
|
||||||
public static readonly long TestLayerTimeout = 10_000;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -3,202 +3,204 @@ 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.Game.ClientState;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Game.Gui;
|
using Dalamud.Plugin.Services;
|
||||||
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
|
||||||
{
|
{
|
||||||
/// <summary>
|
private const int SegmentCount = 20;
|
||||||
/// Simple renderer that only draws basic stuff.
|
|
||||||
///
|
private readonly IClientState _clientState;
|
||||||
/// This is based on what SliceIsRight uses, and what PalacePal used before it was
|
private readonly IGameGui _gameGui;
|
||||||
/// remade into PalacePal (which is the third or fourth iteration on the same idea
|
private readonly IPalacePalConfiguration _configuration;
|
||||||
/// I made, just with a clear vision).
|
private readonly TerritoryState _territoryState;
|
||||||
/// </summary>
|
private readonly ConcurrentDictionary<ELayer, SimpleLayer> _layers = new();
|
||||||
internal sealed class SimpleRenderer : IRenderer, IDisposable
|
|
||||||
|
public SimpleRenderer(IClientState clientState, IGameGui gameGui, IPalacePalConfiguration configuration,
|
||||||
|
TerritoryState territoryState)
|
||||||
{
|
{
|
||||||
private const int SegmentCount = 20;
|
_clientState = clientState;
|
||||||
|
_gameGui = gameGui;
|
||||||
|
_configuration = configuration;
|
||||||
|
_territoryState = territoryState;
|
||||||
|
}
|
||||||
|
|
||||||
private readonly ClientState _clientState;
|
public void SetLayer(ELayer layer, IReadOnlyList<IRenderElement> elements)
|
||||||
private readonly GameGui _gameGui;
|
{
|
||||||
private readonly IPalacePalConfiguration _configuration;
|
_layers[layer] = new SimpleLayer
|
||||||
private readonly TerritoryState _territoryState;
|
|
||||||
private readonly ConcurrentDictionary<ELayer, SimpleLayer> _layers = new();
|
|
||||||
|
|
||||||
public SimpleRenderer(ClientState clientState, GameGui gameGui, IPalacePalConfiguration configuration,
|
|
||||||
TerritoryState territoryState)
|
|
||||||
{
|
{
|
||||||
_clientState = clientState;
|
TerritoryType = _clientState.TerritoryType,
|
||||||
_gameGui = gameGui;
|
Elements = elements.Cast<SimpleElement>().ToList()
|
||||||
_configuration = configuration;
|
};
|
||||||
_territoryState = territoryState;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void SetLayer(ELayer layer, IReadOnlyList<IRenderElement> elements)
|
public void ResetLayer(ELayer layer)
|
||||||
|
{
|
||||||
|
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
|
||||||
{
|
{
|
||||||
_layers[layer] = new SimpleLayer
|
Type = type,
|
||||||
|
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>
|
||||||
{
|
{
|
||||||
TerritoryType = _clientState.TerritoryType,
|
(SimpleElement)CreateElement(
|
||||||
Elements = elements.Cast<SimpleElement>().ToList()
|
MemoryLocation.EType.Trap,
|
||||||
};
|
_clientState.LocalPlayer?.Position ?? default,
|
||||||
}
|
true,
|
||||||
|
trapColor),
|
||||||
|
(SimpleElement)CreateElement(
|
||||||
|
MemoryLocation.EType.Hoard,
|
||||||
|
_clientState.LocalPlayer?.Position ?? default,
|
||||||
|
true,
|
||||||
|
hoardColor)
|
||||||
|
},
|
||||||
|
ExpiresAt = Environment.TickCount64 + RenderData.TestLayerTimeout
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public void ResetLayer(ELayer layer)
|
public void DrawLayers()
|
||||||
{
|
{
|
||||||
if (_layers.Remove(layer, out var l))
|
if (_layers.Count == 0)
|
||||||
l.Dispose();
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
public IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, uint color, bool fill = false)
|
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))
|
||||||
{
|
{
|
||||||
var config = MarkerConfig.ForType(type);
|
foreach (var layer in _layers.Values.Where(l => l.IsValid(_clientState)))
|
||||||
return new SimpleElement
|
|
||||||
{
|
{
|
||||||
Type = type,
|
foreach (var e in layer.Elements)
|
||||||
Position = pos + new Vector3(0, config.OffsetY, 0),
|
Draw(e);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.PopStyleVar();
|
foreach (var key in _layers.Where(l => !l.Value.IsValid(_clientState))
|
||||||
|
.Select(l => l.Key)
|
||||||
|
.ToList())
|
||||||
|
ResetLayer(key);
|
||||||
|
|
||||||
|
ImGui.End();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Draw(SimpleElement e)
|
ImGui.PopStyleVar();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Draw(SimpleElement e)
|
||||||
|
{
|
||||||
|
if (!e.Enabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch (e.Type)
|
||||||
{
|
{
|
||||||
if (e.Color == RenderData.ColorInvisible)
|
case MemoryLocation.EType.Hoard:
|
||||||
return;
|
// ignore distance if this is a found hoard coffer
|
||||||
|
if (_territoryState.PomanderOfIntuition == PomanderState.Active &&
|
||||||
switch (e.Type)
|
_configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander)
|
||||||
{
|
|
||||||
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;
|
||||||
}
|
|
||||||
|
|
||||||
bool onScreen = false;
|
goto case MemoryLocation.EType.Trap;
|
||||||
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);
|
case MemoryLocation.EType.Trap:
|
||||||
}
|
var playerPos = _clientState.LocalPlayer?.Position;
|
||||||
|
if (playerPos == null)
|
||||||
|
return;
|
||||||
|
|
||||||
if (onScreen)
|
if ((playerPos.Value - e.Position).Length() > 65)
|
||||||
{
|
return;
|
||||||
if (e.Fill)
|
break;
|
||||||
ImGui.GetWindowDrawList().PathFillConvex(e.Color);
|
|
||||||
else
|
|
||||||
ImGui.GetWindowDrawList().PathStroke(e.Color, ImDrawFlags.Closed, 2);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
ImGui.GetWindowDrawList().PathClear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ERenderer GetConfigValue()
|
bool onScreen = false;
|
||||||
=> ERenderer.Simple;
|
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
|
||||||
|
ImGui.GetWindowDrawList().PathStroke(e.Color, ImDrawFlags.Closed, 2);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
ImGui.GetWindowDrawList().PathClear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ERenderer GetConfigValue()
|
||||||
|
=> 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 l in _layers.Values)
|
foreach (var e in Elements)
|
||||||
l.Dispose();
|
e.IsValid = false;
|
||||||
}
|
|
||||||
|
|
||||||
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; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
internal sealed class SplatoonRenderer : IRenderer, IDisposable
|
private const long OnTerritoryChange = -2;
|
||||||
|
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
private const long OnTerritoryChange = -2;
|
_logger = logger;
|
||||||
|
_debugState = debugState;
|
||||||
|
_clientState = clientState;
|
||||||
|
_chat = chat;
|
||||||
|
|
||||||
private readonly ILogger<SplatoonRenderer> _logger;
|
_logger.LogInformation("Initializing splatoon");
|
||||||
private readonly DebugState _debugState;
|
ECommonsMain.Init(pluginInterface, dalamudPlugin, ECommons.Module.SplatoonAPI);
|
||||||
private readonly ClientState _clientState;
|
}
|
||||||
private readonly Chat _chat;
|
|
||||||
|
|
||||||
public SplatoonRenderer(
|
private bool IsDisposed { get; set; }
|
||||||
ILogger<SplatoonRenderer> logger,
|
|
||||||
DalamudPluginInterface pluginInterface,
|
|
||||||
IDalamudPlugin dalamudPlugin,
|
|
||||||
DebugState debugState,
|
|
||||||
ClientState clientState,
|
|
||||||
Chat chat)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_debugState = debugState;
|
|
||||||
_clientState = clientState;
|
|
||||||
_chat = chat;
|
|
||||||
|
|
||||||
_logger.LogInformation("Initializing splatoon");
|
public void SetLayer(ELayer layer, IReadOnlyList<IRenderElement> elements)
|
||||||
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.RemoveDynamicElements(ToLayerName(layer));
|
Splatoon.AddDynamicElements(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 reset splatoon layer {Layer}", layer);
|
_logger.LogError(e, "Could not create splatoon layer {Layer} with {Count} elements", 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)
|
||||||
|
=> $"PalacePal.{layer}";
|
||||||
|
|
||||||
|
public IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, bool enabled, uint color, bool fill = false)
|
||||||
|
{
|
||||||
|
MarkerConfig config = MarkerConfig.ForType(type);
|
||||||
|
Element element = new Element(ElementType.CircleAtFixedCoordinates)
|
||||||
|
{
|
||||||
|
refX = pos.X,
|
||||||
|
refY = pos.Z, // z and y are swapped
|
||||||
|
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);
|
||||||
|
|
||||||
|
var elements = new List<IRenderElement>
|
||||||
|
{
|
||||||
|
CreateElement(MemoryLocation.EType.Trap, pos.Value, true, trapColor),
|
||||||
|
CreateElement(MemoryLocation.EType.Hoard, pos.Value, true, hoardColor),
|
||||||
|
};
|
||||||
|
|
||||||
|
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)
|
||||||
private string ToLayerName(ELayer layer)
|
|
||||||
=> $"PalacePal.{layer}";
|
|
||||||
|
|
||||||
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,
|
|
||||||
refY = pos.Z, // z and y are swapped
|
|
||||||
refZ = pos.Y,
|
|
||||||
offX = 0,
|
|
||||||
offY = 0,
|
|
||||||
offZ = config.OffsetY,
|
|
||||||
Filled = fill,
|
|
||||||
radius = config.Radius,
|
|
||||||
FillStep = 1,
|
|
||||||
color = color,
|
|
||||||
thicc = 2,
|
|
||||||
};
|
|
||||||
return new SplatoonElement(this, element);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DrawDebugItems(uint trapColor, uint hoardColor)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Vector3? pos = _clientState.LocalPlayer?.Position;
|
var pluginManager = DalamudReflector.GetPluginManager();
|
||||||
if (pos != null)
|
IList installedPlugins =
|
||||||
|
pluginManager.GetType().GetProperty("InstalledPlugins")?.GetValue(pluginManager) as IList ??
|
||||||
|
new List<object>();
|
||||||
|
|
||||||
|
foreach (var t in installedPlugins)
|
||||||
{
|
{
|
||||||
ResetLayer(ELayer.Test);
|
AssemblyName? assemblyName =
|
||||||
|
(AssemblyName?)t.GetType().GetProperty("AssemblyName")?.GetValue(t);
|
||||||
var elements = new List<IRenderElement>
|
string? pluginName = (string?)t.GetType().GetProperty("Name")?.GetValue(t);
|
||||||
|
if (assemblyName?.Name == "Splatoon" && pluginName != "Splatoon")
|
||||||
{
|
{
|
||||||
CreateElement(MemoryLocation.EType.Trap, pos.Value, trapColor),
|
_chat.Error(
|
||||||
CreateElement(MemoryLocation.EType.Hoard, pos.Value, hoardColor),
|
$"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.");
|
||||||
if (!Splatoon.AddDynamicElements(ToLayerName(ELayer.Test),
|
return;
|
||||||
elements.Cast<SplatoonElement>().Select(x => x.Delegate).ToArray(),
|
|
||||||
new[] { Environment.TickCount64 + RenderData.TestLayerTimeout }))
|
|
||||||
{
|
|
||||||
_chat.Message("Could not draw markers :(");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
try
|
// not relevant
|
||||||
{
|
|
||||||
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?");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_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;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ERenderer GetConfigValue()
|
public Element Delegate { get; }
|
||||||
=> ERenderer.Splatoon;
|
|
||||||
|
|
||||||
public void Dispose()
|
public bool IsValid => !_renderer.IsDisposed && Delegate.IsValid();
|
||||||
|
|
||||||
|
public bool Enabled
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Disposing splatoon");
|
get => Delegate.Enabled;
|
||||||
|
set => Delegate.Enabled = 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 uint Color
|
|
||||||
{
|
|
||||||
get => Delegate.color;
|
|
||||||
set => Delegate.color = value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,38 +2,37 @@
|
|||||||
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 IQueueOnFrameworkThread
|
internal interface IHandler
|
||||||
{
|
{
|
||||||
internal interface IHandler
|
void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal abstract class Handler<T> : IHandler
|
||||||
|
where T : IQueueOnFrameworkThread
|
||||||
|
{
|
||||||
|
protected readonly ILogger<Handler<T>> _logger;
|
||||||
|
|
||||||
|
protected Handler(ILogger<Handler<T>> logger)
|
||||||
{
|
{
|
||||||
void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout);
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal abstract class Handler<T> : IHandler
|
protected abstract void Run(T queued, ref bool recreateLayout);
|
||||||
where T : IQueueOnFrameworkThread
|
|
||||||
|
public void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout)
|
||||||
{
|
{
|
||||||
protected readonly ILogger<Handler<T>> _logger;
|
if (queued is T t)
|
||||||
|
|
||||||
protected Handler(ILogger<Handler<T>> logger)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger.LogDebug("Handling {QueuedType}", queued.GetType());
|
||||||
|
Run(t, ref recreateLayout);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
protected abstract void Run(T queued, ref bool recreateLayout);
|
|
||||||
|
|
||||||
public void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout)
|
|
||||||
{
|
{
|
||||||
if (queued is T t)
|
_logger.LogError("Could not use queue handler {QueuedType}", queued.GetType());
|
||||||
{
|
|
||||||
_logger.LogDebug("Handling {QueuedType}", queued.GetType());
|
|
||||||
Run(t, ref recreateLayout);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogError("Could not use queue handler {QueuedType}", queued.GetType());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,26 +4,25 @@ 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 QueuedConfigUpdate : IQueueOnFrameworkThread
|
internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedConfigUpdate>
|
||||||
{
|
{
|
||||||
internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedConfigUpdate>
|
private readonly RenderAdapter _renderAdapter;
|
||||||
|
|
||||||
|
public Handler(
|
||||||
|
ILogger<Handler> logger,
|
||||||
|
RenderAdapter renderAdapter)
|
||||||
|
: base(logger)
|
||||||
{
|
{
|
||||||
private readonly RenderAdapter _renderAdapter;
|
_renderAdapter = renderAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
public Handler(
|
protected override void Run(QueuedConfigUpdate queued, ref bool recreateLayout)
|
||||||
ILogger<Handler> logger,
|
{
|
||||||
RenderAdapter renderAdapter)
|
_renderAdapter.ConfigUpdated();
|
||||||
: base(logger)
|
|
||||||
{
|
|
||||||
_renderAdapter = renderAdapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Run(QueuedConfigUpdate queued, ref bool recreateLayout)
|
|
||||||
{
|
|
||||||
_renderAdapter.ConfigUpdated();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,114 +10,113 @@ 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; }
|
|
||||||
private Guid ExportId { get; set; }
|
|
||||||
private int ImportedTraps { get; set; }
|
|
||||||
private int ImportedHoardCoffers { get; set; }
|
|
||||||
|
|
||||||
public QueuedImport(string sourcePath)
|
internal sealed class QueuedImport : IQueueOnFrameworkThread
|
||||||
|
{
|
||||||
|
private ExportRoot Export { get; }
|
||||||
|
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);
|
||||||
|
Export = ExportRoot.Parser.ParseFrom(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedImport>
|
||||||
|
{
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
using var input = File.OpenRead(sourcePath);
|
_serviceScopeFactory = serviceScopeFactory;
|
||||||
Export = ExportRoot.Parser.ParseFrom(input);
|
_chat = chat;
|
||||||
|
_importService = importService;
|
||||||
|
_configWindow = configWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedImport>
|
protected override void Run(QueuedImport import, ref bool recreateLayout)
|
||||||
{
|
{
|
||||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
recreateLayout = true;
|
||||||
private readonly Chat _chat;
|
|
||||||
private readonly ImportService _importService;
|
|
||||||
private readonly ConfigWindow _configWindow;
|
|
||||||
|
|
||||||
public Handler(
|
try
|
||||||
ILogger<Handler> logger,
|
|
||||||
IServiceScopeFactory serviceScopeFactory,
|
|
||||||
Chat chat,
|
|
||||||
ImportService importService,
|
|
||||||
ConfigWindow configWindow)
|
|
||||||
: base(logger)
|
|
||||||
{
|
{
|
||||||
_serviceScopeFactory = serviceScopeFactory;
|
if (!Validate(import))
|
||||||
_chat = chat;
|
return;
|
||||||
_importService = importService;
|
|
||||||
_configWindow = configWindow;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Run(QueuedImport import, ref bool recreateLayout)
|
Task.Run(() =>
|
||||||
{
|
|
||||||
recreateLayout = true;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
if (!Validate(import))
|
try
|
||||||
return;
|
|
||||||
|
|
||||||
Task.Run(() =>
|
|
||||||
{
|
{
|
||||||
try
|
using (var scope = _serviceScopeFactory.CreateScope())
|
||||||
{
|
{
|
||||||
using (var scope = _serviceScopeFactory.CreateScope())
|
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
|
||||||
{
|
(import.ImportedTraps, import.ImportedHoardCoffers) =
|
||||||
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
|
_importService.Import(import.Export);
|
||||||
(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");
|
|
||||||
_chat.Error(string.Format(Localization.Error_ImportFailed, e));
|
_logger.LogInformation(
|
||||||
}
|
"Imported {ExportId} for {Traps} traps, {Hoard} hoard coffers", import.ExportId,
|
||||||
});
|
import.ImportedTraps, import.ImportedHoardCoffers);
|
||||||
}
|
_chat.Message(string.Format(Localization.ImportCompleteStatistics, import.ImportedTraps,
|
||||||
catch (Exception e)
|
import.ImportedHoardCoffers));
|
||||||
{
|
}
|
||||||
_logger.LogError(e, "Import failed");
|
catch (Exception e)
|
||||||
_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)
|
|
||||||
{
|
{
|
||||||
if (import.Export.ExportVersion != ExportConfig.ExportVersion)
|
_logger.LogError(e, "Import failed");
|
||||||
{
|
_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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,151 +11,150 @@ 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
|
||||||
{
|
{
|
||||||
internal sealed class QueuedSyncResponse : IQueueOnFrameworkThread
|
public required SyncType Type { get; init; }
|
||||||
|
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>
|
||||||
{
|
{
|
||||||
public required SyncType Type { get; init; }
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
public required ushort TerritoryType { get; init; }
|
private readonly IPalacePalConfiguration _configuration;
|
||||||
public required bool Success { get; init; }
|
private readonly FloorService _floorService;
|
||||||
public required IReadOnlyList<PersistentLocation> Locations { get; init; }
|
private readonly TerritoryState _territoryState;
|
||||||
|
private readonly DebugState _debugState;
|
||||||
|
|
||||||
internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedSyncResponse>
|
public Handler(
|
||||||
|
ILogger<Handler> logger,
|
||||||
|
IServiceScopeFactory serviceScopeFactory,
|
||||||
|
IPalacePalConfiguration configuration,
|
||||||
|
FloorService floorService,
|
||||||
|
TerritoryState territoryState,
|
||||||
|
DebugState debugState)
|
||||||
|
: base(logger)
|
||||||
{
|
{
|
||||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
_serviceScopeFactory = serviceScopeFactory;
|
||||||
private readonly IPalacePalConfiguration _configuration;
|
_configuration = configuration;
|
||||||
private readonly FloorService _floorService;
|
_floorService = floorService;
|
||||||
private readonly TerritoryState _territoryState;
|
_territoryState = territoryState;
|
||||||
private readonly DebugState _debugState;
|
_debugState = debugState;
|
||||||
|
}
|
||||||
|
|
||||||
public Handler(
|
protected override void Run(QueuedSyncResponse queued, ref bool recreateLayout)
|
||||||
ILogger<Handler> logger,
|
{
|
||||||
IServiceScopeFactory serviceScopeFactory,
|
recreateLayout = true;
|
||||||
IPalacePalConfiguration configuration,
|
|
||||||
FloorService floorService,
|
_logger.LogDebug(
|
||||||
TerritoryState territoryState,
|
"Sync response for territory {Territory} of type {Type}, success = {Success}, response objects = {Count}",
|
||||||
DebugState debugState)
|
(ETerritoryType)queued.TerritoryType, queued.Type, queued.Success, queued.Locations.Count);
|
||||||
: base(logger)
|
var memoryTerritory = _floorService.GetTerritoryIfReady(queued.TerritoryType);
|
||||||
|
if (memoryTerritory == null)
|
||||||
{
|
{
|
||||||
_serviceScopeFactory = serviceScopeFactory;
|
_logger.LogWarning("Discarding sync response for territory {Territory} as it isn't ready",
|
||||||
_configuration = configuration;
|
(ETerritoryType)queued.TerritoryType);
|
||||||
_floorService = floorService;
|
return;
|
||||||
_territoryState = territoryState;
|
|
||||||
_debugState = debugState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Run(QueuedSyncResponse queued, ref bool recreateLayout)
|
try
|
||||||
{
|
{
|
||||||
recreateLayout = true;
|
var remoteMarkers = queued.Locations;
|
||||||
|
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)
|
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Discarding sync response for territory {Territory} as it isn't ready",
|
switch (queued.Type)
|
||||||
(ETerritoryType)queued.TerritoryType);
|
{
|
||||||
|
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;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
try
|
if (queued.Type == SyncType.Download)
|
||||||
{
|
{
|
||||||
var remoteMarkers = queued.Locations;
|
if (queued.Success)
|
||||||
if (_configuration.Mode == EMode.Online && queued.Success && remoteMarkers.Count > 0)
|
memoryTerritory.SyncState = ESyncState.Complete;
|
||||||
{
|
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,
|
{
|
||||||
NotNeeded,
|
NotAttempted,
|
||||||
Started,
|
NotNeeded,
|
||||||
Complete,
|
Started,
|
||||||
Failed,
|
Complete,
|
||||||
}
|
Failed,
|
||||||
|
}
|
||||||
public enum SyncType
|
|
||||||
{
|
public enum SyncType
|
||||||
Upload,
|
{
|
||||||
Download,
|
Upload,
|
||||||
MarkSeen,
|
Download,
|
||||||
}
|
MarkSeen,
|
||||||
}
|
}
|
||||||
|
@ -7,36 +7,35 @@ 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
|
||||||
{
|
{
|
||||||
internal sealed class QueuedUndoImport : IQueueOnFrameworkThread
|
public QueuedUndoImport(Guid exportId)
|
||||||
{
|
{
|
||||||
public QueuedUndoImport(Guid exportId)
|
ExportId = 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)
|
||||||
{
|
{
|
||||||
ExportId = exportId;
|
_importService = importService;
|
||||||
|
_configWindow = configWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Guid ExportId { get; }
|
protected override void Run(QueuedUndoImport queued, ref bool recreateLayout)
|
||||||
|
|
||||||
internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedUndoImport>
|
|
||||||
{
|
{
|
||||||
private readonly ImportService _importService;
|
recreateLayout = true;
|
||||||
private readonly ConfigWindow _configWindow;
|
|
||||||
|
|
||||||
public Handler(ILogger<Handler> logger, ImportService importService, ConfigWindow configWindow)
|
_importService.RemoveById(queued.ExportId);
|
||||||
: base(logger)
|
_configWindow.UpdateLastImport();
|
||||||
{
|
|
||||||
_importService = importService;
|
|
||||||
_configWindow = configWindow;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Run(QueuedUndoImport queued, ref bool recreateLayout)
|
|
||||||
{
|
|
||||||
recreateLayout = true;
|
|
||||||
|
|
||||||
_importService.RemoveById(queued.ExportId);
|
|
||||||
_configWindow.UpdateLastImport();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,98 +8,97 @@ 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
|
||||||
{
|
{
|
||||||
internal sealed class AgreementWindow : Window, IDisposable, ILanguageChanged
|
private const string WindowId = "###PalPalaceAgreement";
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
private const string WindowId = "###PalPalaceAgreement";
|
_windowSystem = windowSystem;
|
||||||
private readonly WindowSystem _windowSystem;
|
_configurationManager = configurationManager;
|
||||||
private readonly ConfigurationManager _configurationManager;
|
_configuration = configuration;
|
||||||
private readonly IPalacePalConfiguration _configuration;
|
|
||||||
private int _choice;
|
|
||||||
|
|
||||||
public AgreementWindow(
|
LanguageChanged();
|
||||||
WindowSystem windowSystem,
|
|
||||||
ConfigurationManager configurationManager,
|
Flags = ImGuiWindowFlags.NoCollapse;
|
||||||
IPalacePalConfiguration configuration)
|
Size = new Vector2(500, 500);
|
||||||
: base(WindowId)
|
SizeCondition = ImGuiCond.FirstUseEver;
|
||||||
|
PositionCondition = ImGuiCond.FirstUseEver;
|
||||||
|
Position = new Vector2(310, 310);
|
||||||
|
|
||||||
|
SizeConstraints = new WindowSizeConstraints
|
||||||
{
|
{
|
||||||
_windowSystem = windowSystem;
|
MinimumSize = new Vector2(500, 500),
|
||||||
_configurationManager = configurationManager;
|
MaximumSize = new Vector2(2000, 2000),
|
||||||
_configuration = configuration;
|
};
|
||||||
|
|
||||||
LanguageChanged();
|
IsOpen = configuration.FirstUse;
|
||||||
|
_windowSystem.AddWindow(this);
|
||||||
|
}
|
||||||
|
|
||||||
Flags = ImGuiWindowFlags.NoCollapse;
|
public void Dispose()
|
||||||
Size = new Vector2(500, 500);
|
=> _windowSystem.RemoveWindow(this);
|
||||||
SizeCondition = ImGuiCond.FirstUseEver;
|
|
||||||
PositionCondition = ImGuiCond.FirstUseEver;
|
|
||||||
Position = new Vector2(310, 310);
|
|
||||||
|
|
||||||
SizeConstraints = new WindowSizeConstraints
|
public void LanguageChanged()
|
||||||
{
|
=> WindowName = $"{Localization.Palace_Pal}{WindowId}";
|
||||||
MinimumSize = new Vector2(500, 500),
|
|
||||||
MaximumSize = new Vector2(2000, 2000),
|
|
||||||
};
|
|
||||||
|
|
||||||
IsOpen = configuration.FirstUse;
|
public override void OnOpen()
|
||||||
_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;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
ImGui.EndDisabled();
|
||||||
=> _windowSystem.RemoveWindow(this);
|
|
||||||
|
|
||||||
public void LanguageChanged()
|
ImGui.Separator();
|
||||||
=> WindowName = $"{Localization.Palace_Pal}{WindowId}";
|
|
||||||
|
|
||||||
public override void OnOpen()
|
if (ImGui.Button(Localization.Agreement_ViewPluginAndServerSourceCode))
|
||||||
{
|
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
@ -8,119 +8,118 @@ 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
|
||||||
{
|
{
|
||||||
internal sealed class StatisticsWindow : Window, IDisposable, ILanguageChanged
|
private const string WindowId = "###PalacePalStats";
|
||||||
|
private readonly WindowSystem _windowSystem;
|
||||||
|
private readonly SortedDictionary<ETerritoryType, TerritoryStatistics> _territoryStatistics = new();
|
||||||
|
|
||||||
|
public StatisticsWindow(WindowSystem windowSystem)
|
||||||
|
: base(WindowId)
|
||||||
{
|
{
|
||||||
private const string WindowId = "###PalacePalStats";
|
_windowSystem = windowSystem;
|
||||||
private readonly WindowSystem _windowSystem;
|
|
||||||
private readonly SortedDictionary<ETerritoryType, TerritoryStatistics> _territoryStatistics = new();
|
|
||||||
|
|
||||||
public StatisticsWindow(WindowSystem windowSystem)
|
LanguageChanged();
|
||||||
: base(WindowId)
|
|
||||||
|
Size = new Vector2(500, 500);
|
||||||
|
SizeCondition = ImGuiCond.FirstUseEver;
|
||||||
|
Flags = ImGuiWindowFlags.AlwaysAutoResize;
|
||||||
|
|
||||||
|
foreach (ETerritoryType territory in typeof(ETerritoryType).GetEnumValues())
|
||||||
{
|
{
|
||||||
_windowSystem = windowSystem;
|
_territoryStatistics[territory] = new TerritoryStatistics(territory.ToString());
|
||||||
|
|
||||||
LanguageChanged();
|
|
||||||
|
|
||||||
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.AddWindow(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
_windowSystem.AddWindow(this);
|
||||||
=> _windowSystem.RemoveWindow(this);
|
}
|
||||||
|
|
||||||
public void LanguageChanged()
|
public void Dispose()
|
||||||
=> WindowName = $"{Localization.Palace_Pal} - {Localization.Statistics}{WindowId}";
|
=> _windowSystem.RemoveWindow(this);
|
||||||
|
|
||||||
public override void Draw()
|
public void LanguageChanged()
|
||||||
|
=> WindowName = $"{Localization.Palace_Pal} - {Localization.Statistics}{WindowId}";
|
||||||
|
|
||||||
|
public override void Draw()
|
||||||
|
{
|
||||||
|
if (ImGui.BeginTabBar("Tabs"))
|
||||||
{
|
{
|
||||||
if (ImGui.BeginTabBar("Tabs"))
|
DrawDungeonStats("Palace of the Dead", Localization.PalaceOfTheDead, ETerritoryType.Palace_1_10,
|
||||||
{
|
ETerritoryType.Palace_191_200);
|
||||||
DrawDungeonStats("Palace of the Dead", Localization.PalaceOfTheDead, ETerritoryType.Palace_1_10,
|
DrawDungeonStats("Heaven on High", Localization.HeavenOnHigh, ETerritoryType.HeavenOnHigh_1_10,
|
||||||
ETerritoryType.Palace_191_200);
|
ETerritoryType.HeavenOnHigh_91_100);
|
||||||
DrawDungeonStats("Heaven on High", Localization.HeavenOnHigh, ETerritoryType.HeavenOnHigh_1_10,
|
DrawDungeonStats("Eureka Orthos", Localization.EurekaOrthos, ETerritoryType.EurekaOrthos_1_10,
|
||||||
ETerritoryType.HeavenOnHigh_91_100);
|
ETerritoryType.EurekaOrthos_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)
|
private void DrawDungeonStats(string id, string name, ETerritoryType minTerritory, ETerritoryType maxTerritory)
|
||||||
|
{
|
||||||
|
if (ImGui.BeginTabItem($"{name}###{id}"))
|
||||||
{
|
{
|
||||||
if (ImGui.BeginTabItem($"{name}###{id}"))
|
if (ImGui.BeginTable($"TrapHoardStatistics{id}", 4,
|
||||||
|
ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable))
|
||||||
{
|
{
|
||||||
if (ImGui.BeginTable($"TrapHoardStatistics{id}", 4,
|
ImGui.TableSetupColumn(Localization.Statistics_TerritoryId);
|
||||||
ImGuiTableFlags.Borders | ImGuiTableFlags.Resizable))
|
ImGui.TableSetupColumn(Localization.Statistics_InstanceName);
|
||||||
|
ImGui.TableSetupColumn(Localization.Statistics_Traps);
|
||||||
|
ImGui.TableSetupColumn(Localization.Statistics_HoardCoffers);
|
||||||
|
ImGui.TableHeadersRow();
|
||||||
|
|
||||||
|
foreach (var (territoryType, stats) in _territoryStatistics
|
||||||
|
.Where(x => x.Key >= minTerritory && x.Key <= maxTerritory)
|
||||||
|
.OrderBy(x => x.Key.GetOrder() ?? (int)x.Key))
|
||||||
{
|
{
|
||||||
ImGui.TableSetupColumn(Localization.Statistics_TerritoryId);
|
ImGui.TableNextRow();
|
||||||
ImGui.TableSetupColumn(Localization.Statistics_InstanceName);
|
if (ImGui.TableNextColumn())
|
||||||
ImGui.TableSetupColumn(Localization.Statistics_Traps);
|
ImGui.Text($"{(uint)territoryType}");
|
||||||
ImGui.TableSetupColumn(Localization.Statistics_HoardCoffers);
|
|
||||||
ImGui.TableHeadersRow();
|
|
||||||
|
|
||||||
foreach (var (territoryType, stats) in _territoryStatistics
|
if (ImGui.TableNextColumn())
|
||||||
.Where(x => x.Key >= minTerritory && x.Key <= maxTerritory)
|
ImGui.Text(stats.TerritoryName);
|
||||||
.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.TerritoryName);
|
ImGui.Text(stats.TrapCount?.ToString() ?? "-");
|
||||||
|
|
||||||
if (ImGui.TableNextColumn())
|
if (ImGui.TableNextColumn())
|
||||||
ImGui.Text(stats.TrapCount?.ToString() ?? "-");
|
ImGui.Text(stats.HoardCofferCount?.ToString() ?? "-");
|
||||||
|
|
||||||
if (ImGui.TableNextColumn())
|
|
||||||
ImGui.Text(stats.HoardCofferCount?.ToString() ?? "-");
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.EndTable();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.EndTabItem();
|
ImGui.EndTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImGui.EndTabItem();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SetFloorData(IEnumerable<FloorStatistics> floorStatistics)
|
||||||
|
{
|
||||||
|
foreach (var territoryStatistics in _territoryStatistics.Values)
|
||||||
|
{
|
||||||
|
territoryStatistics.TrapCount = null;
|
||||||
|
territoryStatistics.HoardCofferCount = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void SetFloorData(IEnumerable<FloorStatistics> floorStatistics)
|
foreach (var floor in floorStatistics)
|
||||||
{
|
{
|
||||||
foreach (var territoryStatistics in _territoryStatistics.Values)
|
if (_territoryStatistics.TryGetValue((ETerritoryType)floor.TerritoryType,
|
||||||
|
out TerritoryStatistics? territoryStatistics))
|
||||||
{
|
{
|
||||||
territoryStatistics.TrapCount = null;
|
territoryStatistics.TrapCount = floor.TrapCount;
|
||||||
territoryStatistics.HoardCofferCount = null;
|
territoryStatistics.HoardCofferCount = floor.HoardCount;
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
{
|
|
||||||
public string TerritoryName { get; }
|
|
||||||
public uint? TrapCount { get; set; }
|
|
||||||
public uint? HoardCofferCount { get; set; }
|
|
||||||
|
|
||||||
public TerritoryStatistics(string territoryName)
|
|
||||||
{
|
|
||||||
TerritoryName = territoryName;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class TerritoryStatistics
|
||||||
|
{
|
||||||
|
public string TerritoryName { get; }
|
||||||
|
public uint? TrapCount { get; set; }
|
||||||
|
public uint? HoardCofferCount { get; set; }
|
||||||
|
|
||||||
|
public TerritoryStatistics(string territoryName)
|
||||||
|
{
|
||||||
|
TerritoryName = territoryName;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,280 +1,302 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"net7.0-windows7.0": {
|
"net8.0-windows7.0": {
|
||||||
"Dalamud.Extensions.MicrosoftLogging": {
|
"Dalamud.Extensions.MicrosoftLogging": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[1.0.0, )",
|
"requested": "[4.0.1, )",
|
||||||
"resolved": "1.0.0",
|
"resolved": "4.0.1",
|
||||||
"contentHash": "nPjMrT9n9GJ+TYF1lyVhlvhmFyN4ajMX2ccclgyMc8MNpOGZwxrJ4VEtrUUk7UkuX2wAhtnNsjrcf5sER3/CbA==",
|
"contentHash": "fMEL2ajtF/30SBBku7vMyG0yye5eHN/A9fgT//1CEjUth/Wz2CYco5Ehye21T8KN1IuAPwoqJuu49rB71j+8ug==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.Logging": "7.0.0"
|
"Microsoft.Extensions.Logging": "8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DalamudPackager": {
|
"DalamudPackager": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[2.1.11, )",
|
"requested": "[2.1.13, )",
|
||||||
"resolved": "2.1.11",
|
"resolved": "2.1.13",
|
||||||
"contentHash": "9qlAWoRRTiL/geAvuwR/g6Bcbrd/bJJgVnB/RurBiyKs6srsP0bvpoo8IK+Eg8EA6jWeM6/YJWs66w4FIAzqPw=="
|
"contentHash": "rMN1omGe8536f4xLMvx9NwfvpAc9YFFfeXJ1t4P4PE6Gu8WCIoFliR1sh07hM+bfODmesk/dvMbji7vNI+B/pQ=="
|
||||||
},
|
},
|
||||||
"GitInfo": {
|
"DotNet.ReproducibleBuilds": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[2.3.0, )",
|
"requested": "[1.1.1, )",
|
||||||
"resolved": "2.3.0",
|
"resolved": "1.1.1",
|
||||||
"contentHash": "LdnsKNdwQvdDvpPYQuoGjXML75dY7NybKRe+qlkPPQaTY4dE5Fy8VCrD8YBhXO0fH/5xnmvKeSq4yztzg5KY0Q=="
|
"contentHash": "+H2t/t34h6mhEoUvHi8yGXyuZ2GjSovcGYehJrS2MDm2XgmPfZL2Sdxg+uL2lKgZ4M6tTwKHIlxOob2bgh0NRQ==",
|
||||||
|
"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.22.1, )",
|
"requested": "[3.27.2, )",
|
||||||
"resolved": "3.22.1",
|
"resolved": "3.27.2",
|
||||||
"contentHash": "Ul4gVJWLya83Z8/n3+O4QKhD8ukCCwNLDyoWpUdJSnmzxRe8o3pWiuCzzvN2z/LVH60nozlKpTzhJo3ctI+G4Q=="
|
"contentHash": "0wdgA3LO9mBS477jieBFs4pU1sWhVtwv/P+i9nAEiFDQyUA7PPHDBbJL1CeqYtV18jLiq9og4n7wSVCO171OBg=="
|
||||||
},
|
},
|
||||||
"Grpc.Net.Client": {
|
"Grpc.Net.Client": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[2.52.0, )",
|
"requested": "[2.63.0, )",
|
||||||
"resolved": "2.52.0",
|
"resolved": "2.63.0",
|
||||||
"contentHash": "hWVH9g/Nnjz40ni//2S8UIOyEmhueQREoZIkD0zKHEPqLxXcNlbp4eebXIOicZtkwDSx0TFz9NpkbecEDn6rBw==",
|
"contentHash": "847zG24daOP1242OpbnjhbKtplH/EfV/76QReQA3cbS5SL78uIXsWMe9IN9JlIb4+kT3eE4fjMCXTn8BAQ91Ng==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Grpc.Net.Common": "2.52.0",
|
"Grpc.Net.Common": "2.63.0",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "3.0.3"
|
"Microsoft.Extensions.Logging.Abstractions": "6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Grpc.Tools": {
|
"Grpc.Tools": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[2.53.0, )",
|
"requested": "[2.64.0, )",
|
||||||
"resolved": "2.53.0",
|
"resolved": "2.64.0",
|
||||||
"contentHash": "vm8iRSAF/4PN9g555iYZwhCQptSE4cZ8xk5W1TQ+JcHwaHSrBhD+P6H4l0+SqqfzuX7sGpjjOMQJXHSyrERTgw=="
|
"contentHash": "W5RrhDFHUhioASktxfuDs5fTjWUxwegljZAig9zFL8nWNskeyQA6OXN2choWKYxGrljer25VqCJCMbWz7XHvqg=="
|
||||||
},
|
},
|
||||||
"Microsoft.EntityFrameworkCore.Sqlite": {
|
"Microsoft.EntityFrameworkCore.Sqlite": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[7.0.4, )",
|
"requested": "[8.0.6, )",
|
||||||
"resolved": "7.0.4",
|
"resolved": "8.0.6",
|
||||||
"contentHash": "d1cIR5upwzTZmzycqWEoxfso5b3qD0G43IeECtfeMSPoG8JD4OJHHtbun0wS9RzwAORMa/4Zb3vuogTYY3mtaQ==",
|
"contentHash": "nC4cZN4zReTb22qd9WDU0eDmlXvkyf2g2pqQ3VIHJbkpJcdWSY/PDgwGpbpShsVcAjXbkjGiUcv9aGwa61xQPw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.EntityFrameworkCore.Sqlite.Core": "7.0.4",
|
"Microsoft.EntityFrameworkCore.Sqlite.Core": "8.0.6",
|
||||||
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.4"
|
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.6"
|
||||||
}
|
|
||||||
},
|
|
||||||
"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": "[7.0.0, )",
|
"requested": "[8.0.0, )",
|
||||||
"resolved": "7.0.0",
|
"resolved": "8.0.0",
|
||||||
"contentHash": "Nw2muoNrOG5U5qa2ZekXwudUn2BJcD41e65zwmDHb1fQegTX66UokLWZkJRpqSSHXDOWZ5V0iqhbxOEky91atA==",
|
"contentHash": "tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "7.0.0",
|
"Microsoft.Extensions.DependencyInjection": "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.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": "[7.0.1, )",
|
"requested": "[8.0.0, )",
|
||||||
"resolved": "7.0.1",
|
"resolved": "8.0.0",
|
||||||
"contentHash": "3evI3sBfKqwYSwuBcYgShbmEgtXcg8N5Qu+jExLdkBXPty2yGDXq5m1/4sx9Exb8dqdeMPUs/d9DQ0wy/9Adwg=="
|
"contentHash": "+TUFINV2q2ifyXauQXRwy4CiBhqvDEDZeVJU7qfxya4aRYOKzVBpN+4acx25VcPB9ywUN6C0n8drWl110PhZEg=="
|
||||||
},
|
},
|
||||||
"Grpc.Core.Api": {
|
"Grpc.Core.Api": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.52.0",
|
"resolved": "2.63.0",
|
||||||
"contentHash": "SQiPyBczG4vKPmI6Fd+O58GcxxDSFr6nfRAJuBDUNj+PgdokhjWJvZE/La1c09AkL2FVm/jrDloG89nkzmVF7A==",
|
"contentHash": "t3+/MF8AxIqKq5UmPB9EWAnM9C/+lXOB8TRFfeVMDntf6dekfJmjpKDebaT4t2bbuwVwwvthxxox9BuGr59kYA=="
|
||||||
"dependencies": {
|
|
||||||
"System.Memory": "4.5.3"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"Grpc.Net.Common": {
|
"Grpc.Net.Common": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.52.0",
|
"resolved": "2.63.0",
|
||||||
"contentHash": "di9qzpdx525IxumZdYmu6sG2y/gXJyYeZ1ruFUzB9BJ1nj4kU1/dTAioNCMt1VLRvNVDqh8S8B1oBdKhHJ4xRg==",
|
"contentHash": "RLt6p31ZMsXRcHNeu1dQuIFLYZvnwP6LUzoDPlV3KoR4w9btmwrXIvz9Jbp1SOmxW7nXw9zShAeIt5LsqFAx5w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Grpc.Core.Api": "2.52.0"
|
"Grpc.Core.Api": "2.63.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Humanizer.Core": {
|
"Microsoft.Build.Tasks.Git": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.14.1",
|
"resolved": "8.0.0",
|
||||||
"contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw=="
|
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
|
||||||
},
|
},
|
||||||
"Microsoft.Data.Sqlite.Core": {
|
"Microsoft.Data.Sqlite.Core": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "7.0.4",
|
"resolved": "8.0.6",
|
||||||
"contentHash": "AUBM1KZ7EvmkYhC/ECXL4cjx+q55DJ3lmSf0NwAyRNArubNPRdroGono5uN6aW7Kqp+IUZwEK0Ywd1Gh7FDM2A==",
|
"contentHash": "umhZ0ZF2RI81rGFTnYmCxI+Euj4Aqe/6Y4+8CxN9OVJNGDNIqB5laJ3wxQTU8zXCcm2k9F7FL+/6RVoOT4z1Fw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"SQLitePCLRaw.core": "2.1.4"
|
"SQLitePCLRaw.core": "2.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.EntityFrameworkCore": {
|
"Microsoft.EntityFrameworkCore": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "7.0.4",
|
"resolved": "8.0.6",
|
||||||
"contentHash": "eNcsY3rft5ERJJcen80Jyg57EScjWZmvhwmFLYXmEOTdVqHG+wQZiMOXnO1b5RH3u2qTQq+Tpci7KGfLAG5Gtg==",
|
"contentHash": "Ms5e5QuBAjVIuQsGumeLvkgMiOpnj6wxPvwBIoe1NfTkseWK4NZYztnhgDlpkCPkrUmJEXLv69kl349Ours30Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.EntityFrameworkCore.Abstractions": "7.0.4",
|
"Microsoft.EntityFrameworkCore.Abstractions": "8.0.6",
|
||||||
"Microsoft.EntityFrameworkCore.Analyzers": "7.0.4",
|
"Microsoft.EntityFrameworkCore.Analyzers": "8.0.6",
|
||||||
"Microsoft.Extensions.Caching.Memory": "7.0.0",
|
"Microsoft.Extensions.Caching.Memory": "8.0.0",
|
||||||
"Microsoft.Extensions.DependencyInjection": "7.0.0",
|
"Microsoft.Extensions.Logging": "8.0.0"
|
||||||
"Microsoft.Extensions.Logging": "7.0.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.EntityFrameworkCore.Abstractions": {
|
"Microsoft.EntityFrameworkCore.Abstractions": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "7.0.4",
|
"resolved": "8.0.6",
|
||||||
"contentHash": "6GbYvs4L5oFpYpMzwF05kdDgvX09UmMX7MpDtDlGI5ymijFwquwv+yvdijbtodOuu0yLUpc4n71x6eBdJ8v1xQ=="
|
"contentHash": "X7wSSBNFRuN8j8M9HDYG7rPpEeyhY+PdJZR9rftmgvsZH0eK5+bZ3b3As8iO4rLEpjsBzDnrgSIY6q2F3HQatw=="
|
||||||
},
|
},
|
||||||
"Microsoft.EntityFrameworkCore.Analyzers": {
|
"Microsoft.EntityFrameworkCore.Analyzers": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "7.0.4",
|
"resolved": "8.0.6",
|
||||||
"contentHash": "YRD4bViuaEPEsaBIL52DzXGzLCt3jYoE3wztYEW1QZYDl89hQ+ca0nvBO2mnMHmCXpU/2wlErrUyDp4x5B/3mg=="
|
"contentHash": "fDNtuQ4lAaPaCOlsrwUck/GvnF4QLeDpMmE1L5QtxZpMSmWfnL2/vk8sDL9OVTWcfprooI9V5MNpIx3/Tq5ehg=="
|
||||||
},
|
|
||||||
"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": "7.0.4",
|
"resolved": "8.0.6",
|
||||||
"contentHash": "L41+VonK6L0IurFHopoe5yY+m3MD26OMocKLPPR/XKxnazzZUcGPz0IGJpVnwpZyKVPfEIAnD5vmm60meYr1NA==",
|
"contentHash": "chhfmLusCGLGvNYtvMji6KGQlduPDnJsStG/LjS8qJhFWJDDzTZpSr2LHowewcxMrMo/Axc6Jwe+WwSi/vlkTg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.EntityFrameworkCore": "7.0.4",
|
"Microsoft.EntityFrameworkCore": "8.0.6",
|
||||||
"Microsoft.Extensions.Configuration.Abstractions": "7.0.0"
|
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.EntityFrameworkCore.Sqlite.Core": {
|
"Microsoft.EntityFrameworkCore.Sqlite.Core": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "7.0.4",
|
"resolved": "8.0.6",
|
||||||
"contentHash": "FeuV57+U4A4DO018Jy5Wkv0uYNZhyFVUUdwyVYz8TMghsZAj+3i+fOeFtD/jAWWMzDOFOF7eMni3YqLA+ufu9Q==",
|
"contentHash": "87xfPtqSouxWWdynYZv/rubd0rOUeiN9+XeoMWQzpZm/5svH1TuvzFODGIY0zKuXS18NiOFyHl9N6///eaEs/Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Data.Sqlite.Core": "7.0.4",
|
"Microsoft.Data.Sqlite.Core": "8.0.6",
|
||||||
"Microsoft.EntityFrameworkCore.Relational": "7.0.4",
|
"Microsoft.EntityFrameworkCore.Relational": "8.0.6",
|
||||||
"Microsoft.Extensions.DependencyModel": "7.0.0"
|
"Microsoft.Extensions.DependencyModel": "8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.Extensions.Caching.Abstractions": {
|
"Microsoft.Extensions.Caching.Abstractions": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "7.0.0",
|
"resolved": "8.0.0",
|
||||||
"contentHash": "IeimUd0TNbhB4ded3AbgBLQv2SnsiVugDyGV1MvspQFVlA07nDC7Zul7kcwH5jWN3JiTcp/ySE83AIJo8yfKjg==",
|
"contentHash": "3KuSxeHoNYdxVYfg2IRZCThcrlJ1XJqIXkAWikCsbm5C/bCjv7G0WoKDyuR98Q+T607QT2Zl5GsbGRkENcV2yQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.Primitives": "7.0.0"
|
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.Extensions.Caching.Memory": {
|
"Microsoft.Extensions.Caching.Memory": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "7.0.0",
|
"resolved": "8.0.0",
|
||||||
"contentHash": "xpidBs2KCE2gw1JrD0quHE72kvCaI3xFql5/Peb2GRtUuZX+dYPoK/NTdVMiM67Svym0M0Df9A3xyU0FbMQhHw==",
|
"contentHash": "7pqivmrZDzo1ADPkRwjy+8jtRKWRCPag9qPI+p7sgu7Q4QreWhcvbiWXsbhP+yY8XSiDvZpu2/LWdBv7PnmOpQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.Caching.Abstractions": "7.0.0",
|
"Microsoft.Extensions.Caching.Abstractions": "8.0.0",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "7.0.0",
|
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
|
||||||
"Microsoft.Extensions.Options": "7.0.0",
|
"Microsoft.Extensions.Options": "8.0.0",
|
||||||
"Microsoft.Extensions.Primitives": "7.0.0"
|
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.Extensions.Configuration.Abstractions": {
|
"Microsoft.Extensions.Configuration.Abstractions": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "7.0.0",
|
"resolved": "8.0.0",
|
||||||
"contentHash": "f34u2eaqIjNO9YLHBz8rozVZ+TcFiFs0F3r7nUJd7FRkVSxk8u4OpoK226mi49MwexHOR2ibP9MFvRUaLilcQQ==",
|
"contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.Primitives": "7.0.0"
|
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.Extensions.DependencyInjection": {
|
"Microsoft.Extensions.DependencyInjection": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "7.0.0",
|
"resolved": "8.0.0",
|
||||||
"contentHash": "elNeOmkeX3eDVG6pYVeV82p29hr+UKDaBhrZyWvWLw/EVZSYEkZlQdkp0V39k/Xehs2Qa0mvoCvkVj3eQxNQ1Q==",
|
"contentHash": "V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0"
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "7.0.0",
|
"resolved": "8.0.0",
|
||||||
"contentHash": "h3j/QfmFN4S0w4C2A6X7arXij/M/OVw3uQHSOFxnND4DyAzO1F9eMX7Eti7lU/OkSthEE0WzRsfT/Dmx86jzCw=="
|
"contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg=="
|
||||||
},
|
},
|
||||||
"Microsoft.Extensions.DependencyModel": {
|
"Microsoft.Extensions.DependencyModel": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "7.0.0",
|
"resolved": "8.0.0",
|
||||||
"contentHash": "oONNYd71J3LzkWc4fUHl3SvMfiQMYUCo/mDHDEu76hYYxdhdrPYv6fvGv9nnKVyhE9P0h20AU8RZB5OOWQcAXg==",
|
"contentHash": "NSmDw3K0ozNDgShSIpsZcbFIzBX4w28nDag+TfaQujkXGazBm+lid5onlWoCBy4VsLxqnnKjEBbGSJVWJMf43g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"System.Text.Encodings.Web": "7.0.0",
|
"System.Text.Encodings.Web": "8.0.0",
|
||||||
"System.Text.Json": "7.0.0"
|
"System.Text.Json": "8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.Extensions.Logging.Abstractions": {
|
"Microsoft.Extensions.Logging.Abstractions": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "7.0.0",
|
"resolved": "8.0.0",
|
||||||
"contentHash": "kmn78+LPVMOWeITUjIlfxUPDsI0R6G0RkeAMBmQxAJ7vBJn4q2dTva7pWi65ceN5vPGjJ9q/Uae2WKgvfktJAw=="
|
"contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.Extensions.Options": {
|
"Microsoft.Extensions.Options": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "7.0.0",
|
"resolved": "8.0.0",
|
||||||
"contentHash": "lP1yBnTTU42cKpMozuafbvNtQ7QcBjr/CcK3bYOGEMH55Fjt+iecXjT6chR7vbgCMqy3PG3aNQSZgo/EuY/9qQ==",
|
"contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
|
||||||
"Microsoft.Extensions.Primitives": "7.0.0"
|
"Microsoft.Extensions.Primitives": "8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Microsoft.Extensions.Primitives": {
|
"Microsoft.Extensions.Primitives": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "7.0.0",
|
"resolved": "8.0.0",
|
||||||
"contentHash": "um1KU5kxcRp3CNuI8o/GrZtD4AIOXDk+RLsytjZ9QPok3ttLUelLKpilVPuaFT3TFjOhSibUAso0odbOaCDj3Q=="
|
"contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g=="
|
||||||
},
|
},
|
||||||
"Mono.TextTemplating": {
|
"Microsoft.SourceLink.AzureRepos.Git": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.2.1",
|
"resolved": "1.1.1",
|
||||||
"contentHash": "KZYeKBET/2Z0gY1WlTAK7+RHTl7GSbtvTLDXEZZojUdAPqpQNDL6tHv7VUpqfX5VEOh+uRGKaZXkuD253nEOBQ==",
|
"contentHash": "qB5urvw9LO2bG3eVAkuL+2ughxz2rR7aYgm2iyrB8Rlk9cp2ndvGRCvehk3rNIhRuNtQaeKwctOl1KvWiklv5w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"System.CodeDom": "4.4.0"
|
"Microsoft.Build.Tasks.Git": "1.1.1",
|
||||||
|
"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.4",
|
"resolved": "2.1.6",
|
||||||
"contentHash": "EWI1olKDjFEBMJu0+3wuxwziIAdWDVMYLhuZ3Qs84rrz+DHwD00RzWPZCa+bLnHCf3oJwuFZIRsHT5p236QXww==",
|
"contentHash": "BmAf6XWt4TqtowmiWe4/5rRot6GerAeklmOPfviOvwLoF5WwgxcJHAxZtySuyW9r9w+HLILnm8VfJFLCUJYW8A==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"SQLitePCLRaw.lib.e_sqlite3": "2.1.4",
|
"SQLitePCLRaw.lib.e_sqlite3": "2.1.6",
|
||||||
"SQLitePCLRaw.provider.e_sqlite3": "2.1.4"
|
"SQLitePCLRaw.provider.e_sqlite3": "2.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SQLitePCLRaw.core": {
|
"SQLitePCLRaw.core": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.1.4",
|
"resolved": "2.1.6",
|
||||||
"contentHash": "inBjvSHo9UDKneGNzfUfDjK08JzlcIhn1+SP5Y3m6cgXpCxXKCJDy6Mka7LpgSV+UZmKSnC8rTwB0SQ0xKu5pA==",
|
"contentHash": "wO6v9GeMx9CUngAet8hbO7xdm+M42p1XeJq47ogyRoYSvNSp0NGLI+MgC0bhrMk9C17MTVFlLiN6ylyExLCc5w==",
|
||||||
"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.4",
|
"resolved": "2.1.6",
|
||||||
"contentHash": "2C9Q9eX7CPLveJA0rIhf9RXAvu+7nWZu1A2MdG6SD/NOu26TakGgL1nsbc0JAspGijFOo3HoN79xrx8a368fBg=="
|
"contentHash": "2ObJJLkIUIxRpOUlZNGuD4rICpBnrBR5anjyfUFQep4hMOIeqW+XGQYzrNmHSVz5xSWZ3klSbh7sFR6UyDj68Q=="
|
||||||
},
|
},
|
||||||
"SQLitePCLRaw.provider.e_sqlite3": {
|
"SQLitePCLRaw.provider.e_sqlite3": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.1.4",
|
"resolved": "2.1.6",
|
||||||
"contentHash": "CSlb5dUp1FMIkez9Iv5EXzpeq7rHryVNqwJMWnpq87j9zWZexaEMdisDktMsnnrzKM6ahNrsTkjqNodTBPBxtQ==",
|
"contentHash": "PQ2Oq3yepLY4P7ll145P3xtx2bX8xF4PzaKPRpw9jZlKvfe4LE/saAV82inND9usn1XRpmxXk7Lal3MTI+6CNg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"SQLitePCLRaw.core": "2.1.4"
|
"SQLitePCLRaw.core": "2.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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",
|
||||||
@ -282,34 +304,40 @@
|
|||||||
},
|
},
|
||||||
"System.Text.Encodings.Web": {
|
"System.Text.Encodings.Web": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "7.0.0",
|
"resolved": "8.0.0",
|
||||||
"contentHash": "OP6umVGxc0Z0MvZQBVigj4/U31Pw72ITihDWP9WiWDm+q5aoe0GaJivsfYGq53o6dxH7DcXWiCTl7+0o2CGdmg=="
|
"contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ=="
|
||||||
},
|
},
|
||||||
"System.Text.Json": {
|
"System.Text.Json": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "7.0.0",
|
"resolved": "8.0.0",
|
||||||
"contentHash": "DaGSsVqKsn/ia6RG8frjwmJonfos0srquhw09TlT8KRw5I43E+4gs+/bZj4K0vShJ5H9imCuXupb4RmS+dBy3w==",
|
"contentHash": "OdrZO2WjkiEG6ajEFRABTRCi/wuXQPxeV6g8xvUJqdxMvvuCCEk86zPla8UiIQJz3durtUEbNyY/3lIhS0yZvQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"System.Text.Encodings.Web": "7.0.0"
|
"System.Text.Encodings.Web": "8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ecommons": {
|
"ecommons": {
|
||||||
"type": "Project"
|
"type": "Project"
|
||||||
},
|
},
|
||||||
|
"llib": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"DalamudPackager": "[2.1.13, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
"pal.common": {
|
"pal.common": {
|
||||||
"type": "Project"
|
"type": "Project"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"net7.0-windows7.0/win-x64": {
|
"net8.0-windows7.0/win-x64": {
|
||||||
"SQLitePCLRaw.lib.e_sqlite3": {
|
"SQLitePCLRaw.lib.e_sqlite3": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.1.4",
|
"resolved": "2.1.6",
|
||||||
"contentHash": "2C9Q9eX7CPLveJA0rIhf9RXAvu+7nWZu1A2MdG6SD/NOu26TakGgL1nsbc0JAspGijFOo3HoN79xrx8a368fBg=="
|
"contentHash": "2ObJJLkIUIxRpOUlZNGuD4rICpBnrBR5anjyfUFQep4hMOIeqW+XGQYzrNmHSVz5xSWZ3klSbh7sFR6UyDj68Q=="
|
||||||
},
|
},
|
||||||
"System.Text.Encodings.Web": {
|
"System.Text.Encodings.Web": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "7.0.0",
|
"resolved": "8.0.0",
|
||||||
"contentHash": "OP6umVGxc0Z0MvZQBVigj4/U31Pw72ITihDWP9WiWDm+q5aoe0GaJivsfYGq53o6dxH7DcXWiCTl7+0o2CGdmg=="
|
"contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,63 +1,62 @@
|
|||||||
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
|
||||||
{
|
{
|
||||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
Palace_1_10 = 561,
|
||||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
Palace_11_20,
|
||||||
public enum ETerritoryType : ushort
|
Palace_21_30,
|
||||||
{
|
Palace_31_40,
|
||||||
Palace_1_10 = 561,
|
Palace_41_50,
|
||||||
Palace_11_20,
|
Palace_51_60 = 593,
|
||||||
Palace_21_30,
|
Palace_61_70,
|
||||||
Palace_31_40,
|
Palace_71_80,
|
||||||
Palace_41_50,
|
Palace_81_90,
|
||||||
Palace_51_60 = 593,
|
Palace_91_100,
|
||||||
Palace_61_70,
|
Palace_101_110,
|
||||||
Palace_71_80,
|
Palace_111_120,
|
||||||
Palace_81_90,
|
Palace_121_130,
|
||||||
Palace_91_100,
|
Palace_131_140,
|
||||||
Palace_101_110,
|
Palace_141_150,
|
||||||
Palace_111_120,
|
Palace_151_160,
|
||||||
Palace_121_130,
|
Palace_161_170,
|
||||||
Palace_131_140,
|
Palace_171_180,
|
||||||
Palace_141_150,
|
Palace_181_190,
|
||||||
Palace_151_160,
|
Palace_191_200,
|
||||||
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
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System;
|
||||||
|
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 class EnumExtensions
|
public static int? GetOrder(this Enum e)
|
||||||
{
|
{
|
||||||
public static int? GetOrder(this Enum e)
|
Type type = e.GetType();
|
||||||
{
|
MemberInfo field = type.GetMember(e.ToString()).Single();
|
||||||
Type type = e.GetType();
|
DisplayAttribute? attribute = field.GetCustomAttributes(typeof(DisplayAttribute), false).Cast<DisplayAttribute>().FirstOrDefault();
|
||||||
MemberInfo field = type.GetMember(e.ToString()).Single();
|
return attribute?.Order;
|
||||||
DisplayAttribute? attribute = field.GetCustomAttributes(typeof(DisplayAttribute), false).Cast<DisplayAttribute>().FirstOrDefault();
|
|
||||||
return attribute?.Order;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,9 @@ 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 class ExportConfig
|
public static int ExportVersion => 2;
|
||||||
{
|
|
||||||
public static int ExportVersion => 2;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<LangVersion>11.0</LangVersion>
|
<LangVersion>12.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>
|
||||||
|
@ -1,22 +1,22 @@
|
|||||||
using System.Numerics;
|
using System;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
namespace Pal.Common
|
namespace Pal.Common;
|
||||||
|
|
||||||
|
public class PalaceMath
|
||||||
{
|
{
|
||||||
public class PalaceMath
|
private static readonly Vector3 ScaleFactor = new(5);
|
||||||
|
|
||||||
|
public static bool IsNearlySamePosition(Vector3 a, Vector3 b)
|
||||||
{
|
{
|
||||||
private static readonly Vector3 ScaleFactor = new(5);
|
a *= ScaleFactor;
|
||||||
|
b *= ScaleFactor;
|
||||||
|
return (int)a.X == (int)b.X && (int)a.Y == (int)b.Y && (int)a.Z == (int)b.Z;
|
||||||
|
}
|
||||||
|
|
||||||
public static bool IsNearlySamePosition(Vector3 a, Vector3 b)
|
public static int GetHashCode(Vector3 v)
|
||||||
{
|
{
|
||||||
a *= ScaleFactor;
|
v *= ScaleFactor;
|
||||||
b *= ScaleFactor;
|
return HashCode.Combine((int)v.X, (int)v.Y, (int)v.Z);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ service AccountService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message CreateAccountRequest {
|
message CreateAccountRequest {
|
||||||
|
Version version = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message CreateAccountReply {
|
message CreateAccountReply {
|
||||||
@ -35,6 +36,7 @@ enum CreateAccountError {
|
|||||||
|
|
||||||
message LoginRequest {
|
message LoginRequest {
|
||||||
string accountId = 1;
|
string accountId = 1;
|
||||||
|
Version version = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message LoginReply {
|
message LoginReply {
|
||||||
@ -56,3 +58,8 @@ message VerifyRequest {
|
|||||||
|
|
||||||
message VerifyReply {
|
message VerifyReply {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message Version {
|
||||||
|
int32 major = 1;
|
||||||
|
int32 minor = 2;
|
||||||
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"net7.0": {}
|
"net8.0": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,28 +0,0 @@
|
|||||||
<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>
|
|
@ -1,34 +0,0 @@
|
|||||||
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}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
# 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.
|
|
@ -1,58 +0,0 @@
|
|||||||
{
|
|
||||||
"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
22
Pal.sln
@ -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", "Pal.Server\Pal.Server.csproj", "{AB3E2849-DB06-46F6-8457-9AC1096B4125}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pal.Server", "Server\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.StandaloneClient", "Pal.StandaloneClient\Pal.StandaloneClient.csproj", "{EDC1C408-D832-4C09-97A2-61B223A84166}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pal.Server.Tests", "Server\Tests\Pal.Server.Tests.csproj", "{AEC052FA-F178-492C-9A09-ED28DBE1EF5E}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pal.Server.Tests", "Pal.Server.Tests\Pal.Server.Tests.csproj", "{AEC052FA-F178-492C-9A09-ED28DBE1EF5E}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LLib", "vendor\LLib\LLib.csproj", "{B1321FD5-7BBF-4C9D-83C1-F8D7C394F32A}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@ -69,14 +69,6 @@ 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
|
||||||
@ -85,6 +77,14 @@ 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
|
||||||
|
@ -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://github.com/carvelli/Dalamud-Plugins#installation).
|
[Installation Instructions](https://git.carvel.li/liza/plugin-repo/src/branch/master/README.md).
|
||||||
|
|
||||||
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://github.com/carvelli/PalacePal/issues/new).
|
[please create a new issue](https://git.carvel.li/liza/PalacePal/issues/new).
|
||||||
|
1
Server
Submodule
1
Server
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit e59583fac353fdd960556ed2fa65220d66db5637
|
4
docker-build.sh
Executable file
4
docker-build.sh
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
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
2
vendor/ECommons
vendored
@ -1 +1 @@
|
|||||||
Subproject commit d9dc8c1c6e914cf37ad47703579d85094246f2e5
|
Subproject commit dcd85f8cca504360eda4b2cb5327632cc500a22c
|
1
vendor/LLib
vendored
Submodule
1
vendor/LLib
vendored
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit e206782c1106e1a5292a06af61783faef1ac0c42
|
Loading…
Reference in New Issue
Block a user