Compare commits
115 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 | |||
cec3597629 | |||
ae29eaa52f | |||
b81ced33e8 | |||
87806397d1 | |||
d645aa9ea7 | |||
4288440e6c | |||
36b328c29e | |||
232ab164be | |||
a4890c0159 | |||
7bdf97411c | |||
3e9f14419c | |||
a21e9335aa | |||
1b415d7a7f | |||
c5acb2ca54 | |||
7a514fad2a | |||
125b687a9c | |||
07ed62cd9a | |||
7179d41e59 | |||
8279bfe9bf | |||
e45b72a655 | |||
cbdcf58063 | |||
d25e21619d | |||
2bfbeacdd0 | |||
88ec7a00da | |||
0c922c1695 | |||
e1dc8cafd9 | |||
e79e8de6dc | |||
f140fb870c | |||
3663bbb241 | |||
17f7dcdf12 | |||
f02aeffb26 | |||
b658956c4b | |||
98bc4887d6 | |||
efeb30331c | |||
8a27eca8b3 | |||
0d8f655936 | |||
26b3a54ebd | |||
8d17c02186 | |||
dbe6abd1db | |||
7bccec0bae | |||
d5dc55a0c4 | |||
802e0c4cde | |||
e0d4a5d676 | |||
810aa30cf9 | |||
ff02b1f03c | |||
d1fdd1ce12 | |||
7b5bb3ee3a | |||
7c968fa9d4 | |||
94f3fa2ede | |||
f63e70b0c4 | |||
adddbc452c | |||
5419f51942 | |||
8986b368c7 | |||
e624c5b628 | |||
57a5be7938 | |||
870f29f0c6 | |||
8b6dd52b54 | |||
3954c839fb | |||
f1171d6ccd | |||
0bb7301ca1 | |||
a5456a54a0 | |||
29342264c0 | |||
c7d5aa1eaa | |||
7e2ccd3b42 | |||
5c82382161 | |||
e27f5a3201 | |||
3d560fad7f | |||
e3459a0182 | |||
7d04cd7575 | |||
29aefee135 | |||
c52341eb0d | |||
faa35feade | |||
e7c2cd426b | |||
d1cb7e08f2 | |||
550fa92a53 | |||
16a17e0dcf | |||
4f8deea8e0 | |||
6412afbfbb | |||
d3b8001a61 | |||
b0de113ad2 | |||
4be0bdf637 | |||
c586ced5bd | |||
019a204862 |
@ -4,4 +4,6 @@
|
||||
.gitignore
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
docker-build.sh
|
||||
.vs/
|
||||
.idea/
|
||||
|
31
.editorconfig
Normal file
31
.editorconfig
Normal file
@ -0,0 +1,31 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[{*.cs}]
|
||||
charset = utf-8-bom
|
||||
indent_size = 4
|
||||
dotnet_sort_system_directives_first = true
|
||||
csharp_style_implicit_object_creation_when_type_is_apparent = true
|
||||
csharp_trailing_comma_in_multiline_lists = true
|
||||
csharp_place_simple_embedded_block_on_same_line = false
|
||||
csharp_place_attribute_on_same_line = false
|
||||
|
||||
resharper_indent_text = ZeroIndent
|
||||
|
||||
# methods
|
||||
csharp_style_expression_bodied_methods = true
|
||||
|
||||
# namespaces
|
||||
csharp_style_namespace_declarations = file_scoped
|
||||
|
||||
# braces
|
||||
csharp_prefer_braces = when_multiline
|
||||
|
||||
[{*.resx,*.Designer.cs,packages.lock.json}]
|
||||
generated_code = true
|
||||
ij_formatter_enabled = false
|
29
.github/workflows/build.yml
vendored
29
.github/workflows/build.yml
vendored
@ -1,29 +0,0 @@
|
||||
name: dotnet build
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '**.md'
|
||||
- 'Dockerfile'
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
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\"
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Build
|
||||
run: dotnet build --configuration Release --no-restore
|
30
.github/workflows/server.yml
vendored
30
.github/workflows/server.yml
vendored
@ -1,30 +0,0 @@
|
||||
name: docker build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- '.github/workflows/server.yml'
|
||||
- 'Pal.Common/**'
|
||||
- 'Pal.Server/**'
|
||||
- 'Dockerfile'
|
||||
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"]
|
||||
path = vendor/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
|
||||
|
10
Directory.Build.targets
Normal file
10
Directory.Build.targets
Normal file
@ -0,0 +1,10 @@
|
||||
<Project>
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
<WindowsKitsRoot Condition="'$(WindowsKitsRoot)' == ''">$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows Kits\Installed Roots', 'KitsRoot10', null, RegistryView.Registry32, RegistryView.Default))</WindowsKitsRoot>
|
||||
<SignToolPath Condition="'$(WindowsKitsRoot)' != '' And '$(SignToolPath)' == '' And exists('$(WindowsKitsRoot)bin\10.0.19041.0\')">$(WindowsKitsRoot)bin\10.0.19041.0\x86\</SignToolPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="'$(SignToolPath)' != '' And Exists('$(SolutionDir)codesigning.pfx')">
|
||||
<Exec Command=""$(SignToolPath)signtool.exe" sign /f $(SolutionDir)codesigning.pfx /t http://timestamp.digicert.com /fd SHA256 "$(TargetPath)""/>
|
||||
</Target>
|
||||
</Project>
|
18
Dockerfile
18
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
|
||||
COPY Pal.Common/Pal.Common.csproj Pal.Common/
|
||||
COPY Pal.Server/Pal.Server.csproj Pal.Server/
|
||||
RUN dotnet restore Pal.Server/Pal.Server.csproj
|
||||
COPY Server/Server/Pal.Server.csproj Server/Server/
|
||||
RUN dotnet restore Server/Server/Pal.Server.csproj -a $TARGETARCH
|
||||
|
||||
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
|
||||
ENV DOTNET_ENVIRONMENT=Production
|
||||
ENV ASPNETCORE_URLS=
|
||||
@ -21,4 +27,4 @@ WORKDIR /app
|
||||
COPY --from=build-env /dist .
|
||||
|
||||
USER pal
|
||||
ENTRYPOINT ["dotnet", "Pal.Server.dll"]
|
||||
ENTRYPOINT ["dotnet", "Pal.Server.dll"]
|
||||
|
9
Pal.Client/Commands/ISubCommand.cs
Normal file
9
Pal.Client/Commands/ISubCommand.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Pal.Client.Commands;
|
||||
|
||||
public interface ISubCommand
|
||||
{
|
||||
IReadOnlyDictionary<string, Action<string>> GetHandlers();
|
||||
}
|
39
Pal.Client/Commands/PalConfigCommand.cs
Normal file
39
Pal.Client/Commands/PalConfigCommand.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Pal.Client.Configuration;
|
||||
using Pal.Client.Windows;
|
||||
|
||||
namespace Pal.Client.Commands;
|
||||
|
||||
internal class PalConfigCommand : ISubCommand
|
||||
{
|
||||
private readonly IPalacePalConfiguration _configuration;
|
||||
private readonly AgreementWindow _agreementWindow;
|
||||
private readonly ConfigWindow _configWindow;
|
||||
|
||||
public PalConfigCommand(
|
||||
IPalacePalConfiguration configuration,
|
||||
AgreementWindow agreementWindow,
|
||||
ConfigWindow configWindow)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_agreementWindow = agreementWindow;
|
||||
_configWindow = configWindow;
|
||||
}
|
||||
|
||||
|
||||
public IReadOnlyDictionary<string, Action<string>> GetHandlers()
|
||||
=> new Dictionary<string, Action<string>>
|
||||
{
|
||||
{ "config", _ => Execute() },
|
||||
{ "", _ => Execute() }
|
||||
};
|
||||
|
||||
public void Execute()
|
||||
{
|
||||
if (_configuration.FirstUse)
|
||||
_agreementWindow.IsOpen = true;
|
||||
else
|
||||
_configWindow.Toggle();
|
||||
}
|
||||
}
|
62
Pal.Client/Commands/PalNearCommand.cs
Normal file
62
Pal.Client/Commands/PalNearCommand.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Pal.Client.DependencyInjection;
|
||||
using Pal.Client.Extensions;
|
||||
using Pal.Client.Floors;
|
||||
using Pal.Client.Rendering;
|
||||
|
||||
namespace Pal.Client.Commands;
|
||||
|
||||
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)
|
||||
{
|
||||
_chat = chat;
|
||||
_clientState = clientState;
|
||||
_territoryState = territoryState;
|
||||
_floorService = floorService;
|
||||
}
|
||||
|
||||
|
||||
public IReadOnlyDictionary<string, Action<string>> GetHandlers()
|
||||
=> new Dictionary<string, Action<string>>
|
||||
{
|
||||
{ "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)
|
||||
{
|
||||
if (!_territoryState.IsInDeepDungeon())
|
||||
return;
|
||||
|
||||
var state = _floorService.GetTerritoryIfReady(_clientState.TerritoryType);
|
||||
if (state == null)
|
||||
return;
|
||||
|
||||
var playerPosition = _clientState.LocalPlayer?.Position;
|
||||
if (playerPosition == null)
|
||||
return;
|
||||
_chat.Message($"Your position: {playerPosition}");
|
||||
|
||||
var nearbyMarkers = state.Locations
|
||||
.Where(m => predicate(m))
|
||||
.Where(m => m.RenderElement != null && m.RenderElement.Enabled)
|
||||
.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}");
|
||||
}
|
||||
}
|
24
Pal.Client/Commands/PalStatsCommand.cs
Normal file
24
Pal.Client/Commands/PalStatsCommand.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Pal.Client.DependencyInjection;
|
||||
|
||||
namespace Pal.Client.Commands;
|
||||
|
||||
internal sealed class PalStatsCommand : ISubCommand
|
||||
{
|
||||
private readonly 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();
|
||||
}
|
29
Pal.Client/Commands/PalTestConnectionCommand.cs
Normal file
29
Pal.Client/Commands/PalTestConnectionCommand.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ECommons.Schedulers;
|
||||
using Pal.Client.Windows;
|
||||
|
||||
namespace Pal.Client.Commands;
|
||||
|
||||
internal sealed class PalTestConnectionCommand : ISubCommand
|
||||
{
|
||||
private readonly ConfigWindow _configWindow;
|
||||
|
||||
public PalTestConnectionCommand(ConfigWindow configWindow)
|
||||
{
|
||||
_configWindow = configWindow;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, Action<string>> GetHandlers()
|
||||
=> new Dictionary<string, Action<string>>
|
||||
{
|
||||
{ "test-connection", _ => Execute() },
|
||||
{ "tc", _ => Execute() },
|
||||
};
|
||||
|
||||
private void Execute()
|
||||
{
|
||||
_configWindow.IsOpen = true;
|
||||
var _ = new TickScheduler(() => _configWindow.TestConnection());
|
||||
}
|
||||
}
|
@ -1,257 +0,0 @@
|
||||
using Dalamud.Configuration;
|
||||
using Dalamud.Logging;
|
||||
using ECommons.Schedulers;
|
||||
using Newtonsoft.Json;
|
||||
using Pal.Client.Scheduled;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Security.Cryptography;
|
||||
using Pal.Client.Extensions;
|
||||
|
||||
namespace Pal.Client
|
||||
{
|
||||
public class Configuration : IPluginConfiguration
|
||||
{
|
||||
private static readonly byte[] Entropy = { 0x22, 0x4b, 0xe7, 0x21, 0x44, 0x83, 0x69, 0x55, 0x80, 0x38 };
|
||||
|
||||
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]
|
||||
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
|
||||
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
public void Migrate()
|
||||
{
|
||||
if (Version == 1)
|
||||
{
|
||||
PluginLog.Information("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();
|
||||
}
|
||||
|
||||
if (Version == 2)
|
||||
{
|
||||
PluginLog.Information("Updating config to version 3");
|
||||
|
||||
Accounts = AccountIds.ToDictionary(x => x.Key, x => new AccountInfo
|
||||
{
|
||||
Id = x.Value
|
||||
});
|
||||
Version = 3;
|
||||
Save();
|
||||
}
|
||||
|
||||
if (Version == 3)
|
||||
{
|
||||
Version = 4;
|
||||
Save();
|
||||
}
|
||||
|
||||
if (Version == 4)
|
||||
{
|
||||
// 2.2 had a bug that would mark chests as traps, there's no easy way to detect this -- or clean this up.
|
||||
// Not a problem for online players, but offline players might be fucked.
|
||||
bool changedAnyFile = false;
|
||||
LocalState.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<Marker>(s.Markers.Where(m => m.SinceVersion != "0.0" || m.Type == Marker.EType.Hoard || m.WasImported));
|
||||
s.Save();
|
||||
|
||||
changedAnyFile = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// just add version information, nothing else
|
||||
s.Save();
|
||||
}
|
||||
});
|
||||
|
||||
// Only notify offline users - we can just re-download the backup markers from the server seamlessly.
|
||||
if (Mode == EMode.Offline && changedAnyFile)
|
||||
{
|
||||
_ = new TickScheduler(delegate
|
||||
{
|
||||
Service.Chat.PalError("Due to a bug, some coffers were accidentally saved as traps. To fix the related display issue, locally cached data was cleaned up.");
|
||||
Service.Chat.PrintError($"If you have any backup tools installed, please restore the contents of '{Service.PluginInterface.GetPluginConfigDirectory()}' to any backup from February 2, 2023 or before.");
|
||||
Service.Chat.PrintError("You can also manually restore .json.bak files (by removing the '.bak') if you have not been in any deep dungeon since February 2, 2023.");
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
Version = 5;
|
||||
Save();
|
||||
}
|
||||
|
||||
if (Version == 5)
|
||||
{
|
||||
LocalState.UpdateAll();
|
||||
|
||||
Version = 6;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
|
||||
public void Save()
|
||||
{
|
||||
Service.PluginInterface.SavePluginConfig(this);
|
||||
Service.Plugin.EarlyEventQueue.Enqueue(new QueuedConfigUpdate());
|
||||
}
|
||||
|
||||
public enum EMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Fetches trap locations from remote server.
|
||||
/// </summary>
|
||||
Online = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Only shows traps found by yourself uisng a pomander of sight.
|
||||
/// </summary>
|
||||
Offline = 2,
|
||||
}
|
||||
|
||||
public enum ERenderer
|
||||
{
|
||||
/// <see cref="Rendering.SimpleRenderer"/>
|
||||
Simple = 0,
|
||||
|
||||
/// <see cref="Rendering.SplatoonRenderer"/>
|
||||
Splatoon = 1,
|
||||
}
|
||||
|
||||
public class AccountInfo
|
||||
{
|
||||
[JsonConverter(typeof(AccountIdConverter))]
|
||||
public Guid? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This is taken from the JWT, and is only refreshed on a successful login.
|
||||
///
|
||||
/// If you simply reload the plugin without any server interaction, this doesn't change.
|
||||
///
|
||||
/// This has no impact on what roles the JWT actually contains, but is just to make it
|
||||
/// easier to draw a consistent UI. The server will still reject unauthorized calls.
|
||||
/// </summary>
|
||||
public List<string> CachedRoles { get; set; } = new();
|
||||
}
|
||||
|
||||
public class AccountIdConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType) => true;
|
||||
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.TokenType == JsonToken.String)
|
||||
{
|
||||
string? text = reader.Value?.ToString();
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return null;
|
||||
|
||||
if (Guid.TryParse(text, out Guid guid) && guid != Guid.Empty)
|
||||
return guid;
|
||||
|
||||
if (text.StartsWith("s:"))
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] guidBytes = ProtectedData.Unprotect(Convert.FromBase64String(text.Substring(2)), Entropy, DataProtectionScope.CurrentUser);
|
||||
return new Guid(guidBytes);
|
||||
}
|
||||
catch (CryptographicException e)
|
||||
{
|
||||
PluginLog.Error(e, "Could not load account id");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new JsonSerializationException();
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteNull();
|
||||
return;
|
||||
}
|
||||
|
||||
Guid g = (Guid)value;
|
||||
string text;
|
||||
try
|
||||
{
|
||||
byte[] guidBytes = ProtectedData.Protect(g.ToByteArray(), Entropy, DataProtectionScope.CurrentUser);
|
||||
text = $"s:{Convert.ToBase64String(guidBytes)}";
|
||||
}
|
||||
catch (CryptographicException)
|
||||
{
|
||||
text = g.ToString();
|
||||
}
|
||||
|
||||
writer.WriteValue(text);
|
||||
}
|
||||
}
|
||||
|
||||
public 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; }
|
||||
}
|
||||
}
|
||||
}
|
144
Pal.Client/Configuration/AccountConfigurationV7.cs
Normal file
144
Pal.Client/Configuration/AccountConfigurationV7.cs
Normal file
@ -0,0 +1,144 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Pal.Client.Configuration;
|
||||
|
||||
public sealed class AccountConfigurationV7 : IAccountConfiguration
|
||||
{
|
||||
private const int DefaultEntropyLength = 16;
|
||||
|
||||
[JsonConstructor]
|
||||
public AccountConfigurationV7()
|
||||
{
|
||||
}
|
||||
|
||||
public AccountConfigurationV7(string server, Guid accountId)
|
||||
{
|
||||
Server = server;
|
||||
(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);
|
||||
Entropy = ConfigurationData.FixedV1Entropy;
|
||||
Format = EFormat.UseProtectedData;
|
||||
EncryptIfNeeded();
|
||||
}
|
||||
else if (Guid.TryParse(accountId, out Guid guid))
|
||||
(EncryptedId, Entropy, Format) = EncryptAccountId(guid);
|
||||
else
|
||||
throw new InvalidOperationException($"Invalid account id format, can't migrate account for server {server}");
|
||||
}
|
||||
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
public EFormat Format { get; private set; } = EFormat.Unencrypted;
|
||||
|
||||
/// <summary>
|
||||
/// Depending on <see cref="Format"/>, this is either a Guid as string or a base64 encoded byte array.
|
||||
/// </summary>
|
||||
[JsonPropertyName("Id")]
|
||||
[JsonInclude]
|
||||
[JsonRequired]
|
||||
public string EncryptedId { get; private set; } = null!;
|
||||
|
||||
[JsonInclude]
|
||||
public byte[]? Entropy { get; private set; }
|
||||
|
||||
[JsonRequired]
|
||||
public string Server { get; init; } = null!;
|
||||
|
||||
[JsonIgnore] public bool IsUsable => DecryptAccountId() != null;
|
||||
|
||||
[JsonIgnore] public Guid AccountId => DecryptAccountId() ?? throw new InvalidOperationException("Account id can't be read");
|
||||
|
||||
public List<string> CachedRoles { get; set; } = new();
|
||||
|
||||
private Guid? DecryptAccountId()
|
||||
{
|
||||
if (Format == EFormat.UseProtectedData && ConfigurationData.SupportsDpapi)
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] guidBytes = ProtectedData.Unprotect(Convert.FromBase64String(EncryptedId), Entropy, DataProtectionScope.CurrentUser);
|
||||
return new Guid(guidBytes);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else if (Format == EFormat.Unencrypted)
|
||||
return Guid.Parse(EncryptedId);
|
||||
else if (Format == EFormat.ProtectedDataUnsupported && !ConfigurationData.SupportsDpapi)
|
||||
return Guid.Parse(EncryptedId);
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
private (string encryptedId, byte[]? entropy, EFormat format) EncryptAccountId(Guid g)
|
||||
{
|
||||
if (!ConfigurationData.SupportsDpapi)
|
||||
return (g.ToString(), null, EFormat.ProtectedDataUnsupported);
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] entropy = RandomNumberGenerator.GetBytes(DefaultEntropyLength);
|
||||
byte[] guidBytes = ProtectedData.Protect(g.ToByteArray(), entropy, DataProtectionScope.CurrentUser);
|
||||
return (Convert.ToBase64String(guidBytes), entropy, EFormat.UseProtectedData);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return (g.ToString(), null, EFormat.Unencrypted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool EncryptIfNeeded()
|
||||
{
|
||||
if (Format == EFormat.Unencrypted)
|
||||
{
|
||||
var (newId, newEntropy, newFormat) = EncryptAccountId(Guid.Parse(EncryptedId));
|
||||
if (newFormat != EFormat.Unencrypted)
|
||||
{
|
||||
EncryptedId = newId;
|
||||
Entropy = newEntropy;
|
||||
Format = newFormat;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (Format == EFormat.UseProtectedData && Entropy is { Length: < DefaultEntropyLength })
|
||||
{
|
||||
Guid? g = DecryptAccountId();
|
||||
if (g != null)
|
||||
{
|
||||
(EncryptedId, Entropy, Format) = EncryptAccountId(g.Value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public 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,
|
||||
}
|
||||
}
|
40
Pal.Client/Configuration/ConfigurationData.cs
Normal file
40
Pal.Client/Configuration/ConfigurationData.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Pal.Client.Configuration;
|
||||
|
||||
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
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_supportsDpapi == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] input = RandomNumberGenerator.GetBytes(32);
|
||||
byte[] entropy = RandomNumberGenerator.GetBytes(16);
|
||||
byte[] temp = ProtectedData.Protect(input, entropy, DataProtectionScope.CurrentUser);
|
||||
byte[] output = ProtectedData.Unprotect(temp, entropy, DataProtectionScope.CurrentUser);
|
||||
_supportsDpapi = input.SequenceEqual(output);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_supportsDpapi = false;
|
||||
}
|
||||
}
|
||||
|
||||
return _supportsDpapi.Value;
|
||||
}
|
||||
}
|
||||
}
|
170
Pal.Client/Configuration/ConfigurationManager.cs
Normal file
170
Pal.Client/Configuration/ConfigurationManager.cs
Normal file
@ -0,0 +1,170 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using Dalamud.Logging;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Pal.Client.Configuration.Legacy;
|
||||
using Pal.Client.Database;
|
||||
using NJson = Newtonsoft.Json;
|
||||
|
||||
namespace Pal.Client.Configuration;
|
||||
|
||||
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)
|
||||
{
|
||||
_logger = logger;
|
||||
_pluginInterface = pluginInterface;
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
private string ConfigPath =>
|
||||
Path.Join(_pluginInterface.GetPluginConfigDirectory(), ConfigurationData.ConfigFileName);
|
||||
|
||||
public IPalacePalConfiguration Load()
|
||||
{
|
||||
if (!File.Exists(ConfigPath))
|
||||
{
|
||||
_logger.LogInformation("No config file exists, creating one");
|
||||
Save(new ConfigurationV7(), false);
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize<ConfigurationV7>(File.ReadAllText(ConfigPath, Encoding.UTF8)) ??
|
||||
new ConfigurationV7();
|
||||
}
|
||||
|
||||
public void Save(IConfigurationInConfigDirectory config, bool queue = true)
|
||||
{
|
||||
File.WriteAllText(ConfigPath,
|
||||
JsonSerializer.Serialize(config, config.GetType(),
|
||||
new JsonSerializerOptions
|
||||
{ WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }),
|
||||
Encoding.UTF8);
|
||||
|
||||
if (queue && config is ConfigurationV7 v7)
|
||||
Saved?.Invoke(this, v7);
|
||||
}
|
||||
|
||||
#pragma warning disable CS0612
|
||||
#pragma warning disable CS0618
|
||||
public void Migrate()
|
||||
{
|
||||
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())
|
||||
{
|
||||
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
|
||||
dbContext.Imports.RemoveRange(dbContext.Imports);
|
||||
|
||||
foreach (var importHistory in configurationV1.ImportHistory)
|
||||
{
|
||||
_logger.LogInformation("Migrating import {Id}", importHistory.Id);
|
||||
dbContext.Imports.Add(new ImportHistory
|
||||
{
|
||||
Id = importHistory.Id,
|
||||
RemoteUrl = importHistory.RemoteUrl
|
||||
?.Replace(".μ.tv", ".liza.sh")
|
||||
.Replace("pal.liza.sh", "connect.palacepal.com"),
|
||||
ExportedAt = importHistory.ExportedAt,
|
||||
ImportedAt = importHistory.ImportedAt
|
||||
});
|
||||
}
|
||||
|
||||
dbContext.SaveChanges();
|
||||
}
|
||||
|
||||
File.Move(_pluginInterface.ConfigFile.FullName, _pluginInterface.ConfigFile.FullName + ".old", true);
|
||||
}
|
||||
|
||||
IPalacePalConfiguration? currentConfig = Load();
|
||||
IAccountConfiguration? legacyAccount = currentConfig?.FindAccount("https://pal.liza.sh");
|
||||
if (currentConfig != null && legacyAccount != null)
|
||||
{
|
||||
IAccountConfiguration newAccount = currentConfig.CreateAccount("https://connect.palacepal.com", legacyAccount.AccountId);
|
||||
newAccount.CachedRoles = legacyAccount.CachedRoles;
|
||||
|
||||
currentConfig.RemoveAccount(legacyAccount.Server);
|
||||
Save(currentConfig, false);
|
||||
}
|
||||
}
|
||||
|
||||
private ConfigurationV7 MigrateToV7(ConfigurationV1 v1)
|
||||
{
|
||||
ConfigurationV7 v7 = new()
|
||||
{
|
||||
Version = 7,
|
||||
FirstUse = v1.FirstUse,
|
||||
Mode = v1.Mode,
|
||||
BetaKey = v1.BetaKey,
|
||||
|
||||
DeepDungeons = new DeepDungeonConfiguration
|
||||
{
|
||||
Traps = new MarkerConfiguration
|
||||
{
|
||||
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 CS0612
|
||||
}
|
53
Pal.Client/Configuration/ConfigurationV7.cs
Normal file
53
Pal.Client/Configuration/ConfigurationV7.cs
Normal file
@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Pal.Client.Configuration;
|
||||
|
||||
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)
|
||||
{
|
||||
var account = new AccountConfigurationV7(server, accountId);
|
||||
Accounts.Add(account);
|
||||
return account;
|
||||
}
|
||||
|
||||
[Obsolete("for V1 import")]
|
||||
internal IAccountConfiguration CreateAccount(string server, string accountId)
|
||||
{
|
||||
var account = new AccountConfigurationV7(server, accountId);
|
||||
Accounts.Add(account);
|
||||
return account;
|
||||
}
|
||||
|
||||
public IAccountConfiguration? FindAccount(string server)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
14
Pal.Client/Configuration/EMode.cs
Normal file
14
Pal.Client/Configuration/EMode.cs
Normal file
@ -0,0 +1,14 @@
|
||||
namespace Pal.Client.Configuration;
|
||||
|
||||
public enum EMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Fetches trap locations from remote server.
|
||||
/// </summary>
|
||||
Online = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Only shows traps found by yourself using a pomander of sight.
|
||||
/// </summary>
|
||||
Offline = 2,
|
||||
}
|
10
Pal.Client/Configuration/ERenderer.cs
Normal file
10
Pal.Client/Configuration/ERenderer.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Pal.Client.Configuration;
|
||||
|
||||
public enum ERenderer
|
||||
{
|
||||
/// <see cref="Rendering.SimpleRenderer"/>
|
||||
Simple = 0,
|
||||
|
||||
/// <see cref="Rendering.SplatoonRenderer"/>
|
||||
Splatoon = 1,
|
||||
}
|
110
Pal.Client/Configuration/IPalacePalConfiguration.cs
Normal file
110
Pal.Client/Configuration/IPalacePalConfiguration.cs
Normal file
@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using ImGuiNET;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Pal.Client.Configuration;
|
||||
|
||||
public interface IVersioned
|
||||
{
|
||||
int Version { get; set; }
|
||||
}
|
||||
public interface IConfigurationInConfigDirectory : IVersioned
|
||||
{
|
||||
}
|
||||
|
||||
public interface IPalacePalConfiguration : IConfigurationInConfigDirectory
|
||||
{
|
||||
bool FirstUse { get; set; }
|
||||
EMode Mode { get; set; }
|
||||
string BetaKey { get; }
|
||||
bool HasBetaFeature(string feature) => BetaKey.Contains(feature);
|
||||
|
||||
DeepDungeonConfiguration DeepDungeons { get; set; }
|
||||
RendererConfiguration Renderer { get; set; }
|
||||
BackupConfiguration Backups { get; set; }
|
||||
|
||||
IAccountConfiguration CreateAccount(string server, Guid accountId);
|
||||
IAccountConfiguration? FindAccount(string server);
|
||||
void RemoveAccount(string server);
|
||||
|
||||
bool HasRoleOnCurrentServer(string server, string role);
|
||||
}
|
||||
|
||||
public class DeepDungeonConfiguration
|
||||
{
|
||||
public MarkerConfiguration Traps { get; set; } = new()
|
||||
{
|
||||
Show = true,
|
||||
Color = ImGui.ColorConvertFloat4ToU32(new Vector4(1, 0, 0, 0.4f)),
|
||||
OnlyVisibleAfterPomander = true,
|
||||
Fill = false
|
||||
};
|
||||
|
||||
public MarkerConfiguration HoardCoffers { get; set; } = new()
|
||||
{
|
||||
Show = true,
|
||||
Color = ImGui.ColorConvertFloat4ToU32(new Vector4(0, 1, 1, 0.4f)),
|
||||
OnlyVisibleAfterPomander = true,
|
||||
Fill = false
|
||||
};
|
||||
|
||||
public MarkerConfiguration SilverCoffers { get; set; } = new()
|
||||
{
|
||||
Show = false,
|
||||
Color = ImGui.ColorConvertFloat4ToU32(new Vector4(1, 1, 1, 0.4f)),
|
||||
OnlyVisibleAfterPomander = false,
|
||||
Fill = true
|
||||
};
|
||||
|
||||
public MarkerConfiguration GoldCoffers { get; set; } = new()
|
||||
{
|
||||
Show = false,
|
||||
Color = ImGui.ColorConvertFloat4ToU32(new Vector4(1, 1, 0, 0.4f)),
|
||||
OnlyVisibleAfterPomander = false,
|
||||
Fill = true
|
||||
};
|
||||
}
|
||||
|
||||
public class MarkerConfiguration
|
||||
{
|
||||
[JsonRequired]
|
||||
public bool Show { get; set; }
|
||||
|
||||
[JsonRequired]
|
||||
public uint Color { get; set; }
|
||||
|
||||
public bool OnlyVisibleAfterPomander { get; set; }
|
||||
public bool Fill { get; set; }
|
||||
}
|
||||
|
||||
public class RendererConfiguration
|
||||
{
|
||||
public ERenderer SelectedRenderer { get; set; } = ERenderer.Splatoon;
|
||||
}
|
||||
|
||||
public interface IAccountConfiguration
|
||||
{
|
||||
bool IsUsable { get; }
|
||||
string Server { get; }
|
||||
Guid AccountId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// This is taken from the JWT, and is only refreshed on a successful login.
|
||||
///
|
||||
/// If you simply reload the plugin without any server interaction, this doesn't change.
|
||||
///
|
||||
/// This has no impact on what roles the JWT actually contains, but is just to make it
|
||||
/// easier to draw a consistent UI. The server will still reject unauthorized calls.
|
||||
/// </summary>
|
||||
List<string> CachedRoles { get; set; }
|
||||
|
||||
bool EncryptIfNeeded();
|
||||
}
|
||||
|
||||
public class BackupConfiguration
|
||||
{
|
||||
public int MinimumBackupsToKeep { get; set; } = 3;
|
||||
public int DaysToDeleteAfter { get; set; } = 21;
|
||||
}
|
166
Pal.Client/Configuration/Legacy/ConfigurationV1.cs
Normal file
166
Pal.Client/Configuration/Legacy/ConfigurationV1.cs
Normal file
@ -0,0 +1,166 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Plugin;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
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]
|
||||
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)
|
||||
{
|
||||
if (Version == 1)
|
||||
{
|
||||
logger.LogInformation("Updating config to version 2");
|
||||
|
||||
if (DebugAccountId != null && Guid.TryParse(DebugAccountId, out Guid debugAccountId))
|
||||
AccountIds["http://localhost:5145"] = debugAccountId;
|
||||
|
||||
if (AccountId != null && Guid.TryParse(AccountId, out Guid accountId))
|
||||
AccountIds["https://pal.μ.tv"] = accountId;
|
||||
|
||||
Version = 2;
|
||||
Save(pluginInterface);
|
||||
}
|
||||
|
||||
if (Version == 2)
|
||||
{
|
||||
logger.LogInformation("Updating config to version 3");
|
||||
|
||||
Accounts = AccountIds.ToDictionary(x => x.Key, x => new AccountInfo
|
||||
{
|
||||
Id = x.Value.ToString() // encryption happens in V7 migration at latest
|
||||
});
|
||||
Version = 3;
|
||||
Save(pluginInterface);
|
||||
}
|
||||
|
||||
if (Version == 3)
|
||||
{
|
||||
Version = 4;
|
||||
Save(pluginInterface);
|
||||
}
|
||||
|
||||
if (Version == 4)
|
||||
{
|
||||
// 2.2 had a bug that would mark chests as traps, there's no easy way to detect this -- or clean this up.
|
||||
// Not a problem for online players, but offline players might be fucked.
|
||||
//bool changedAnyFile = false;
|
||||
JsonFloorState.ForEach(s =>
|
||||
{
|
||||
foreach (var marker in s.Markers)
|
||||
marker.SinceVersion = "0.0";
|
||||
|
||||
var lastModified = File.GetLastWriteTimeUtc(s.GetSaveLocation());
|
||||
if (lastModified >= new DateTime(2023, 2, 3, 0, 0, 0, DateTimeKind.Utc))
|
||||
{
|
||||
s.Backup(suffix: "bak");
|
||||
|
||||
s.Markers = new ConcurrentBag<JsonMarker>(s.Markers.Where(m => m.SinceVersion != "0.0" || m.Type == JsonMarker.EType.Hoard || m.WasImported));
|
||||
s.Save();
|
||||
|
||||
//changedAnyFile = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// just add version information, nothing else
|
||||
s.Save();
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
// Only notify offline users - we can just re-download the backup markers from the server seamlessly.
|
||||
if (Mode == EMode.Offline && changedAnyFile)
|
||||
{
|
||||
_ = new TickScheduler(delegate
|
||||
{
|
||||
Service.Chat.PalError("Due to a bug, some coffers were accidentally saved as traps. To fix the related display issue, locally cached data was cleaned up.");
|
||||
Service.Chat.PrintError($"If you have any backup tools installed, please restore the contents of '{Service.PluginInterface.GetPluginConfigDirectory()}' to any backup from February 2, 2023 or before.");
|
||||
Service.Chat.PrintError("You can also manually restore .json.bak files (by removing the '.bak') if you have not been in any deep dungeon since February 2, 2023.");
|
||||
}, 2500);
|
||||
}
|
||||
*/
|
||||
|
||||
Version = 5;
|
||||
Save(pluginInterface);
|
||||
}
|
||||
|
||||
if (Version == 5)
|
||||
{
|
||||
JsonFloorState.UpdateAll();
|
||||
|
||||
Version = 6;
|
||||
Save(pluginInterface);
|
||||
}
|
||||
}
|
||||
|
||||
public void Save(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; }
|
||||
}
|
||||
}
|
161
Pal.Client/Configuration/Legacy/JsonFloorState.cs
Normal file
161
Pal.Client/Configuration/Legacy/JsonFloorState.cs
Normal file
@ -0,0 +1,161 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Pal.Client.Extensions;
|
||||
using Pal.Common;
|
||||
|
||||
namespace Pal.Client.Configuration.Legacy;
|
||||
|
||||
/// <summary>
|
||||
/// Legacy JSON file for marker locations.
|
||||
/// </summary>
|
||||
[Obsolete]
|
||||
public sealed class JsonFloorState
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonSerializerOptions = new() { IncludeFields = true };
|
||||
private const int CurrentVersion = 4;
|
||||
|
||||
private static string _pluginConfigDirectory = null!;
|
||||
|
||||
internal static void SetContextProperties(string pluginConfigDirectory)
|
||||
{
|
||||
_pluginConfigDirectory = pluginConfigDirectory;
|
||||
}
|
||||
|
||||
public ushort TerritoryType { get; set; }
|
||||
public ConcurrentBag<JsonMarker> Markers { get; set; } = new();
|
||||
|
||||
public JsonFloorState(ushort territoryType)
|
||||
{
|
||||
TerritoryType = territoryType;
|
||||
}
|
||||
|
||||
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] == '[')
|
||||
{
|
||||
// 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
|
||||
{
|
||||
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()
|
||||
{
|
||||
string path = GetSaveLocation(TerritoryType);
|
||||
|
||||
ApplyFilters();
|
||||
SaveImpl(path);
|
||||
}
|
||||
|
||||
public void Backup(string suffix)
|
||||
{
|
||||
string path = $"{GetSaveLocation(TerritoryType)}.{suffix}";
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
SaveImpl(path);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
25
Pal.Client/Configuration/Legacy/JsonMarker.cs
Normal file
25
Pal.Client/Configuration/Legacy/JsonMarker.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
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
|
||||
{
|
||||
Unknown = 0,
|
||||
Trap = 1,
|
||||
Hoard = 2,
|
||||
Debug = 3,
|
||||
}
|
||||
}
|
146
Pal.Client/Configuration/Legacy/JsonMigration.cs
Normal file
146
Pal.Client/Configuration/Legacy/JsonMigration.cs
Normal file
@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Plugin;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Pal.Client.Database;
|
||||
using Pal.Common;
|
||||
|
||||
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 IDalamudPluginInterface _pluginInterface;
|
||||
|
||||
public JsonMigration(ILogger<JsonMigration> logger, IServiceScopeFactory serviceScopeFactory,
|
||||
IDalamudPluginInterface pluginInterface)
|
||||
{
|
||||
_logger = logger;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_pluginInterface = pluginInterface;
|
||||
}
|
||||
|
||||
#pragma warning disable CS0612
|
||||
public async Task MigrateAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
List<JsonFloorState> floorsToMigrate = new();
|
||||
JsonFloorState.ForEach(floorsToMigrate.Add);
|
||||
|
||||
if (floorsToMigrate.Count == 0)
|
||||
{
|
||||
_logger.LogInformation("Found no floors to migrate");
|
||||
return;
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await using var scope = _serviceScopeFactory.CreateAsyncScope();
|
||||
await using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
|
||||
|
||||
var fileStream = new FileStream(
|
||||
Path.Join(_pluginInterface.GetPluginConfigDirectory(),
|
||||
$"territory-backup-{DateTime.Now:yyyyMMdd-HHmmss}.zip"),
|
||||
FileMode.CreateNew);
|
||||
using (var backup = new ZipArchive(fileStream, ZipArchiveMode.Create, false))
|
||||
{
|
||||
IReadOnlyDictionary<Guid, ImportHistory> imports =
|
||||
await dbContext.Imports.ToDictionaryAsync(import => import.Id, cancellationToken);
|
||||
|
||||
foreach (var floorToMigrate in floorsToMigrate)
|
||||
{
|
||||
backup.CreateEntryFromFile(floorToMigrate.GetSaveLocation(),
|
||||
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)
|
||||
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");
|
||||
}
|
||||
|
||||
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
|
||||
}
|
@ -1,20 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project>
|
||||
<Target Name="PackagePlugin" AfterTargets="Build" Condition="'$(Configuration)' == 'Debug'">
|
||||
<DalamudPackager
|
||||
ProjectDir="$(ProjectDir)"
|
||||
OutputPath="$(OutputPath)"
|
||||
AssemblyName="$(AssemblyName)"
|
||||
MakeZip="false"
|
||||
VersionComponents="2"/>
|
||||
</Target>
|
||||
<Target Name="PackagePlugin" AfterTargets="Build" Condition="'$(Configuration)' == 'Debug'">
|
||||
<DalamudPackager
|
||||
ProjectDir="$(ProjectDir)"
|
||||
OutputPath="$(OutputPath)"
|
||||
AssemblyName="$(AssemblyName)"
|
||||
MakeZip="false"
|
||||
VersionComponents="2"/>
|
||||
</Target>
|
||||
|
||||
<Target Name="PackagePlugin" AfterTargets="Build" Condition="'$(Configuration)' == 'Release'">
|
||||
<DalamudPackager
|
||||
ProjectDir="$(ProjectDir)"
|
||||
OutputPath="$(OutputPath)"
|
||||
AssemblyName="$(AssemblyName)"
|
||||
MakeZip="true"
|
||||
VersionComponents="2"/>
|
||||
</Target>
|
||||
</Project>
|
||||
<Target Name="PackagePlugin" AfterTargets="Build" Condition="'$(Configuration)' == 'Release'">
|
||||
<DalamudPackager
|
||||
ProjectDir="$(ProjectDir)"
|
||||
OutputPath="$(OutputPath)"
|
||||
AssemblyName="$(AssemblyName)"
|
||||
MakeZip="true"
|
||||
VersionComponents="2"
|
||||
Exclude="ECommons.pdb;ECommons.xml"/>
|
||||
</Target>
|
||||
</Project>
|
||||
|
66
Pal.Client/Database/Cleanup.cs
Normal file
66
Pal.Client/Database/Cleanup.cs
Normal file
@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Pal.Client.Configuration;
|
||||
using Pal.Common;
|
||||
|
||||
namespace Pal.Client.Database;
|
||||
|
||||
internal sealed class Cleanup
|
||||
{
|
||||
private readonly ILogger<Cleanup> _logger;
|
||||
private readonly IPalacePalConfiguration _configuration;
|
||||
|
||||
public Cleanup(ILogger<Cleanup> logger, IPalacePalConfiguration configuration)
|
||||
{
|
||||
_logger = logger;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public void Purge(PalClientContext dbContext)
|
||||
{
|
||||
var toDelete = dbContext.Locations
|
||||
.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, ETerritoryType territoryType)
|
||||
{
|
||||
var toDelete = dbContext.Locations
|
||||
.Include(o => o.ImportedBy)
|
||||
.Include(o => o.RemoteEncounters)
|
||||
.AsSplitQuery()
|
||||
.Where(o => o.TerritoryType == (ushort)territoryType)
|
||||
.Where(DefaultPredicate())
|
||||
.Where(AnyRemoteEncounter())
|
||||
.ToList();
|
||||
_logger.LogInformation("Cleaning up {Count} outdated locations for territory {Territory}", toDelete.Count,
|
||||
territoryType);
|
||||
dbContext.Locations.RemoveRange(toDelete);
|
||||
}
|
||||
|
||||
private Expression<Func<ClientLocation, bool>> DefaultPredicate()
|
||||
{
|
||||
return o => !o.Seen &&
|
||||
o.ImportedBy.Count == 0 &&
|
||||
o.Source != ClientLocation.ESource.SeenLocally &&
|
||||
o.Source != ClientLocation.ESource.ExplodedLocally;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
58
Pal.Client/Database/ClientLocation.cs
Normal file
58
Pal.Client/Database/ClientLocation.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Pal.Client.Database;
|
||||
|
||||
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
|
||||
{
|
||||
Trap = 1,
|
||||
Hoard = 2,
|
||||
}
|
||||
|
||||
public enum ESource
|
||||
{
|
||||
Unknown = 0,
|
||||
SeenLocally = 1,
|
||||
ExplodedLocally = 2,
|
||||
Import = 3,
|
||||
Download = 4,
|
||||
}
|
||||
}
|
123
Pal.Client/Database/Compiled/ClientLocationEntityType.cs
Normal file
123
Pal.Client/Database/Compiled/ClientLocationEntityType.cs
Normal file
@ -0,0 +1,123 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
||||
#pragma warning disable 219, 612, 618
|
||||
#nullable enable
|
||||
|
||||
namespace Pal.Client.Database.Compiled
|
||||
{
|
||||
internal partial class ClientLocationEntityType
|
||||
{
|
||||
public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? baseEntityType = null)
|
||||
{
|
||||
var runtimeEntityType = model.AddEntityType(
|
||||
"Pal.Client.Database.ClientLocation",
|
||||
typeof(ClientLocation),
|
||||
baseEntityType);
|
||||
|
||||
var localId = runtimeEntityType.AddProperty(
|
||||
"LocalId",
|
||||
typeof(int),
|
||||
propertyInfo: typeof(ClientLocation).GetProperty("LocalId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(ClientLocation).GetField("<LocalId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
valueGenerated: ValueGenerated.OnAdd,
|
||||
afterSaveBehavior: PropertySaveBehavior.Throw);
|
||||
|
||||
var seen = runtimeEntityType.AddProperty(
|
||||
"Seen",
|
||||
typeof(bool),
|
||||
propertyInfo: typeof(ClientLocation).GetProperty("Seen", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(ClientLocation).GetField("<Seen>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
|
||||
|
||||
var sinceVersion = runtimeEntityType.AddProperty(
|
||||
"SinceVersion",
|
||||
typeof(string),
|
||||
propertyInfo: typeof(ClientLocation).GetProperty("SinceVersion", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(ClientLocation).GetField("<SinceVersion>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
|
||||
|
||||
var source = runtimeEntityType.AddProperty(
|
||||
"Source",
|
||||
typeof(ClientLocation.ESource),
|
||||
propertyInfo: typeof(ClientLocation).GetProperty("Source", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(ClientLocation).GetField("<Source>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
|
||||
|
||||
var territoryType = runtimeEntityType.AddProperty(
|
||||
"TerritoryType",
|
||||
typeof(ushort),
|
||||
propertyInfo: typeof(ClientLocation).GetProperty("TerritoryType", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(ClientLocation).GetField("<TerritoryType>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
|
||||
|
||||
var type = runtimeEntityType.AddProperty(
|
||||
"Type",
|
||||
typeof(ClientLocation.EType),
|
||||
propertyInfo: typeof(ClientLocation).GetProperty("Type", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(ClientLocation).GetField("<Type>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
|
||||
|
||||
var x = runtimeEntityType.AddProperty(
|
||||
"X",
|
||||
typeof(float),
|
||||
propertyInfo: typeof(ClientLocation).GetProperty("X", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(ClientLocation).GetField("<X>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
|
||||
|
||||
var y = runtimeEntityType.AddProperty(
|
||||
"Y",
|
||||
typeof(float),
|
||||
propertyInfo: typeof(ClientLocation).GetProperty("Y", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(ClientLocation).GetField("<Y>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
|
||||
|
||||
var z = runtimeEntityType.AddProperty(
|
||||
"Z",
|
||||
typeof(float),
|
||||
propertyInfo: typeof(ClientLocation).GetProperty("Z", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(ClientLocation).GetField("<Z>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
|
||||
|
||||
var key = runtimeEntityType.AddKey(
|
||||
new[] { localId });
|
||||
runtimeEntityType.SetPrimaryKey(key);
|
||||
|
||||
return runtimeEntityType;
|
||||
}
|
||||
|
||||
public static RuntimeSkipNavigation CreateSkipNavigation1(RuntimeEntityType declaringEntityType, RuntimeEntityType targetEntityType, RuntimeEntityType joinEntityType)
|
||||
{
|
||||
var skipNavigation = declaringEntityType.AddSkipNavigation(
|
||||
"ImportedBy",
|
||||
targetEntityType,
|
||||
joinEntityType.FindForeignKey(
|
||||
new[] { joinEntityType.FindProperty("ImportedLocationsLocalId")! },
|
||||
declaringEntityType.FindKey(new[] { declaringEntityType.FindProperty("LocalId")! })!,
|
||||
declaringEntityType)!,
|
||||
true,
|
||||
false,
|
||||
typeof(List<ImportHistory>),
|
||||
propertyInfo: typeof(ClientLocation).GetProperty("ImportedBy", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(ClientLocation).GetField("<ImportedBy>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
|
||||
|
||||
var inverse = targetEntityType.FindSkipNavigation("ImportedLocations");
|
||||
if (inverse != null)
|
||||
{
|
||||
skipNavigation.Inverse = inverse;
|
||||
inverse.Inverse = skipNavigation;
|
||||
}
|
||||
|
||||
return skipNavigation;
|
||||
}
|
||||
|
||||
public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
|
||||
{
|
||||
runtimeEntityType.AddAnnotation("Relational:FunctionName", null);
|
||||
runtimeEntityType.AddAnnotation("Relational:Schema", null);
|
||||
runtimeEntityType.AddAnnotation("Relational:SqlQuery", null);
|
||||
runtimeEntityType.AddAnnotation("Relational:TableName", "Locations");
|
||||
runtimeEntityType.AddAnnotation("Relational:ViewName", null);
|
||||
runtimeEntityType.AddAnnotation("Relational:ViewSchema", null);
|
||||
|
||||
Customize(runtimeEntityType);
|
||||
}
|
||||
|
||||
static partial void Customize(RuntimeEntityType runtimeEntityType);
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
||||
#pragma warning disable 219, 612, 618
|
||||
#nullable enable
|
||||
|
||||
namespace Pal.Client.Database.Compiled
|
||||
{
|
||||
internal partial class ClientLocationImportHistoryEntityType
|
||||
{
|
||||
public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? baseEntityType = null)
|
||||
{
|
||||
var runtimeEntityType = model.AddEntityType(
|
||||
"ClientLocationImportHistory",
|
||||
typeof(Dictionary<string, object>),
|
||||
baseEntityType,
|
||||
sharedClrType: true,
|
||||
indexerPropertyInfo: RuntimeEntityType.FindIndexerProperty(typeof(Dictionary<string, object>)),
|
||||
propertyBag: true);
|
||||
|
||||
var importedById = runtimeEntityType.AddProperty(
|
||||
"ImportedById",
|
||||
typeof(Guid),
|
||||
propertyInfo: runtimeEntityType.FindIndexerPropertyInfo(),
|
||||
afterSaveBehavior: PropertySaveBehavior.Throw);
|
||||
|
||||
var importedLocationsLocalId = runtimeEntityType.AddProperty(
|
||||
"ImportedLocationsLocalId",
|
||||
typeof(int),
|
||||
propertyInfo: runtimeEntityType.FindIndexerPropertyInfo(),
|
||||
afterSaveBehavior: PropertySaveBehavior.Throw);
|
||||
|
||||
var key = runtimeEntityType.AddKey(
|
||||
new[] { importedById, importedLocationsLocalId });
|
||||
runtimeEntityType.SetPrimaryKey(key);
|
||||
|
||||
var index = runtimeEntityType.AddIndex(
|
||||
new[] { importedLocationsLocalId });
|
||||
|
||||
return runtimeEntityType;
|
||||
}
|
||||
|
||||
public static RuntimeForeignKey CreateForeignKey1(RuntimeEntityType declaringEntityType, RuntimeEntityType principalEntityType)
|
||||
{
|
||||
var runtimeForeignKey = declaringEntityType.AddForeignKey(new[] { declaringEntityType.FindProperty("ImportedById")! },
|
||||
principalEntityType.FindKey(new[] { principalEntityType.FindProperty("Id")! })!,
|
||||
principalEntityType,
|
||||
deleteBehavior: DeleteBehavior.Cascade,
|
||||
required: true);
|
||||
|
||||
return runtimeForeignKey;
|
||||
}
|
||||
|
||||
public static RuntimeForeignKey CreateForeignKey2(RuntimeEntityType declaringEntityType, RuntimeEntityType principalEntityType)
|
||||
{
|
||||
var runtimeForeignKey = declaringEntityType.AddForeignKey(new[] { declaringEntityType.FindProperty("ImportedLocationsLocalId")! },
|
||||
principalEntityType.FindKey(new[] { principalEntityType.FindProperty("LocalId")! })!,
|
||||
principalEntityType,
|
||||
deleteBehavior: DeleteBehavior.Cascade,
|
||||
required: true);
|
||||
|
||||
return runtimeForeignKey;
|
||||
}
|
||||
|
||||
public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
|
||||
{
|
||||
runtimeEntityType.AddAnnotation("Relational:FunctionName", null);
|
||||
runtimeEntityType.AddAnnotation("Relational:Schema", null);
|
||||
runtimeEntityType.AddAnnotation("Relational:SqlQuery", null);
|
||||
runtimeEntityType.AddAnnotation("Relational:TableName", "LocationImports");
|
||||
runtimeEntityType.AddAnnotation("Relational:ViewName", null);
|
||||
runtimeEntityType.AddAnnotation("Relational:ViewSchema", null);
|
||||
|
||||
Customize(runtimeEntityType);
|
||||
}
|
||||
|
||||
static partial void Customize(RuntimeEntityType runtimeEntityType);
|
||||
}
|
||||
}
|
94
Pal.Client/Database/Compiled/ImportHistoryEntityType.cs
Normal file
94
Pal.Client/Database/Compiled/ImportHistoryEntityType.cs
Normal file
@ -0,0 +1,94 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
||||
#pragma warning disable 219, 612, 618
|
||||
#nullable enable
|
||||
|
||||
namespace Pal.Client.Database.Compiled
|
||||
{
|
||||
internal partial class ImportHistoryEntityType
|
||||
{
|
||||
public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? baseEntityType = null)
|
||||
{
|
||||
var runtimeEntityType = model.AddEntityType(
|
||||
"Pal.Client.Database.ImportHistory",
|
||||
typeof(ImportHistory),
|
||||
baseEntityType);
|
||||
|
||||
var id = runtimeEntityType.AddProperty(
|
||||
"Id",
|
||||
typeof(Guid),
|
||||
propertyInfo: typeof(ImportHistory).GetProperty("Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(ImportHistory).GetField("<Id>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
valueGenerated: ValueGenerated.OnAdd,
|
||||
afterSaveBehavior: PropertySaveBehavior.Throw);
|
||||
|
||||
var exportedAt = runtimeEntityType.AddProperty(
|
||||
"ExportedAt",
|
||||
typeof(DateTime),
|
||||
propertyInfo: typeof(ImportHistory).GetProperty("ExportedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(ImportHistory).GetField("<ExportedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
|
||||
|
||||
var importedAt = runtimeEntityType.AddProperty(
|
||||
"ImportedAt",
|
||||
typeof(DateTime),
|
||||
propertyInfo: typeof(ImportHistory).GetProperty("ImportedAt", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(ImportHistory).GetField("<ImportedAt>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
|
||||
|
||||
var remoteUrl = runtimeEntityType.AddProperty(
|
||||
"RemoteUrl",
|
||||
typeof(string),
|
||||
propertyInfo: typeof(ImportHistory).GetProperty("RemoteUrl", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(ImportHistory).GetField("<RemoteUrl>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
nullable: true);
|
||||
|
||||
var key = runtimeEntityType.AddKey(
|
||||
new[] { id });
|
||||
runtimeEntityType.SetPrimaryKey(key);
|
||||
|
||||
return runtimeEntityType;
|
||||
}
|
||||
|
||||
public static RuntimeSkipNavigation CreateSkipNavigation1(RuntimeEntityType declaringEntityType, RuntimeEntityType targetEntityType, RuntimeEntityType joinEntityType)
|
||||
{
|
||||
var skipNavigation = declaringEntityType.AddSkipNavigation(
|
||||
"ImportedLocations",
|
||||
targetEntityType,
|
||||
joinEntityType.FindForeignKey(
|
||||
new[] { joinEntityType.FindProperty("ImportedById")! },
|
||||
declaringEntityType.FindKey(new[] { declaringEntityType.FindProperty("Id")! })!,
|
||||
declaringEntityType)!,
|
||||
true,
|
||||
false,
|
||||
typeof(List<ClientLocation>),
|
||||
propertyInfo: typeof(ImportHistory).GetProperty("ImportedLocations", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(ImportHistory).GetField("<ImportedLocations>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
|
||||
|
||||
var inverse = targetEntityType.FindSkipNavigation("ImportedBy");
|
||||
if (inverse != null)
|
||||
{
|
||||
skipNavigation.Inverse = inverse;
|
||||
inverse.Inverse = skipNavigation;
|
||||
}
|
||||
|
||||
return skipNavigation;
|
||||
}
|
||||
|
||||
public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
|
||||
{
|
||||
runtimeEntityType.AddAnnotation("Relational:FunctionName", null);
|
||||
runtimeEntityType.AddAnnotation("Relational:Schema", null);
|
||||
runtimeEntityType.AddAnnotation("Relational:SqlQuery", null);
|
||||
runtimeEntityType.AddAnnotation("Relational:TableName", "Imports");
|
||||
runtimeEntityType.AddAnnotation("Relational:ViewName", null);
|
||||
runtimeEntityType.AddAnnotation("Relational:ViewSchema", null);
|
||||
|
||||
Customize(runtimeEntityType);
|
||||
}
|
||||
|
||||
static partial void Customize(RuntimeEntityType runtimeEntityType);
|
||||
}
|
||||
}
|
28
Pal.Client/Database/Compiled/PalClientContextModel.cs
Normal file
28
Pal.Client/Database/Compiled/PalClientContextModel.cs
Normal file
@ -0,0 +1,28 @@
|
||||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
||||
#pragma warning disable 219, 612, 618
|
||||
#nullable enable
|
||||
|
||||
namespace Pal.Client.Database.Compiled
|
||||
{
|
||||
[DbContext(typeof(PalClientContext))]
|
||||
public partial class PalClientContextModel : RuntimeModel
|
||||
{
|
||||
static PalClientContextModel()
|
||||
{
|
||||
var model = new PalClientContextModel();
|
||||
model.Initialize();
|
||||
model.Customize();
|
||||
_instance = model;
|
||||
}
|
||||
|
||||
private static PalClientContextModel _instance;
|
||||
public static IModel Instance => _instance;
|
||||
|
||||
partial void Initialize();
|
||||
|
||||
partial void Customize();
|
||||
}
|
||||
}
|
35
Pal.Client/Database/Compiled/PalClientContextModelBuilder.cs
Normal file
35
Pal.Client/Database/Compiled/PalClientContextModelBuilder.cs
Normal file
@ -0,0 +1,35 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
||||
#pragma warning disable 219, 612, 618
|
||||
#nullable enable
|
||||
|
||||
namespace Pal.Client.Database.Compiled
|
||||
{
|
||||
public partial class PalClientContextModel
|
||||
{
|
||||
partial void Initialize()
|
||||
{
|
||||
var clientLocationImportHistory = ClientLocationImportHistoryEntityType.Create(this);
|
||||
var clientLocation = ClientLocationEntityType.Create(this);
|
||||
var importHistory = ImportHistoryEntityType.Create(this);
|
||||
var remoteEncounter = RemoteEncounterEntityType.Create(this);
|
||||
|
||||
ClientLocationImportHistoryEntityType.CreateForeignKey1(clientLocationImportHistory, importHistory);
|
||||
ClientLocationImportHistoryEntityType.CreateForeignKey2(clientLocationImportHistory, clientLocation);
|
||||
RemoteEncounterEntityType.CreateForeignKey1(remoteEncounter, clientLocation);
|
||||
|
||||
ClientLocationEntityType.CreateSkipNavigation1(clientLocation, importHistory, clientLocationImportHistory);
|
||||
ImportHistoryEntityType.CreateSkipNavigation1(importHistory, clientLocation, clientLocationImportHistory);
|
||||
|
||||
ClientLocationImportHistoryEntityType.CreateAnnotations(clientLocationImportHistory);
|
||||
ClientLocationEntityType.CreateAnnotations(clientLocation);
|
||||
ImportHistoryEntityType.CreateAnnotations(importHistory);
|
||||
RemoteEncounterEntityType.CreateAnnotations(remoteEncounter);
|
||||
|
||||
AddAnnotation("ProductVersion", "7.0.3");
|
||||
}
|
||||
}
|
||||
}
|
92
Pal.Client/Database/Compiled/RemoteEncounterEntityType.cs
Normal file
92
Pal.Client/Database/Compiled/RemoteEncounterEntityType.cs
Normal file
@ -0,0 +1,92 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
||||
#pragma warning disable 219, 612, 618
|
||||
#nullable enable
|
||||
|
||||
namespace Pal.Client.Database.Compiled
|
||||
{
|
||||
internal partial class RemoteEncounterEntityType
|
||||
{
|
||||
public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType? baseEntityType = null)
|
||||
{
|
||||
var runtimeEntityType = model.AddEntityType(
|
||||
"Pal.Client.Database.RemoteEncounter",
|
||||
typeof(RemoteEncounter),
|
||||
baseEntityType);
|
||||
|
||||
var id = runtimeEntityType.AddProperty(
|
||||
"Id",
|
||||
typeof(int),
|
||||
propertyInfo: typeof(RemoteEncounter).GetProperty("Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(RemoteEncounter).GetField("<Id>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
valueGenerated: ValueGenerated.OnAdd,
|
||||
afterSaveBehavior: PropertySaveBehavior.Throw);
|
||||
|
||||
var accountId = runtimeEntityType.AddProperty(
|
||||
"AccountId",
|
||||
typeof(string),
|
||||
propertyInfo: typeof(RemoteEncounter).GetProperty("AccountId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(RemoteEncounter).GetField("<AccountId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
maxLength: 13);
|
||||
|
||||
var clientLocationId = runtimeEntityType.AddProperty(
|
||||
"ClientLocationId",
|
||||
typeof(int),
|
||||
propertyInfo: typeof(RemoteEncounter).GetProperty("ClientLocationId", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(RemoteEncounter).GetField("<ClientLocationId>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
|
||||
|
||||
var key = runtimeEntityType.AddKey(
|
||||
new[] { id });
|
||||
runtimeEntityType.SetPrimaryKey(key);
|
||||
|
||||
var index = runtimeEntityType.AddIndex(
|
||||
new[] { clientLocationId });
|
||||
|
||||
return runtimeEntityType;
|
||||
}
|
||||
|
||||
public static RuntimeForeignKey CreateForeignKey1(RuntimeEntityType declaringEntityType, RuntimeEntityType principalEntityType)
|
||||
{
|
||||
var runtimeForeignKey = declaringEntityType.AddForeignKey(new[] { declaringEntityType.FindProperty("ClientLocationId")! },
|
||||
principalEntityType.FindKey(new[] { principalEntityType.FindProperty("LocalId")! })!,
|
||||
principalEntityType,
|
||||
deleteBehavior: DeleteBehavior.Cascade,
|
||||
required: true);
|
||||
|
||||
var clientLocation = declaringEntityType.AddNavigation("ClientLocation",
|
||||
runtimeForeignKey,
|
||||
onDependent: true,
|
||||
typeof(ClientLocation),
|
||||
propertyInfo: typeof(RemoteEncounter).GetProperty("ClientLocation", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(RemoteEncounter).GetField("<ClientLocation>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
|
||||
|
||||
var remoteEncounters = principalEntityType.AddNavigation("RemoteEncounters",
|
||||
runtimeForeignKey,
|
||||
onDependent: false,
|
||||
typeof(List<RemoteEncounter>),
|
||||
propertyInfo: typeof(ClientLocation).GetProperty("RemoteEncounters", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
|
||||
fieldInfo: typeof(ClientLocation).GetField("<RemoteEncounters>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly));
|
||||
|
||||
return runtimeForeignKey;
|
||||
}
|
||||
|
||||
public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
|
||||
{
|
||||
runtimeEntityType.AddAnnotation("Relational:FunctionName", null);
|
||||
runtimeEntityType.AddAnnotation("Relational:Schema", null);
|
||||
runtimeEntityType.AddAnnotation("Relational:SqlQuery", null);
|
||||
runtimeEntityType.AddAnnotation("Relational:TableName", "RemoteEncounters");
|
||||
runtimeEntityType.AddAnnotation("Relational:ViewName", null);
|
||||
runtimeEntityType.AddAnnotation("Relational:ViewSchema", null);
|
||||
|
||||
Customize(runtimeEntityType);
|
||||
}
|
||||
|
||||
static partial void Customize(RuntimeEntityType runtimeEntityType);
|
||||
}
|
||||
}
|
14
Pal.Client/Database/ImportHistory.cs
Normal file
14
Pal.Client/Database/ImportHistory.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
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();
|
||||
}
|
45
Pal.Client/Database/Migrations/20230216154417_AddImportHistory.Designer.cs
generated
Normal file
45
Pal.Client/Database/Migrations/20230216154417_AddImportHistory.Designer.cs
generated
Normal file
@ -0,0 +1,45 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Pal.Client.Database;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Pal.Client.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(PalClientContext))]
|
||||
[Migration("20230216154417_AddImportHistory")]
|
||||
partial class AddImportHistory
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.3");
|
||||
|
||||
modelBuilder.Entity("Pal.Client.Database.ImportHistory", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("ExportedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("ImportedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RemoteUrl")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Imports");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Pal.Client.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddImportHistory : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Imports",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
RemoteUrl = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ExportedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
ImportedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Imports", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Imports");
|
||||
}
|
||||
}
|
||||
}
|
136
Pal.Client/Database/Migrations/20230217160342_AddClientLocations.Designer.cs
generated
Normal file
136
Pal.Client/Database/Migrations/20230217160342_AddClientLocations.Designer.cs
generated
Normal file
@ -0,0 +1,136 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Pal.Client.Database;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Pal.Client.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(PalClientContext))]
|
||||
[Migration("20230217160342_AddClientLocations")]
|
||||
partial class AddClientLocations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.3");
|
||||
|
||||
modelBuilder.Entity("ClientLocationImportHistory", b =>
|
||||
{
|
||||
b.Property<Guid>("ImportedById")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ImportedLocationsLocalId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ImportedById", "ImportedLocationsLocalId");
|
||||
|
||||
b.HasIndex("ImportedLocationsLocalId");
|
||||
|
||||
b.ToTable("LocationImports", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Pal.Client.Database.ClientLocation", b =>
|
||||
{
|
||||
b.Property<int>("LocalId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Seen")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ushort>("TerritoryType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<float>("X")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<float>("Y")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<float>("Z")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.HasKey("LocalId");
|
||||
|
||||
b.ToTable("Locations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Pal.Client.Database.ImportHistory", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("ExportedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("ImportedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RemoteUrl")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Imports");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AccountId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(13)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ClientLocationId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClientLocationId");
|
||||
|
||||
b.ToTable("RemoteEncounters");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClientLocationImportHistory", b =>
|
||||
{
|
||||
b.HasOne("Pal.Client.Database.ImportHistory", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ImportedById")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Pal.Client.Database.ClientLocation", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ImportedLocationsLocalId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b =>
|
||||
{
|
||||
b.HasOne("Pal.Client.Database.ClientLocation", "ClientLocation")
|
||||
.WithMany()
|
||||
.HasForeignKey("ClientLocationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ClientLocation");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Pal.Client.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddClientLocations : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Locations",
|
||||
columns: table => new
|
||||
{
|
||||
LocalId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
TerritoryType = table.Column<ushort>(type: "INTEGER", nullable: false),
|
||||
Type = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
X = table.Column<float>(type: "REAL", nullable: false),
|
||||
Y = table.Column<float>(type: "REAL", nullable: false),
|
||||
Z = table.Column<float>(type: "REAL", nullable: false),
|
||||
Seen = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Locations", x => x.LocalId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "LocationImports",
|
||||
columns: table => new
|
||||
{
|
||||
ImportedById = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
ImportedLocationsLocalId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_LocationImports", x => new { x.ImportedById, x.ImportedLocationsLocalId });
|
||||
table.ForeignKey(
|
||||
name: "FK_LocationImports_Imports_ImportedById",
|
||||
column: x => x.ImportedById,
|
||||
principalTable: "Imports",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_LocationImports_Locations_ImportedLocationsLocalId",
|
||||
column: x => x.ImportedLocationsLocalId,
|
||||
principalTable: "Locations",
|
||||
principalColumn: "LocalId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "RemoteEncounters",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ClientLocationId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
AccountId = table.Column<string>(type: "TEXT", maxLength: 13, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_RemoteEncounters", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_RemoteEncounters_Locations_ClientLocationId",
|
||||
column: x => x.ClientLocationId,
|
||||
principalTable: "Locations",
|
||||
principalColumn: "LocalId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_LocationImports_ImportedLocationsLocalId",
|
||||
table: "LocationImports",
|
||||
column: "ImportedLocationsLocalId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_RemoteEncounters_ClientLocationId",
|
||||
table: "RemoteEncounters",
|
||||
column: "ClientLocationId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "LocationImports");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "RemoteEncounters");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Locations");
|
||||
}
|
||||
}
|
||||
}
|
148
Pal.Client/Database/Migrations/20230218112804_AddImportedAndSinceVersionToClientLocation.Designer.cs
generated
Normal file
148
Pal.Client/Database/Migrations/20230218112804_AddImportedAndSinceVersionToClientLocation.Designer.cs
generated
Normal file
@ -0,0 +1,148 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Pal.Client.Database;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Pal.Client.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(PalClientContext))]
|
||||
[Migration("20230218112804_AddImportedAndSinceVersionToClientLocation")]
|
||||
partial class AddImportedAndSinceVersionToClientLocation
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.3");
|
||||
|
||||
modelBuilder.Entity("ClientLocationImportHistory", b =>
|
||||
{
|
||||
b.Property<Guid>("ImportedById")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ImportedLocationsLocalId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ImportedById", "ImportedLocationsLocalId");
|
||||
|
||||
b.HasIndex("ImportedLocationsLocalId");
|
||||
|
||||
b.ToTable("LocationImports", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Pal.Client.Database.ClientLocation", b =>
|
||||
{
|
||||
b.Property<int>("LocalId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Imported")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Seen")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SinceVersion")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<ushort>("TerritoryType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<float>("X")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<float>("Y")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<float>("Z")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.HasKey("LocalId");
|
||||
|
||||
b.ToTable("Locations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Pal.Client.Database.ImportHistory", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("ExportedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("ImportedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RemoteUrl")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Imports");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AccountId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(13)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ClientLocationId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClientLocationId");
|
||||
|
||||
b.ToTable("RemoteEncounters");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClientLocationImportHistory", b =>
|
||||
{
|
||||
b.HasOne("Pal.Client.Database.ImportHistory", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ImportedById")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Pal.Client.Database.ClientLocation", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ImportedLocationsLocalId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b =>
|
||||
{
|
||||
b.HasOne("Pal.Client.Database.ClientLocation", "ClientLocation")
|
||||
.WithMany("RemoteEncounters")
|
||||
.HasForeignKey("ClientLocationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ClientLocation");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Pal.Client.Database.ClientLocation", b =>
|
||||
{
|
||||
b.Navigation("RemoteEncounters");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Pal.Client.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddImportedAndSinceVersionToClientLocation : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "Imported",
|
||||
table: "Locations",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SinceVersion",
|
||||
table: "Locations",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Imported",
|
||||
table: "Locations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SinceVersion",
|
||||
table: "Locations");
|
||||
}
|
||||
}
|
||||
}
|
148
Pal.Client/Database/Migrations/20230222191929_ChangeLocationImportedToSource.Designer.cs
generated
Normal file
148
Pal.Client/Database/Migrations/20230222191929_ChangeLocationImportedToSource.Designer.cs
generated
Normal file
@ -0,0 +1,148 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Pal.Client.Database;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Pal.Client.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(PalClientContext))]
|
||||
[Migration("20230222191929_ChangeLocationImportedToSource")]
|
||||
partial class ChangeLocationImportedToSource
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.3");
|
||||
|
||||
modelBuilder.Entity("ClientLocationImportHistory", b =>
|
||||
{
|
||||
b.Property<Guid>("ImportedById")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ImportedLocationsLocalId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ImportedById", "ImportedLocationsLocalId");
|
||||
|
||||
b.HasIndex("ImportedLocationsLocalId");
|
||||
|
||||
b.ToTable("LocationImports", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Pal.Client.Database.ClientLocation", b =>
|
||||
{
|
||||
b.Property<int>("LocalId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Seen")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SinceVersion")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Source")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ushort>("TerritoryType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<float>("X")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<float>("Y")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<float>("Z")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.HasKey("LocalId");
|
||||
|
||||
b.ToTable("Locations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Pal.Client.Database.ImportHistory", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("ExportedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("ImportedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RemoteUrl")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Imports");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AccountId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(13)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ClientLocationId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClientLocationId");
|
||||
|
||||
b.ToTable("RemoteEncounters");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClientLocationImportHistory", b =>
|
||||
{
|
||||
b.HasOne("Pal.Client.Database.ImportHistory", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ImportedById")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Pal.Client.Database.ClientLocation", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ImportedLocationsLocalId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b =>
|
||||
{
|
||||
b.HasOne("Pal.Client.Database.ClientLocation", "ClientLocation")
|
||||
.WithMany("RemoteEncounters")
|
||||
.HasForeignKey("ClientLocationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ClientLocation");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Pal.Client.Database.ClientLocation", b =>
|
||||
{
|
||||
b.Navigation("RemoteEncounters");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Pal.Client.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ChangeLocationImportedToSource : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Imported",
|
||||
table: "Locations",
|
||||
newName: "Source");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Source",
|
||||
table: "Locations",
|
||||
newName: "Imported");
|
||||
}
|
||||
}
|
||||
}
|
145
Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs
Normal file
145
Pal.Client/Database/Migrations/PalClientContextModelSnapshot.cs
Normal file
@ -0,0 +1,145 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Pal.Client.Database;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Pal.Client.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(PalClientContext))]
|
||||
partial class PalClientContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.3");
|
||||
|
||||
modelBuilder.Entity("ClientLocationImportHistory", b =>
|
||||
{
|
||||
b.Property<Guid>("ImportedById")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ImportedLocationsLocalId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("ImportedById", "ImportedLocationsLocalId");
|
||||
|
||||
b.HasIndex("ImportedLocationsLocalId");
|
||||
|
||||
b.ToTable("LocationImports", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Pal.Client.Database.ClientLocation", b =>
|
||||
{
|
||||
b.Property<int>("LocalId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Seen")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SinceVersion")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Source")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<ushort>("TerritoryType")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<float>("X")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<float>("Y")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<float>("Z")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.HasKey("LocalId");
|
||||
|
||||
b.ToTable("Locations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Pal.Client.Database.ImportHistory", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("ExportedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("ImportedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RemoteUrl")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Imports");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AccountId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(13)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ClientLocationId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClientLocationId");
|
||||
|
||||
b.ToTable("RemoteEncounters");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ClientLocationImportHistory", b =>
|
||||
{
|
||||
b.HasOne("Pal.Client.Database.ImportHistory", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ImportedById")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Pal.Client.Database.ClientLocation", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ImportedLocationsLocalId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Pal.Client.Database.RemoteEncounter", b =>
|
||||
{
|
||||
b.HasOne("Pal.Client.Database.ClientLocation", "ClientLocation")
|
||||
.WithMany("RemoteEncounters")
|
||||
.HasForeignKey("ClientLocationId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ClientLocation");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Pal.Client.Database.ClientLocation", b =>
|
||||
{
|
||||
b.Navigation("RemoteEncounters");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
23
Pal.Client/Database/PalClientContext.cs
Normal file
23
Pal.Client/Database/PalClientContext.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Pal.Client.Database;
|
||||
|
||||
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)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<ClientLocation>()
|
||||
.HasMany(o => o.ImportedBy)
|
||||
.WithMany(o => o.ImportedLocations)
|
||||
.UsingEntity(o => o.ToTable("LocationImports"));
|
||||
}
|
||||
}
|
20
Pal.Client/Database/PalClientContextFactory.cs
Normal file
20
Pal.Client/Database/PalClientContextFactory.cs
Normal file
@ -0,0 +1,20 @@
|
||||
#if EF
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
|
||||
namespace Pal.Client.Database
|
||||
{
|
||||
internal sealed class PalClientContextFactory : IDesignTimeDbContextFactory<PalClientContext>
|
||||
{
|
||||
public PalClientContext CreateDbContext(string[] args)
|
||||
{
|
||||
var optionsBuilder =
|
||||
new DbContextOptionsBuilder<PalClientContext>().UseSqlite(
|
||||
$"Data Source={Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "XIVLauncher", "pluginConfigs", "Palace Pal", "palace-pal.data.sqlite3")}");
|
||||
return new PalClientContext(optionsBuilder.Options);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
40
Pal.Client/Database/RemoteEncounter.cs
Normal file
40
Pal.Client/Database/RemoteEncounter.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Pal.Client.Extensions;
|
||||
using Pal.Client.Net;
|
||||
|
||||
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>
|
||||
/// 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();
|
||||
}
|
||||
}
|
195
Pal.Client/DependencyContextInitializer.cs
Normal file
195
Pal.Client/DependencyContextInitializer.cs
Normal file
@ -0,0 +1,195 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Plugin;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Pal.Client.Commands;
|
||||
using Pal.Client.Configuration;
|
||||
using Pal.Client.Configuration.Legacy;
|
||||
using Pal.Client.Database;
|
||||
using Pal.Client.DependencyInjection;
|
||||
using Pal.Client.Floors;
|
||||
using Pal.Client.Windows;
|
||||
|
||||
namespace Pal.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Takes care of async plugin init - this is mostly everything that requires either the config or the database to
|
||||
/// be available.
|
||||
/// </summary>
|
||||
internal sealed class DependencyContextInitializer
|
||||
{
|
||||
private readonly ILogger<DependencyContextInitializer> _logger;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public DependencyContextInitializer(ILogger<DependencyContextInitializer> logger,
|
||||
IServiceProvider serviceProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using IDisposable? logScope = _logger.BeginScope("AsyncInit");
|
||||
|
||||
_logger.LogInformation("Starting async init");
|
||||
|
||||
await CreateBackup();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await RunMigrations(cancellationToken);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// v1 migration: config migration for import history, json migration for markers
|
||||
_serviceProvider.GetRequiredService<ConfigurationManager>().Migrate();
|
||||
await _serviceProvider.GetRequiredService<JsonMigration>().MigrateAsync(cancellationToken);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await RunCleanup();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await RemoveOldBackups();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// windows that have logic to open on startup
|
||||
_serviceProvider.GetRequiredService<AgreementWindow>();
|
||||
|
||||
// initialize components that are mostly self-contained/self-registered
|
||||
_serviceProvider.GetRequiredService<GameHooks>();
|
||||
_serviceProvider.GetRequiredService<FrameworkService>();
|
||||
_serviceProvider.GetRequiredService<ChatService>();
|
||||
|
||||
// eager load any commands to find errors now, not when running them
|
||||
_serviceProvider.GetRequiredService<IEnumerable<ISubCommand>>();
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (_serviceProvider.GetRequiredService<IPalacePalConfiguration>().HasBetaFeature(ObjectTableDebug.FeatureName))
|
||||
_serviceProvider.GetRequiredService<ObjectTableDebug>();
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
_logger.LogInformation("Async init complete");
|
||||
}
|
||||
|
||||
private async Task RemoveOldBackups()
|
||||
{
|
||||
await using var scope = _serviceProvider.CreateAsyncScope();
|
||||
var pluginInterface = scope.ServiceProvider.GetRequiredService<IDalamudPluginInterface>();
|
||||
var configuration = scope.ServiceProvider.GetRequiredService<IPalacePalConfiguration>();
|
||||
|
||||
var paths = Directory.GetFiles(pluginInterface.GetPluginConfigDirectory(), "backup-*.data.sqlite3",
|
||||
new EnumerationOptions
|
||||
{
|
||||
IgnoreInaccessible = true,
|
||||
RecurseSubdirectories = false,
|
||||
MatchCasing = MatchCasing.CaseSensitive,
|
||||
AttributesToSkip = FileAttributes.ReadOnly | FileAttributes.Hidden | FileAttributes.System,
|
||||
ReturnSpecialDirectories = false,
|
||||
});
|
||||
if (paths.Length == 0)
|
||||
return;
|
||||
|
||||
Regex backupRegex = new Regex(@"backup-([\d\-]{10})\.data\.sqlite3", RegexOptions.Compiled);
|
||||
List<(DateTime Date, string Path)> backupFiles = new();
|
||||
foreach (string path in paths)
|
||||
{
|
||||
var match = backupRegex.Match(Path.GetFileName(path));
|
||||
if (!match.Success)
|
||||
continue;
|
||||
|
||||
if (DateTime.TryParseExact(match.Groups[1].Value, "yyyy-MM-dd", CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AssumeUniversal, out DateTime backupDate))
|
||||
{
|
||||
backupFiles.Add((backupDate, path));
|
||||
}
|
||||
}
|
||||
|
||||
var toDelete = backupFiles.OrderByDescending(x => x.Date)
|
||||
.Skip(configuration.Backups.MinimumBackupsToKeep)
|
||||
.Where(x => (DateTime.Now.ToUniversalTime() - x.Date).Days > configuration.Backups.DaysToDeleteAfter)
|
||||
.Select(x => x.Path);
|
||||
foreach (var path in toDelete)
|
||||
{
|
||||
try
|
||||
{
|
||||
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()
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
38
Pal.Client/DependencyInjection/Chat.cs
Normal file
38
Pal.Client/DependencyInjection/Chat.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Plugin.Services;
|
||||
using ECommons.DalamudServices.Legacy;
|
||||
using Pal.Client.Properties;
|
||||
|
||||
namespace Pal.Client.DependencyInjection;
|
||||
|
||||
internal sealed class Chat
|
||||
{
|
||||
private readonly IChatGui _chatGui;
|
||||
|
||||
public Chat(IChatGui 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);
|
||||
}
|
116
Pal.Client/DependencyInjection/ChatService.cs
Normal file
116
Pal.Client/DependencyInjection/ChatService.cs
Normal file
@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using Pal.Client.Configuration;
|
||||
using Pal.Client.Floors;
|
||||
|
||||
namespace Pal.Client.DependencyInjection;
|
||||
|
||||
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)
|
||||
{
|
||||
_chatGui = chatGui;
|
||||
_territoryState = territoryState;
|
||||
_configuration = configuration;
|
||||
_dataManager = dataManager;
|
||||
|
||||
_localizedChatMessages = LoadLanguageStrings();
|
||||
|
||||
_chatGui.ChatMessage += OnChatMessage;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
=> _chatGui.ChatMessage -= OnChatMessage;
|
||||
|
||||
private void OnChatMessage(XivChatType type, int senderId, ref SeString sender, ref SeString seMessage,
|
||||
ref bool isHandled)
|
||||
{
|
||||
if (_configuration.FirstUse)
|
||||
return;
|
||||
|
||||
if (type != (XivChatType)2105)
|
||||
return;
|
||||
|
||||
string message = seMessage.ToString();
|
||||
if (_localizedChatMessages.FloorChanged.IsMatch(message))
|
||||
{
|
||||
_territoryState.PomanderOfSight = PomanderState.Inactive;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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+)$");
|
||||
}
|
||||
}
|
14
Pal.Client/DependencyInjection/DebugState.cs
Normal file
14
Pal.Client/DependencyInjection/DebugState.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace Pal.Client.DependencyInjection;
|
||||
|
||||
internal sealed class DebugState
|
||||
{
|
||||
public string? DebugMessage { get; set; }
|
||||
|
||||
public void SetFromException(Exception e)
|
||||
=> DebugMessage = $"{DateTime.Now}\n{e}";
|
||||
|
||||
public void Reset()
|
||||
=> DebugMessage = null;
|
||||
}
|
106
Pal.Client/DependencyInjection/GameHooks.cs
Normal file
106
Pal.Client/DependencyInjection/GameHooks.cs
Normal file
@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Memory;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility.Signatures;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Pal.Client.Floors;
|
||||
|
||||
namespace Pal.Client.DependencyInjection;
|
||||
|
||||
internal sealed unsafe class GameHooks : IDisposable
|
||||
{
|
||||
private readonly ILogger<GameHooks> _logger;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly TerritoryState _territoryState;
|
||||
private readonly FrameworkService _frameworkService;
|
||||
|
||||
#pragma warning disable CS0649
|
||||
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))]
|
||||
private Hook<ActorVfxCreateDelegate> ActorVfxCreateHook { get; init; } = null!;
|
||||
#pragma warning restore CS0649
|
||||
|
||||
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
|
||||
{
|
||||
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))
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "VFX Create Hook failed");
|
||||
}
|
||||
return ActorVfxCreateHook.Original(a1, a2, a3, a4, a5, a6, a7);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_logger.LogDebug("Disposing game hooks");
|
||||
ActorVfxCreateHook.Dispose();
|
||||
}
|
||||
}
|
165
Pal.Client/DependencyInjection/ImportService.cs
Normal file
165
Pal.Client/DependencyInjection/ImportService.cs
Normal file
@ -0,0 +1,165 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Export;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Pal.Client.Database;
|
||||
using Pal.Client.Floors;
|
||||
using Pal.Client.Floors.Tasks;
|
||||
using Pal.Common;
|
||||
|
||||
namespace Pal.Client.DependencyInjection;
|
||||
|
||||
internal sealed class ImportService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly FloorService _floorService;
|
||||
private readonly Cleanup _cleanup;
|
||||
|
||||
public ImportService(
|
||||
IServiceProvider serviceProvider,
|
||||
FloorService floorService,
|
||||
Cleanup cleanup)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_floorService = floorService;
|
||||
_cleanup = cleanup;
|
||||
}
|
||||
|
||||
public async Task<ImportHistory?> FindLast(CancellationToken token = default)
|
||||
{
|
||||
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)
|
||||
.FirstOrDefaultAsync(cancellationToken: token);
|
||||
}
|
||||
|
||||
public (int traps, int hoard) Import(ExportRoot import)
|
||||
{
|
||||
try
|
||||
{
|
||||
_floorService.SetToImportState();
|
||||
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
|
||||
|
||||
dbContext.Imports.RemoveRange(dbContext.Imports.Where(x => x.RemoteUrl == import.ServerUrl).ToList());
|
||||
dbContext.SaveChanges();
|
||||
|
||||
ImportHistory importHistory = new ImportHistory
|
||||
{
|
||||
Id = Guid.Parse(import.ExportId),
|
||||
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),
|
||||
Source = ClientLocation.ESource.Unknown,
|
||||
};
|
||||
|
||||
var existingLocation = existingLocations.FirstOrDefault(x => x == persistentLocation);
|
||||
if (existingLocation != null)
|
||||
{
|
||||
var clientLoc = dbContext.Locations.FirstOrDefault(o => o.LocalId == existingLocation.LocalId);
|
||||
clientLoc?.ImportedBy.Add(importHistory);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
ClientLocation clientLocation = new ClientLocation
|
||||
{
|
||||
TerritoryType = (ushort)territoryType,
|
||||
Type = ToClientLocationType(exportLocation.Type),
|
||||
X = exportLocation.X,
|
||||
Y = exportLocation.Y,
|
||||
Z = exportLocation.Z,
|
||||
Seen = false,
|
||||
Source = ClientLocation.ESource.Import,
|
||||
ImportedBy = new List<ImportHistory> { importHistory },
|
||||
SinceVersion = typeof(Plugin).Assembly.GetName().Version!.ToString(2),
|
||||
};
|
||||
dbContext.Locations.Add(clientLocation);
|
||||
|
||||
if (exportLocation.Type == ExportObjectType.Trap)
|
||||
traps++;
|
||||
else if (exportLocation.Type == ExportObjectType.Hoard)
|
||||
hoard++;
|
||||
}
|
||||
}
|
||||
|
||||
dbContext.SaveChanges();
|
||||
|
||||
_cleanup.Purge(dbContext);
|
||||
dbContext.SaveChanges();
|
||||
|
||||
return (traps, hoard);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_floorService.ResetAll();
|
||||
}
|
||||
}
|
||||
|
||||
private MemoryLocation.EType ToMemoryType(ExportObjectType exportLocationType)
|
||||
{
|
||||
return exportLocationType switch
|
||||
{
|
||||
ExportObjectType.Trap => MemoryLocation.EType.Trap,
|
||||
ExportObjectType.Hoard => MemoryLocation.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
|
||||
{
|
||||
_floorService.ResetAll();
|
||||
}
|
||||
}
|
||||
}
|
27
Pal.Client/DependencyInjection/RepoVerification.cs
Normal file
27
Pal.Client/DependencyInjection/RepoVerification.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Logging;
|
||||
using Dalamud.Plugin;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Pal.Client.Extensions;
|
||||
using Pal.Client.Properties;
|
||||
|
||||
namespace Pal.Client.DependencyInjection;
|
||||
|
||||
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
|
||||
{
|
||||
}
|
||||
}
|
74
Pal.Client/DependencyInjection/StatisticsService.cs
Normal file
74
Pal.Client/DependencyInjection/StatisticsService.cs
Normal file
@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Game.Gui;
|
||||
using Grpc.Core;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Pal.Client.Configuration;
|
||||
using Pal.Client.Extensions;
|
||||
using Pal.Client.Net;
|
||||
using Pal.Client.Properties;
|
||||
using Pal.Client.Windows;
|
||||
|
||||
namespace Pal.Client.DependencyInjection;
|
||||
|
||||
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)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
_remoteApi = remoteApi;
|
||||
_statisticsWindow = statisticsWindow;
|
||||
_chat = chat;
|
||||
}
|
||||
|
||||
public void ShowGlobalStatistics()
|
||||
{
|
||||
Task.Run(async () => await FetchFloorStatistics());
|
||||
}
|
||||
|
||||
private async Task FetchFloorStatistics()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_configuration.HasRoleOnCurrentServer(RemoteApi.RemoteUrl, "statistics:view"))
|
||||
{
|
||||
_chat.Error(Localization.Command_pal_stats_CurrentFloor);
|
||||
return;
|
||||
}
|
||||
|
||||
var (success, floorStatistics) = await _remoteApi.FetchStatistics();
|
||||
if (success)
|
||||
{
|
||||
_statisticsWindow.SetFloorData(floorStatistics);
|
||||
_statisticsWindow.IsOpen = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_chat.Error(Localization.Command_pal_stats_UnableToFetchStatistics);
|
||||
}
|
||||
}
|
||||
catch (RpcException e) when (e.StatusCode == StatusCode.PermissionDenied)
|
||||
{
|
||||
_logger.LogWarning(e, "Access denied while fetching floor statistics");
|
||||
_chat.Error(Localization.Command_pal_stats_CurrentFloor);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Could not fetch floor statistics");
|
||||
_chat.Error(string.Format(Localization.Error_CommandFailed,
|
||||
$"{e.GetType()} - {e.Message}"));
|
||||
}
|
||||
}
|
||||
}
|
194
Pal.Client/DependencyInjectionContext.cs
Normal file
194
Pal.Client/DependencyInjectionContext.cs
Normal file
@ -0,0 +1,194 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Extensions.MicrosoftLogging;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.ClientState;
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.Command;
|
||||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Pal.Client.Commands;
|
||||
using Pal.Client.Configuration;
|
||||
using Pal.Client.Configuration.Legacy;
|
||||
using Pal.Client.Database;
|
||||
using Pal.Client.DependencyInjection;
|
||||
using Pal.Client.Floors;
|
||||
using Pal.Client.Net;
|
||||
using Pal.Client.Rendering;
|
||||
using Pal.Client.Scheduled;
|
||||
using Pal.Client.Windows;
|
||||
|
||||
namespace Pal.Client;
|
||||
|
||||
/// <summary>
|
||||
/// DI-aware Plugin.
|
||||
/// </summary>
|
||||
internal sealed class DependencyInjectionContext : IDisposable
|
||||
{
|
||||
public const string DatabaseFileName = "palace-pal.data.sqlite3";
|
||||
|
||||
/// <summary>
|
||||
/// Initialized as temporary logger, will be overriden once context is ready with a logger that supports scopes.
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
var loggerProvider = new DalamudLoggerProvider(pluginLog);
|
||||
_logger = loggerProvider.CreateLogger<DependencyInjectionContext>();
|
||||
_logger.LogInformation("Building dalamud service container for {Assembly}",
|
||||
typeof(DependencyInjectionContext).Assembly.FullName);
|
||||
|
||||
// set up legacy services
|
||||
#pragma warning disable CS0612
|
||||
JsonFloorState.SetContextProperties(pluginInterface.GetPluginConfigDirectory());
|
||||
#pragma warning restore CS0612
|
||||
|
||||
// set up logging
|
||||
_serviceCollection.AddLogging(builder =>
|
||||
builder.AddFilter("Pal", LogLevel.Trace)
|
||||
.AddFilter("Microsoft.EntityFrameworkCore.Database", LogLevel.Warning)
|
||||
.AddFilter("Grpc", LogLevel.Debug)
|
||||
.ClearProviders()
|
||||
.AddDalamudLogger(pluginLog));
|
||||
|
||||
// dalamud
|
||||
_serviceCollection.AddSingleton<IDalamudPlugin>(plugin);
|
||||
_serviceCollection.AddSingleton(pluginInterface);
|
||||
_serviceCollection.AddSingleton(clientState);
|
||||
_serviceCollection.AddSingleton(gameGui);
|
||||
_serviceCollection.AddSingleton(chatGui);
|
||||
_serviceCollection.AddSingleton<Chat>();
|
||||
_serviceCollection.AddSingleton(objectTable);
|
||||
_serviceCollection.AddSingleton(framework);
|
||||
_serviceCollection.AddSingleton(condition);
|
||||
_serviceCollection.AddSingleton(commandManager);
|
||||
_serviceCollection.AddSingleton(dataManager);
|
||||
_serviceCollection.AddSingleton(gameInteropProvider);
|
||||
_serviceCollection.AddSingleton(new WindowSystem(typeof(DependencyInjectionContext).AssemblyQualifiedName));
|
||||
|
||||
_sqliteConnectionString =
|
||||
$"Data Source={Path.Join(pluginInterface.GetPluginConfigDirectory(), DatabaseFileName)}";
|
||||
}
|
||||
|
||||
public IServiceProvider BuildServiceContainer()
|
||||
{
|
||||
_logger.LogInformation("Building async service container for {Assembly}",
|
||||
typeof(DependencyInjectionContext).Assembly.FullName);
|
||||
|
||||
// EF core
|
||||
_serviceCollection.AddDbContext<PalClientContext>(o => o
|
||||
.UseSqlite(_sqliteConnectionString)
|
||||
.UseModel(Database.Compiled.PalClientContextModel.Instance));
|
||||
_serviceCollection.AddTransient<JsonMigration>();
|
||||
_serviceCollection.AddScoped<Cleanup>();
|
||||
|
||||
// plugin-specific
|
||||
_serviceCollection.AddScoped<DependencyContextInitializer>();
|
||||
_serviceCollection.AddScoped<DebugState>();
|
||||
_serviceCollection.AddScoped<GameHooks>();
|
||||
_serviceCollection.AddScoped<RemoteApi>();
|
||||
_serviceCollection.AddScoped<ConfigurationManager>();
|
||||
_serviceCollection.AddScoped<IPalacePalConfiguration>(sp =>
|
||||
sp.GetRequiredService<ConfigurationManager>().Load());
|
||||
_serviceCollection.AddTransient<RepoVerification>();
|
||||
|
||||
// commands
|
||||
_serviceCollection.AddScoped<PalConfigCommand>();
|
||||
_serviceCollection.AddScoped<ISubCommand, PalConfigCommand>();
|
||||
_serviceCollection.AddScoped<ISubCommand, PalNearCommand>();
|
||||
_serviceCollection.AddScoped<ISubCommand, PalStatsCommand>();
|
||||
_serviceCollection.AddScoped<ISubCommand, PalTestConnectionCommand>();
|
||||
|
||||
// territory & marker related services
|
||||
_serviceCollection.AddScoped<TerritoryState>();
|
||||
_serviceCollection.AddScoped<FrameworkService>();
|
||||
_serviceCollection.AddScoped<ChatService>();
|
||||
_serviceCollection.AddScoped<FloorService>();
|
||||
_serviceCollection.AddScoped<ImportService>();
|
||||
_serviceCollection.AddScoped<ObjectTableDebug>();
|
||||
|
||||
// windows & related services
|
||||
_serviceCollection.AddScoped<AgreementWindow>();
|
||||
_serviceCollection.AddScoped<ConfigWindow>();
|
||||
_serviceCollection.AddScoped<StatisticsService>();
|
||||
_serviceCollection.AddScoped<StatisticsWindow>();
|
||||
|
||||
// rendering
|
||||
_serviceCollection.AddScoped<SimpleRenderer>();
|
||||
_serviceCollection.AddScoped<SplatoonRenderer>();
|
||||
_serviceCollection.AddScoped<RenderAdapter>();
|
||||
|
||||
// queue handling
|
||||
_serviceCollection.AddTransient<IQueueOnFrameworkThread.Handler<QueuedImport>, QueuedImport.Handler>();
|
||||
_serviceCollection
|
||||
.AddTransient<IQueueOnFrameworkThread.Handler<QueuedUndoImport>, QueuedUndoImport.Handler>();
|
||||
_serviceCollection
|
||||
.AddTransient<IQueueOnFrameworkThread.Handler<QueuedConfigUpdate>, QueuedConfigUpdate.Handler>();
|
||||
_serviceCollection
|
||||
.AddTransient<IQueueOnFrameworkThread.Handler<QueuedSyncResponse>, QueuedSyncResponse.Handler>();
|
||||
|
||||
// build
|
||||
_serviceProvider = _serviceCollection.BuildServiceProvider(new ServiceProviderOptions
|
||||
{
|
||||
ValidateOnBuild = true,
|
||||
ValidateScopes = true,
|
||||
});
|
||||
|
||||
|
||||
#if RELEASE
|
||||
// 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
|
||||
// - you host your own server instance
|
||||
//
|
||||
// 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).
|
||||
_serviceProvider.GetService<RepoVerification>();
|
||||
#endif
|
||||
|
||||
// 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,
|
||||
// even if it's not.
|
||||
//
|
||||
// There's 2-3 seconds of slowdown primarily caused by the sqlite init, but that needs to happen for
|
||||
// config stuff.
|
||||
_logger = _serviceProvider.GetRequiredService<ILogger<DependencyInjectionContext>>();
|
||||
_logger.LogInformation("Service container built");
|
||||
|
||||
return _serviceProvider;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_logger.LogInformation("Disposing DI Context");
|
||||
_serviceProvider?.Dispose();
|
||||
|
||||
// ensure we're not keeping the file open longer than the plugin is loaded
|
||||
using (SqliteConnection sqliteConnection = new(_sqliteConnectionString))
|
||||
SqliteConnection.ClearPool(sqliteConnection);
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
using Dalamud.Game.Gui;
|
||||
using Pal.Client.Properties;
|
||||
|
||||
namespace Pal.Client.Extensions
|
||||
{
|
||||
public static class ChatExtensions
|
||||
{
|
||||
public static void PalError(this ChatGui chat, string e)
|
||||
=> chat.PrintError($"[{Localization.Palace_Pal}] {e}");
|
||||
}
|
||||
}
|
@ -1,13 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace Pal.Client.Extensions
|
||||
{
|
||||
public static class GuidExtensions
|
||||
{
|
||||
public static string ToPartialId(this Guid g, int length = 13)
|
||||
=> g.ToString().ToPartialId();
|
||||
namespace Pal.Client.Extensions;
|
||||
|
||||
public static string ToPartialId(this string s, int length = 13)
|
||||
=> s.PadRight(length + 1).Substring(0, length);
|
||||
}
|
||||
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)
|
||||
=> s.PadRight(length + 1).Substring(0, length);
|
||||
}
|
||||
|
35
Pal.Client/Extensions/PalImGui.cs
Normal file
35
Pal.Client/Extensions/PalImGui.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Pal.Client.Extensions;
|
||||
|
||||
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)
|
||||
{
|
||||
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);
|
||||
labelPtr[labelLength] = 0;
|
||||
|
||||
return ImGuiNative.igBeginTabItem(labelPtr, null, flags) != 0;
|
||||
}
|
||||
|
||||
public static void RadioButtonWrapped(string label, ref int choice, int value)
|
||||
{
|
||||
ImGui.BeginGroup();
|
||||
ImGui.RadioButton($"##radio{value}", value == choice);
|
||||
ImGui.SameLine();
|
||||
ImGui.TextWrapped(label);
|
||||
ImGui.EndGroup();
|
||||
if (ImGui.IsItemClicked())
|
||||
choice = value;
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
using System.Linq;
|
||||
using Dalamud.Interface.Windowing;
|
||||
|
||||
namespace Pal.Client.Extensions
|
||||
{
|
||||
internal static class WindowSystemExtensions
|
||||
{
|
||||
public static T? GetWindow<T>(this WindowSystem windowSystem)
|
||||
where T : Window
|
||||
{
|
||||
return windowSystem.Windows.OfType<T>().FirstOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
28
Pal.Client/Floors/EphemeralLocation.cs
Normal file
28
Pal.Client/Floors/EphemeralLocation.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
|
||||
namespace Pal.Client.Floors;
|
||||
|
||||
/// <summary>
|
||||
/// This is a currently-visible marker.
|
||||
/// </summary>
|
||||
internal sealed class EphemeralLocation : MemoryLocation
|
||||
{
|
||||
public override bool Equals(object? obj) => obj is EphemeralLocation && base.Equals(obj);
|
||||
|
||||
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)
|
||||
{
|
||||
return !Equals(a, b);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"EphemeralLocation(Position={Position}, Type={Type})";
|
||||
}
|
||||
}
|
162
Pal.Client/Floors/FloorService.cs
Normal file
162
Pal.Client/Floors/FloorService.cs
Normal file
@ -0,0 +1,162 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Pal.Client.Configuration;
|
||||
using Pal.Client.Database;
|
||||
using Pal.Client.Extensions;
|
||||
using Pal.Client.Floors.Tasks;
|
||||
using Pal.Client.Net;
|
||||
using Pal.Common;
|
||||
|
||||
namespace Pal.Client.Floors;
|
||||
|
||||
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)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_cleanup = cleanup;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_territories = Enum.GetValues<ETerritoryType>().ToDictionary(o => o, o => new MemoryTerritory(o));
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<EphemeralLocation> EphemeralLocations => _ephemeralLocations;
|
||||
public bool IsImportRunning { get; private set; }
|
||||
|
||||
public void ChangeTerritory(ushort territoryType)
|
||||
{
|
||||
_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);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public MemoryTerritory? GetTerritoryIfReady(ETerritoryType territoryType)
|
||||
{
|
||||
var territory = _territories[territoryType];
|
||||
if (territory.ReadyState != MemoryTerritory.EReadyState.Ready)
|
||||
return null;
|
||||
|
||||
return territory;
|
||||
}
|
||||
|
||||
public bool IsReady(ushort territoryId) => GetTerritoryIfReady(territoryId) != null;
|
||||
|
||||
public bool MergePersistentLocations(
|
||||
ETerritoryType territoryType,
|
||||
IReadOnlyList<PersistentLocation> visibleLocations,
|
||||
bool recreateLayout,
|
||||
out List<PersistentLocation> locationsToSync)
|
||||
{
|
||||
MemoryTerritory? territory = GetTerritoryIfReady(territoryType);
|
||||
locationsToSync = new();
|
||||
if (territory == null)
|
||||
return false;
|
||||
|
||||
var partialAccountId = _configuration.FindAccount(RemoteApi.RemoteUrl)?.AccountId.ToPartialId();
|
||||
var persistentLocations = territory.Locations.ToList();
|
||||
|
||||
List<PersistentLocation> markAsSeen = new();
|
||||
List<PersistentLocation> newLocations = new();
|
||||
foreach (var visibleLocation in visibleLocations)
|
||||
{
|
||||
PersistentLocation? existingLocation = persistentLocations.SingleOrDefault(x => x == visibleLocation);
|
||||
if (existingLocation != null)
|
||||
{
|
||||
if (existingLocation is { Seen: false, LocalId: { } })
|
||||
{
|
||||
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);
|
||||
newLocations.Add(visibleLocation);
|
||||
recreateLayout = true;
|
||||
}
|
||||
|
||||
if (markAsSeen.Count > 0)
|
||||
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)
|
||||
{
|
||||
lock (memoryTerritory.LockObj)
|
||||
memoryTerritory.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetToImportState()
|
||||
{
|
||||
IsImportRunning = true;
|
||||
foreach (var memoryTerritory in _territories.Values)
|
||||
{
|
||||
lock (memoryTerritory.LockObj)
|
||||
memoryTerritory.ReadyState = MemoryTerritory.EReadyState.Importing;
|
||||
}
|
||||
}
|
||||
}
|
464
Pal.Client/Floors/FrameworkService.cs
Normal file
464
Pal.Client/Floors/FrameworkService.cs
Normal file
@ -0,0 +1,464 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Pal.Client.Configuration;
|
||||
using Pal.Client.Database;
|
||||
using Pal.Client.DependencyInjection;
|
||||
using Pal.Client.Net;
|
||||
using Pal.Client.Rendering;
|
||||
using Pal.Client.Scheduled;
|
||||
using Pal.Common;
|
||||
|
||||
namespace Pal.Client.Floors;
|
||||
|
||||
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)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_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;
|
||||
_configurationManager.Saved += OnSaved;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_framework.Update -= OnUpdate;
|
||||
_configurationManager.Saved -= OnSaved;
|
||||
}
|
||||
|
||||
private void OnSaved(object? sender, IPalacePalConfiguration? config)
|
||||
=> EarlyEventQueue.Enqueue(new QueuedConfigUpdate());
|
||||
|
||||
private void OnUpdate(IFramework framework)
|
||||
{
|
||||
if (_configuration.FirstUse)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
bool recreateLayout = false;
|
||||
|
||||
while (EarlyEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued))
|
||||
HandleQueued(queued, ref recreateLayout);
|
||||
|
||||
if (_territoryState.LastTerritory != _clientState.TerritoryType)
|
||||
{
|
||||
MemoryTerritory? oldTerritory = _floorService.GetTerritoryIfReady(_territoryState.LastTerritory);
|
||||
if (oldTerritory != 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)
|
||||
{
|
||||
bool isEnabled = DetermineVisibility(location, visibleLocations);
|
||||
if (location.RenderElement == null)
|
||||
{
|
||||
if (isEnabled)
|
||||
return true;
|
||||
else
|
||||
continue;
|
||||
}
|
||||
|
||||
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, DetermineVisibility(location, visibleMarkers),
|
||||
_configuration.DeepDungeons.Traps);
|
||||
}
|
||||
else if (location.Type == MemoryLocation.EType.Hoard)
|
||||
{
|
||||
CreateRenderElement(location, elements, DetermineVisibility(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, true, _configuration.DeepDungeons.SilverCoffers);
|
||||
}
|
||||
else if (location.Type == MemoryLocation.EType.GoldCoffer &&
|
||||
_configuration.DeepDungeons.GoldCoffers.Show)
|
||||
{
|
||||
CreateRenderElement(location, elements, true, _configuration.DeepDungeons.GoldCoffers);
|
||||
}
|
||||
}
|
||||
|
||||
if (elements.Count == 0)
|
||||
return;
|
||||
|
||||
_renderAdapter.SetLayer(ELayer.RegularCoffers, elements);
|
||||
}
|
||||
|
||||
private bool DetermineVisibility(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 true;
|
||||
case MemoryLocation.EType.Hoard
|
||||
when _territoryState.PomanderOfIntuition == PomanderState.Inactive ||
|
||||
!_configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander ||
|
||||
visibleLocations.Any(x => x == location):
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateRenderElement(MemoryLocation location, List<IRenderElement> elements, bool enabled,
|
||||
MarkerConfiguration config)
|
||||
{
|
||||
if (!config.Show)
|
||||
{
|
||||
location.RenderElement = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var element =
|
||||
_renderAdapter.CreateElement(location.Type, location.Position, enabled, config.Color, config.Fill);
|
||||
location.RenderElement = element;
|
||||
elements.Add(element);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Up-/Download
|
||||
|
||||
private async Task DownloadLocationsForTerritory(ushort territoryId)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Downloading territory {Territory} from server", (ETerritoryType)territoryId);
|
||||
var (success, downloadedMarkers) = await _remoteApi.DownloadRemoteMarkers(territoryId);
|
||||
LateEventQueue.Enqueue(new QueuedSyncResponse
|
||||
{
|
||||
Type = SyncType.Download,
|
||||
TerritoryType = territoryId,
|
||||
Success = success,
|
||||
Locations = downloadedMarkers
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_debugState.SetFromException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UploadLocationsForTerritory(ushort territoryId, List<PersistentLocation> locationsToUpload)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Uploading {Count} locations for territory {Territory} to server",
|
||||
locationsToUpload.Count, (ETerritoryType)territoryId);
|
||||
var (success, uploadedLocations) = await _remoteApi.UploadLocations(territoryId, locationsToUpload);
|
||||
LateEventQueue.Enqueue(new QueuedSyncResponse
|
||||
{
|
||||
Type = SyncType.Upload,
|
||||
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
|
||||
{
|
||||
Type = MemoryLocation.EType.Trap,
|
||||
Position = obj.Position,
|
||||
Seen = true,
|
||||
Source = ClientLocation.ESource.SeenLocally,
|
||||
});
|
||||
break;
|
||||
|
||||
case 2007542:
|
||||
case 2007543:
|
||||
persistentLocations.Add(new PersistentLocation
|
||||
{
|
||||
Type = MemoryLocation.EType.Hoard,
|
||||
Position = obj.Position,
|
||||
Seen = true,
|
||||
Source = ClientLocation.ESource.SeenLocally,
|
||||
});
|
||||
break;
|
||||
|
||||
case 2007357:
|
||||
ephemeralLocations.Add(new EphemeralLocation
|
||||
{
|
||||
Type = MemoryLocation.EType.SilverCoffer,
|
||||
Position = obj.Position,
|
||||
Seen = true,
|
||||
});
|
||||
break;
|
||||
|
||||
case 2007358:
|
||||
ephemeralLocations.Add(new EphemeralLocation
|
||||
{
|
||||
Type = MemoryLocation.EType.GoldCoffer,
|
||||
Position = obj.Position,
|
||||
Seen = true
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
while (NextUpdateObjects.TryDequeue(out nint address))
|
||||
{
|
||||
var obj = _objectTable.FirstOrDefault(x => x.Address == address);
|
||||
if (obj != null && obj.Position.Length() > 0.1)
|
||||
{
|
||||
persistentLocations.Add(new PersistentLocation
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
66
Pal.Client/Floors/MemoryLocation.cs
Normal file
66
Pal.Client/Floors/MemoryLocation.cs
Normal file
@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Pal.Client.Rendering;
|
||||
using Pal.Common;
|
||||
using Palace;
|
||||
|
||||
namespace Pal.Client.Floors;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for <see cref="MemoryLocation"/> and <see cref="EphemeralLocation"/>.
|
||||
/// </summary>
|
||||
internal abstract class MemoryLocation
|
||||
{
|
||||
public required EType Type { get; init; }
|
||||
public required Vector3 Position { get; init; }
|
||||
public bool Seen { get; set; }
|
||||
|
||||
public IRenderElement? RenderElement { get; set; }
|
||||
|
||||
public enum EType
|
||||
{
|
||||
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 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)
|
||||
};
|
||||
}
|
||||
}
|
62
Pal.Client/Floors/MemoryTerritory.cs
Normal file
62
Pal.Client/Floors/MemoryTerritory.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Pal.Client.Configuration;
|
||||
using Pal.Client.Scheduled;
|
||||
using Pal.Common;
|
||||
|
||||
namespace Pal.Client.Floors;
|
||||
|
||||
/// <summary>
|
||||
/// A single set of floors loaded entirely in memory, can be e.g. POTD 51-60.
|
||||
/// </summary>
|
||||
internal sealed class MemoryTerritory
|
||||
{
|
||||
public MemoryTerritory(ETerritoryType territoryType)
|
||||
{
|
||||
TerritoryType = territoryType;
|
||||
}
|
||||
|
||||
public ETerritoryType TerritoryType { get; }
|
||||
public EReadyState ReadyState { get; set; } = EReadyState.NotLoaded;
|
||||
public ESyncState SyncState { get; set; } = ESyncState.NotAttempted;
|
||||
|
||||
public ConcurrentBag<PersistentLocation> Locations { get; } = new();
|
||||
public object LockObj { get; } = new();
|
||||
|
||||
public void Initialize(IEnumerable<PersistentLocation> locations)
|
||||
{
|
||||
Locations.Clear();
|
||||
foreach (var location in locations)
|
||||
Locations.Add(location);
|
||||
|
||||
ReadyState = EReadyState.Ready;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Locations.Clear();
|
||||
SyncState = ESyncState.NotAttempted;
|
||||
ReadyState = EReadyState.NotLoaded;
|
||||
}
|
||||
|
||||
public enum EReadyState
|
||||
{
|
||||
NotLoaded,
|
||||
|
||||
/// <summary>
|
||||
/// Currently loading from the database.
|
||||
/// </summary>
|
||||
Loading,
|
||||
|
||||
/// <summary>
|
||||
/// Locations loaded, no import running.
|
||||
/// </summary>
|
||||
Ready,
|
||||
|
||||
/// <summary>
|
||||
/// Import running, should probably not interact with this too much.
|
||||
/// </summary>
|
||||
Importing,
|
||||
}
|
||||
}
|
99
Pal.Client/Floors/ObjectTableDebug.cs
Normal file
99
Pal.Client/Floors/ObjectTableDebug.cs
Normal file
@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Pal.Client.Floors;
|
||||
|
||||
/// <summary>
|
||||
/// This isn't very useful for running deep dungeons normally, but it is for plugin dev.
|
||||
///
|
||||
/// Needs the corresponding beta feature to be enabled.
|
||||
/// </summary>
|
||||
internal sealed class ObjectTableDebug : IDisposable
|
||||
{
|
||||
public const string FeatureName = nameof(ObjectTableDebug);
|
||||
|
||||
private readonly IDalamudPluginInterface _pluginInterface;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly IGameGui _gameGui;
|
||||
private readonly IClientState _clientState;
|
||||
|
||||
public ObjectTableDebug(IDalamudPluginInterface pluginInterface, IObjectTable objectTable, IGameGui gameGui,
|
||||
IClientState clientState)
|
||||
{
|
||||
_pluginInterface = pluginInterface;
|
||||
_objectTable = objectTable;
|
||||
_gameGui = gameGui;
|
||||
_clientState = clientState;
|
||||
|
||||
_pluginInterface.UiBuilder.Draw += Draw;
|
||||
}
|
||||
|
||||
private void Draw()
|
||||
{
|
||||
int index = 0;
|
||||
foreach (IGameObject obj in _objectTable)
|
||||
{
|
||||
if (obj is IEventObj eventObj && string.IsNullOrEmpty(eventObj.Name.ToString()))
|
||||
{
|
||||
++index;
|
||||
int model = Marshal.ReadInt32(obj.Address + 128);
|
||||
|
||||
if (_gameGui.WorldToScreen(obj.Position, out var screenCoords))
|
||||
{
|
||||
// So, while WorldToScreen will return false if the point is off of game client screen, to
|
||||
// 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}";
|
||||
|
||||
var screenPos = ImGui.GetMainViewport().Pos;
|
||||
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;
|
||||
|
||||
if (screenCoords.X + windowSize.X > screenPos.X + screenSize.X ||
|
||||
screenCoords.Y + windowSize.Y > screenPos.Y + screenSize.Y)
|
||||
continue;
|
||||
|
||||
if (distance > 50f)
|
||||
continue;
|
||||
|
||||
ImGui.SetNextWindowPos(new Vector2(screenCoords.X, screenCoords.Y));
|
||||
|
||||
ImGui.SetNextWindowBgAlpha(Math.Max(1f - (distance / 50f), 0.2f));
|
||||
if (ImGui.Begin(
|
||||
$"PalacePal_{nameof(ObjectTableDebug)}_{index}",
|
||||
ImGuiWindowFlags.NoDecoration |
|
||||
ImGuiWindowFlags.AlwaysAutoResize |
|
||||
ImGuiWindowFlags.NoSavedSettings |
|
||||
ImGuiWindowFlags.NoMove |
|
||||
ImGuiWindowFlags.NoMouseInputs |
|
||||
ImGuiWindowFlags.NoDocking |
|
||||
ImGuiWindowFlags.NoFocusOnAppearing |
|
||||
ImGuiWindowFlags.NoNav))
|
||||
ImGui.Text(objectText);
|
||||
ImGui.End();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private float DistanceToPlayer(Vector3 center)
|
||||
=> Vector3.Distance(_clientState.LocalPlayer?.Position ?? Vector3.Zero, center);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_pluginInterface.UiBuilder.Draw -= Draw;
|
||||
}
|
||||
}
|
54
Pal.Client/Floors/PersistentLocation.cs
Normal file
54
Pal.Client/Floors/PersistentLocation.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Pal.Client.Database;
|
||||
|
||||
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>
|
||||
/// Network id for the server you're currently connected to.
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
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})";
|
||||
}
|
||||
}
|
46
Pal.Client/Floors/Tasks/DbTask.cs
Normal file
46
Pal.Client/Floors/Tasks/DbTask.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Pal.Client.Database;
|
||||
|
||||
namespace Pal.Client.Floors.Tasks;
|
||||
|
||||
internal abstract class DbTask<T>
|
||||
where T : DbTask<T>
|
||||
{
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
protected DbTask(IServiceScopeFactory serviceScopeFactory)
|
||||
{
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
ILogger<T> logger = scope.ServiceProvider.GetRequiredService<ILogger<T>>();
|
||||
try
|
||||
{
|
||||
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
|
||||
|
||||
Run(dbContext, logger);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Failed to run DbTask");
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// nothing we can do here but catch it, if we don't we crash the game
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract void Run(PalClientContext dbContext, ILogger<T> logger);
|
||||
}
|
78
Pal.Client/Floors/Tasks/LoadTerritory.cs
Normal file
78
Pal.Client/Floors/Tasks/LoadTerritory.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Pal.Client.Database;
|
||||
|
||||
namespace Pal.Client.Floors.Tasks;
|
||||
|
||||
internal sealed class LoadTerritory : DbTask<LoadTerritory>
|
||||
{
|
||||
private readonly Cleanup _cleanup;
|
||||
private readonly MemoryTerritory _territory;
|
||||
|
||||
public LoadTerritory(IServiceScopeFactory serviceScopeFactory,
|
||||
Cleanup cleanup,
|
||||
MemoryTerritory territory)
|
||||
: base(serviceScopeFactory)
|
||||
{
|
||||
_cleanup = cleanup;
|
||||
_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);
|
||||
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)
|
||||
{
|
||||
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)
|
||||
};
|
||||
}
|
||||
}
|
37
Pal.Client/Floors/Tasks/MarkLocalSeen.cs
Normal file
37
Pal.Client/Floors/Tasks/MarkLocalSeen.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Pal.Client.Database;
|
||||
|
||||
namespace Pal.Client.Floors.Tasks;
|
||||
|
||||
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)
|
||||
{
|
||||
_territory = territory;
|
||||
_locations = locations;
|
||||
}
|
||||
|
||||
protected override void Run(PalClientContext dbContext, ILogger<MarkLocalSeen> logger)
|
||||
{
|
||||
lock (_territory.LockObj)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
50
Pal.Client/Floors/Tasks/MarkRemoteSeen.cs
Normal file
50
Pal.Client/Floors/Tasks/MarkRemoteSeen.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Pal.Client.Database;
|
||||
|
||||
namespace Pal.Client.Floors.Tasks;
|
||||
|
||||
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)
|
||||
{
|
||||
_territory = territory;
|
||||
_locations = locations;
|
||||
_accountId = accountId;
|
||||
}
|
||||
|
||||
protected override void Run(PalClientContext dbContext, ILogger<MarkRemoteSeen> logger)
|
||||
{
|
||||
lock (_territory.LockObj)
|
||||
{
|
||||
logger.LogInformation("Marking {Count} locations as seen remotely on {Account} in territory {Territory}",
|
||||
_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();
|
||||
}
|
||||
}
|
||||
}
|
76
Pal.Client/Floors/Tasks/SaveNewLocations.cs
Normal file
76
Pal.Client/Floors/Tasks/SaveNewLocations.cs
Normal file
@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Pal.Client.Database;
|
||||
using Pal.Common;
|
||||
|
||||
namespace Pal.Client.Floors.Tasks;
|
||||
|
||||
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)
|
||||
{
|
||||
_territory = territory;
|
||||
_newLocations = newLocations;
|
||||
}
|
||||
|
||||
protected override void Run(PalClientContext dbContext, ILogger<SaveNewLocations> logger)
|
||||
{
|
||||
Run(_territory, dbContext, logger, _newLocations);
|
||||
}
|
||||
|
||||
public static void Run<T>(
|
||||
MemoryTerritory territory,
|
||||
PalClientContext dbContext,
|
||||
ILogger<T> logger,
|
||||
List<PersistentLocation> locations)
|
||||
{
|
||||
lock (territory.LockObj)
|
||||
{
|
||||
logger.LogInformation("Saving {Count} new locations for territory {Territory}", locations.Count,
|
||||
territory.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)
|
||||
{
|
||||
return new ClientLocation
|
||||
{
|
||||
TerritoryType = (ushort)territoryType,
|
||||
Type = ToDatabaseType(location.Type),
|
||||
X = location.Position.X,
|
||||
Y = location.Position.Y,
|
||||
Z = location.Position.Z,
|
||||
Seen = location.Seen,
|
||||
Source = location.Source,
|
||||
SinceVersion = typeof(Plugin).Assembly.GetName().Version!.ToString(2),
|
||||
};
|
||||
}
|
||||
|
||||
private static ClientLocation.EType ToDatabaseType(MemoryLocation.EType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
MemoryLocation.EType.Trap => ClientLocation.EType.Trap,
|
||||
MemoryLocation.EType.Hoard => ClientLocation.EType.Hoard,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
|
||||
};
|
||||
}
|
||||
}
|
34
Pal.Client/Floors/TerritoryState.cs
Normal file
34
Pal.Client/Floors/TerritoryState.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Pal.Common;
|
||||
|
||||
namespace Pal.Client.Floors;
|
||||
|
||||
public sealed class TerritoryState
|
||||
{
|
||||
private readonly IClientState _clientState;
|
||||
private readonly ICondition _condition;
|
||||
|
||||
public TerritoryState(IClientState clientState, ICondition 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
|
||||
{
|
||||
Inactive,
|
||||
Active,
|
||||
FoundOnCurrentFloor,
|
||||
PomanderOfSafetyUsed,
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Logging;
|
||||
using Dalamud.Memory;
|
||||
using Dalamud.Utility.Signatures;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Pal.Client
|
||||
{
|
||||
internal unsafe class Hooks
|
||||
{
|
||||
#pragma warning disable CS0649
|
||||
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))]
|
||||
private Hook<ActorVfxCreateDelegate> ActorVfxCreateHook { get; init; } = null!;
|
||||
#pragma warning restore CS0649
|
||||
|
||||
public Hooks()
|
||||
{
|
||||
SignatureHelper.Initialise(this);
|
||||
ActorVfxCreateHook.Enable();
|
||||
}
|
||||
|
||||
/// <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 (Service.Plugin.IsInDeepDungeon())
|
||||
{
|
||||
var vfxPath = MemoryHelper.ReadString(new nint(a1), Encoding.ASCII, 256);
|
||||
var obj = Service.ObjectTable.CreateObjectReference(a2);
|
||||
|
||||
/*
|
||||
if (Service.Configuration.BetaKey == "VFX")
|
||||
Service.Chat.Print($"{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")
|
||||
{
|
||||
Service.Plugin.NextUpdateObjects.Enqueue(obj.Address);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PluginLog.Error(e, "VFX Create Hook failed");
|
||||
}
|
||||
return ActorVfxCreateHook.Original(a1, a2, a3, a4, a5, a6, a7);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ActorVfxCreateHook?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
@ -4,10 +4,9 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Pal.Client
|
||||
namespace Pal.Client;
|
||||
|
||||
internal interface ILanguageChanged
|
||||
{
|
||||
internal interface ILanguageChanged
|
||||
{
|
||||
void LanguageChanged();
|
||||
}
|
||||
void LanguageChanged();
|
||||
}
|
||||
|
@ -1,155 +0,0 @@
|
||||
using Pal.Common;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Pal.Client.Extensions;
|
||||
|
||||
namespace Pal.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON for a single floor set (e.g. 51-60).
|
||||
/// </summary>
|
||||
internal class LocalState
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonSerializerOptions = new() { IncludeFields = true };
|
||||
private const int CurrentVersion = 4;
|
||||
|
||||
public uint TerritoryType { get; set; }
|
||||
public ConcurrentBag<Marker> Markers { get; set; } = new();
|
||||
|
||||
public LocalState(uint territoryType)
|
||||
{
|
||||
TerritoryType = territoryType;
|
||||
}
|
||||
|
||||
private void ApplyFilters()
|
||||
{
|
||||
if (Service.Configuration.Mode == Configuration.EMode.Offline)
|
||||
Markers = new ConcurrentBag<Marker>(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<Marker>(Markers.Where(x => x.Seen || !x.WasImported || x.Imports.Count > 0));
|
||||
}
|
||||
|
||||
public static LocalState? Load(uint territoryType)
|
||||
{
|
||||
string path = GetSaveLocation(territoryType);
|
||||
if (!File.Exists(path))
|
||||
return null;
|
||||
|
||||
string content = File.ReadAllText(path);
|
||||
if (content.Length == 0)
|
||||
return null;
|
||||
|
||||
LocalState localState;
|
||||
int version = 1;
|
||||
if (content[0] == '[')
|
||||
{
|
||||
// v1 only had a list of markers, not a JSON object as root
|
||||
localState = new LocalState(territoryType)
|
||||
{
|
||||
Markers = new ConcurrentBag<Marker>(JsonSerializer.Deserialize<HashSet<Marker>>(content, JsonSerializerOptions) ?? new()),
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
var save = JsonSerializer.Deserialize<SaveFile>(content, JsonSerializerOptions);
|
||||
if (save == null)
|
||||
return null;
|
||||
|
||||
localState = new LocalState(territoryType)
|
||||
{
|
||||
Markers = new ConcurrentBag<Marker>(save.Markers.Where(o => o.Type == Marker.EType.Trap || o.Type == Marker.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()
|
||||
{
|
||||
string path = GetSaveLocation(TerritoryType);
|
||||
|
||||
ApplyFilters();
|
||||
SaveImpl(path);
|
||||
}
|
||||
|
||||
public void Backup(string suffix)
|
||||
{
|
||||
string path = $"{GetSaveLocation(TerritoryType)}.{suffix}";
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
SaveImpl(path);
|
||||
}
|
||||
}
|
||||
|
||||
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<Marker>(Markers)
|
||||
}, JsonSerializerOptions));
|
||||
}
|
||||
}
|
||||
|
||||
public string GetSaveLocation() => GetSaveLocation(TerritoryType);
|
||||
|
||||
private static string GetSaveLocation(uint territoryType) => Path.Join(Service.PluginInterface.GetPluginConfigDirectory(), $"{territoryType}.json");
|
||||
|
||||
public static void ForEach(Action<LocalState> action)
|
||||
{
|
||||
foreach (ETerritoryType territory in typeof(ETerritoryType).GetEnumValues())
|
||||
{
|
||||
LocalState? 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 class SaveFile
|
||||
{
|
||||
public int Version { get; set; }
|
||||
public HashSet<Marker> Markers { get; set; } = new();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
using ECommons.SplatoonAPI;
|
||||
using Pal.Client.Rendering;
|
||||
using Pal.Common;
|
||||
using Palace;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Pal.Client
|
||||
{
|
||||
internal class Marker
|
||||
{
|
||||
public EType Type { get; set; } = EType.Unknown;
|
||||
public Vector3 Position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether we have encountered the trap/coffer at this location in-game.
|
||||
/// </summary>
|
||||
public bool Seen { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Network id for the server you're currently connected to.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
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>
|
||||
[JsonIgnore]
|
||||
public bool UploadRequested { 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<string> RemoteSeenOn { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Whether this marker was requested to be seen, to avoid duplicate requests.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool RemoteSeenRequested { get; set; }
|
||||
|
||||
/// <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<Guid> Imports { get; set; } = new();
|
||||
|
||||
public bool WasImported { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// To make rollbacks of local data easier, keep track of the version which was used to write the marker initially.
|
||||
/// </summary>
|
||||
public string? SinceVersion { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public IRenderElement? RenderElement { get; set; }
|
||||
|
||||
public Marker(EType type, Vector3 position, Guid? networkId = null)
|
||||
{
|
||||
Type = type;
|
||||
Position = position;
|
||||
NetworkId = networkId;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Type, PalaceMath.GetHashCode(Position));
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is Marker otherMarker && Type == otherMarker.Type && PalaceMath.IsNearlySamePosition(Position, otherMarker.Position);
|
||||
}
|
||||
|
||||
public static bool operator ==(Marker? a, object? b)
|
||||
{
|
||||
return Equals(a, b);
|
||||
}
|
||||
|
||||
public static bool operator !=(Marker? a, object? b)
|
||||
{
|
||||
return !Equals(a, b);
|
||||
}
|
||||
|
||||
|
||||
public bool IsPermanent() => Type == EType.Trap || Type == EType.Hoard;
|
||||
|
||||
public enum EType
|
||||
{
|
||||
Unknown = ObjectType.Unknown,
|
||||
|
||||
#region Permanent Markers
|
||||
Trap = ObjectType.Trap,
|
||||
Hoard = ObjectType.Hoard,
|
||||
|
||||
[Obsolete]
|
||||
Debug = 3,
|
||||
#endregion
|
||||
|
||||
# region Markers that only show up if they're currently visible
|
||||
SilverCoffer = 100,
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
using Dalamud.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Pal.Client.Net
|
||||
{
|
||||
internal class GrpcLogger : ILogger
|
||||
{
|
||||
private readonly string _name;
|
||||
|
||||
public GrpcLogger(string name)
|
||||
{
|
||||
_name = name;
|
||||
}
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state)
|
||||
where TState : notnull
|
||||
=> NullScope.Instance;
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)] // PluginLog detects the plugin name as `Microsoft.Extensions.Logging` if inlined
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
if (!IsEnabled(logLevel))
|
||||
return;
|
||||
|
||||
if (formatter == null)
|
||||
throw new ArgumentNullException(nameof(formatter));
|
||||
|
||||
string message = $"gRPC[{_name}] {formatter(state, null)}";
|
||||
if (string.IsNullOrEmpty(message))
|
||||
return;
|
||||
|
||||
#pragma warning disable CS8604 // the nullability on PluginLog methods is wrong and allows nulls for exceptions, WriteLog even declares the parameter as `Exception? exception = null`
|
||||
switch (logLevel)
|
||||
{
|
||||
case LogLevel.Critical:
|
||||
PluginLog.Fatal(exception, message);
|
||||
break;
|
||||
|
||||
case LogLevel.Error:
|
||||
PluginLog.Error(exception, message);
|
||||
break;
|
||||
|
||||
case LogLevel.Warning:
|
||||
PluginLog.Warning(exception, message);
|
||||
break;
|
||||
|
||||
case LogLevel.Information:
|
||||
PluginLog.Information(exception, message);
|
||||
break;
|
||||
|
||||
case LogLevel.Debug:
|
||||
PluginLog.Debug(exception, message);
|
||||
break;
|
||||
|
||||
case LogLevel.Trace:
|
||||
PluginLog.Verbose(exception, message);
|
||||
break;
|
||||
}
|
||||
#pragma warning restore CS8604
|
||||
}
|
||||
|
||||
private class NullScope : IDisposable
|
||||
{
|
||||
public static NullScope Instance { get; } = new();
|
||||
|
||||
private NullScope()
|
||||
{
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
|
||||
namespace Pal.Client.Net
|
||||
{
|
||||
internal class GrpcLoggerProvider : ILoggerProvider
|
||||
{
|
||||
public ILogger CreateLogger(string categoryName) => new GrpcLogger(categoryName);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -1,95 +1,79 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Pal.Client.Net
|
||||
namespace Pal.Client.Net;
|
||||
|
||||
internal sealed class JwtClaims
|
||||
{
|
||||
internal 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")]
|
||||
public Guid NameId { get; set; }
|
||||
if (string.IsNullOrEmpty(authToken))
|
||||
throw new ArgumentException("Server sent no auth token", nameof(authToken));
|
||||
|
||||
[JsonPropertyName("role")]
|
||||
[JsonConverter(typeof(JwtRoleConverter))]
|
||||
public List<string> Roles { get; set; } = new();
|
||||
string[] parts = authToken.Split('.');
|
||||
if (parts.Length != 3)
|
||||
throw new ArgumentException("Unsupported token type", nameof(authToken));
|
||||
|
||||
[JsonPropertyName("nbf")]
|
||||
[JsonConverter(typeof(JwtDateConverter))]
|
||||
public DateTimeOffset NotBefore { get; set; }
|
||||
// fix padding manually
|
||||
string payload = parts[1].Replace(",", "=").Replace("-", "+").Replace("/", "_");
|
||||
if (payload.Length % 4 == 2)
|
||||
payload += "==";
|
||||
else if (payload.Length % 4 == 3)
|
||||
payload += "=";
|
||||
|
||||
[JsonPropertyName("exp")]
|
||||
[JsonConverter(typeof(JwtDateConverter))]
|
||||
public DateTimeOffset ExpiresAt { get; set; }
|
||||
|
||||
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 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 class JwtDateConverter : JsonConverter<DateTimeOffset>
|
||||
{
|
||||
static readonly DateTimeOffset Zero = new DateTimeOffset(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();
|
||||
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();
|
||||
}
|
||||
|
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();
|
||||
}
|
@ -1,174 +1,248 @@
|
||||
using Account;
|
||||
using Dalamud.Logging;
|
||||
using Grpc.Core;
|
||||
using Grpc.Net.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Account;
|
||||
using Grpc.Core;
|
||||
using Grpc.Net.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Pal.Client.Configuration;
|
||||
using Pal.Client.Extensions;
|
||||
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 async Task<(bool Success, string Error)> TryConnect(CancellationToken cancellationToken, ILoggerFactory? loggerFactory = null, bool retry = true)
|
||||
using IDisposable? logScope = _logger.BeginScope("TryConnect");
|
||||
|
||||
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(
|
||||
CancellationToken cancellationToken,
|
||||
ILoggerFactory? loggerFactory)
|
||||
{
|
||||
if (_configuration.Mode != EMode.Online)
|
||||
{
|
||||
if (Service.Configuration.Mode != Configuration.EMode.Online)
|
||||
{
|
||||
PluginLog.Debug("TryConnect: Not Online, not attempting to establish a connection");
|
||||
return (false, Localization.ConnectionError_NotOnline);
|
||||
}
|
||||
_logger.LogDebug("Not Online, not attempting to establish a connection");
|
||||
return (false, Localization.ConnectionError_NotOnline, false);
|
||||
}
|
||||
|
||||
if (_channel == null || !(_channel.State == ConnectivityState.Ready || _channel.State == ConnectivityState.Idle))
|
||||
{
|
||||
Dispose();
|
||||
if (_channel == null ||
|
||||
!(_channel.State == ConnectivityState.Ready || _channel.State == ConnectivityState.Idle))
|
||||
{
|
||||
Dispose();
|
||||
|
||||
PluginLog.Information("TryConnect: Creating new gRPC channel");
|
||||
_channel = GrpcChannel.ForAddress(RemoteUrl, new GrpcChannelOptions
|
||||
_logger.LogInformation("Creating new gRPC channel");
|
||||
_channel = GrpcChannel.ForAddress(RemoteUrl, new GrpcChannelOptions
|
||||
{
|
||||
HttpHandler = new SocketsHttpHandler
|
||||
{
|
||||
HttpHandler = new SocketsHttpHandler
|
||||
{
|
||||
ConnectTimeout = TimeSpan.FromSeconds(5),
|
||||
SslOptions = GetSslClientAuthenticationOptions(),
|
||||
},
|
||||
LoggerFactory = loggerFactory,
|
||||
});
|
||||
ConnectTimeout = TimeSpan.FromSeconds(5),
|
||||
SslOptions = GetSslClientAuthenticationOptions(),
|
||||
},
|
||||
LoggerFactory = loggerFactory,
|
||||
});
|
||||
|
||||
PluginLog.Information($"TryConnect: Connecting to upstream service at {RemoteUrl}");
|
||||
await _channel.ConnectAsync(cancellationToken);
|
||||
}
|
||||
_logger.LogInformation("Connecting to upstream service at {Url}", RemoteUrl);
|
||||
await _channel.ConnectAsync(cancellationToken);
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
_logger.LogTrace("Acquiring connect lock");
|
||||
await _connectLock.WaitAsync(cancellationToken);
|
||||
_logger.LogTrace("Obtained connect lock");
|
||||
|
||||
try
|
||||
{
|
||||
var accountClient = new AccountService.AccountServiceClient(_channel);
|
||||
if (AccountId == null)
|
||||
IAccountConfiguration? configuredAccount = _configuration.FindAccount(RemoteUrl);
|
||||
if (configuredAccount == null)
|
||||
{
|
||||
PluginLog.Information($"TryConnect: No account information saved for {RemoteUrl}, creating new account");
|
||||
var createAccountReply = await accountClient.CreateAccountAsync(new CreateAccountRequest(), headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken);
|
||||
_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)
|
||||
{
|
||||
Account = new Configuration.AccountInfo
|
||||
{
|
||||
Id = Guid.Parse(createAccountReply.AccountId),
|
||||
};
|
||||
PluginLog.Information($"TryConnect: Account created with id {FormattedPartialAccountId}");
|
||||
if (!Guid.TryParse(createAccountReply.AccountId, out Guid accountId))
|
||||
throw new InvalidOperationException("invalid account id returned");
|
||||
|
||||
Service.Configuration.Save();
|
||||
configuredAccount = _configuration.CreateAccount(RemoteUrl, accountId);
|
||||
_logger.LogInformation("Account created with id {AccountId}", accountId.ToPartialId());
|
||||
|
||||
_configurationManager.Save(_configuration);
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginLog.Error($"TryConnect: Account creation failed with error {createAccountReply.Error}");
|
||||
_logger.LogError("Account creation failed with error {Error}", createAccountReply.Error);
|
||||
if (createAccountReply.Error == CreateAccountError.UpgradeRequired && !_warnedAboutUpgrade)
|
||||
{
|
||||
Service.Chat.PalError(Localization.ConnectionError_OldVersion);
|
||||
_chat.Error(Localization.ConnectionError_OldVersion);
|
||||
_warnedAboutUpgrade = true;
|
||||
}
|
||||
return (false, string.Format(Localization.ConnectionError_CreateAccountFailed, createAccountReply.Error));
|
||||
|
||||
return (false,
|
||||
string.Format(Localization.ConnectionError_CreateAccountFailed, createAccountReply.Error),
|
||||
false);
|
||||
}
|
||||
}
|
||||
|
||||
if (AccountId == null)
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
||||
if (configuredAccount == null)
|
||||
{
|
||||
PluginLog.Warning("TryConnect: No account id to login with");
|
||||
return (false, Localization.ConnectionError_CreateAccountReturnedNoId);
|
||||
_logger.LogWarning("No account to login with");
|
||||
return (false, Localization.ConnectionError_CreateAccountReturnedNoId, false);
|
||||
}
|
||||
|
||||
if (!_loginInfo.IsValid)
|
||||
{
|
||||
PluginLog.Information($"TryConnect: Logging in with account id {FormattedPartialAccountId}");
|
||||
LoginReply loginReply = await accountClient.LoginAsync(new LoginRequest { AccountId = AccountId?.ToString() }, headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken);
|
||||
_logger.LogInformation("Logging in with account id {AccountId}",
|
||||
configuredAccount.AccountId.ToPartialId());
|
||||
LoginReply loginReply = await accountClient.LoginAsync(
|
||||
new LoginRequest
|
||||
{
|
||||
AccountId = configuredAccount.AccountId.ToString(),
|
||||
Version = new()
|
||||
{
|
||||
Major = PluginVersion.Major,
|
||||
Minor = PluginVersion.Minor,
|
||||
},
|
||||
},
|
||||
headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10),
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
if (loginReply.Success)
|
||||
{
|
||||
PluginLog.Information($"TryConnect: Login successful with account id: {FormattedPartialAccountId}");
|
||||
_logger.LogInformation("Login successful with account id: {AccountId}",
|
||||
configuredAccount.AccountId.ToPartialId());
|
||||
_loginInfo = new LoginInfo(loginReply.AuthToken);
|
||||
|
||||
var account = Account;
|
||||
if (account != null)
|
||||
bool save = configuredAccount.EncryptIfNeeded();
|
||||
|
||||
List<string> newRoles = _loginInfo.Claims?.Roles.ToList() ?? new();
|
||||
if (!newRoles.SequenceEqual(configuredAccount.CachedRoles))
|
||||
{
|
||||
account.CachedRoles = _loginInfo.Claims?.Roles.ToList() ?? new List<string>();
|
||||
Service.Configuration.Save();
|
||||
configuredAccount.CachedRoles = newRoles;
|
||||
save = true;
|
||||
}
|
||||
|
||||
if (save)
|
||||
_configurationManager.Save(_configuration);
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginLog.Error($"TryConnect: Login failed with error {loginReply.Error}");
|
||||
_logger.LogError("Login failed with error {Error}", loginReply.Error);
|
||||
_loginInfo = new LoginInfo(null);
|
||||
if (loginReply.Error == LoginError.InvalidAccountId)
|
||||
{
|
||||
Account = null;
|
||||
Service.Configuration.Save();
|
||||
if (retry)
|
||||
{
|
||||
PluginLog.Information("TryConnect: Attempting connection retry without account id");
|
||||
return await TryConnect(cancellationToken, retry: false);
|
||||
}
|
||||
else
|
||||
return (false, Localization.ConnectionError_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)
|
||||
{
|
||||
Service.Chat.PalError(Localization.ConnectionError_OldVersion);
|
||||
_chat.Error(Localization.ConnectionError_OldVersion);
|
||||
_warnedAboutUpgrade = true;
|
||||
}
|
||||
return (false, string.Format(Localization.ConnectionError_LoginFailed, loginReply.Error));
|
||||
|
||||
return (false, string.Format(Localization.ConnectionError_LoginFailed, loginReply.Error),
|
||||
false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!_loginInfo.IsValid)
|
||||
{
|
||||
PluginLog.Error($"TryConnect: Login state is loggedIn={_loginInfo.IsLoggedIn}, expired={_loginInfo.IsExpired}");
|
||||
return (false, Localization.ConnectionError_LoginReturnedNoToken);
|
||||
_logger.LogError("Login state is loggedIn={LoggedIn}, expired={Expired}", _loginInfo.IsLoggedIn,
|
||||
_loginInfo.IsExpired);
|
||||
return (false, Localization.ConnectionError_LoginReturnedNoToken, false);
|
||||
}
|
||||
|
||||
return (true, string.Empty);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return (true, string.Empty, false);
|
||||
}
|
||||
|
||||
private async Task<bool> Connect(CancellationToken cancellationToken)
|
||||
finally
|
||||
{
|
||||
var result = await TryConnect(cancellationToken);
|
||||
return result.Success;
|
||||
}
|
||||
|
||||
public async Task<string> VerifyConnection(CancellationToken cancellationToken = default)
|
||||
{
|
||||
_warnedAboutUpgrade = false;
|
||||
|
||||
var connectionResult = await TryConnect(cancellationToken, loggerFactory: _grpcToPluginLogLoggerFactory);
|
||||
if (!connectionResult.Success)
|
||||
return string.Format(Localization.ConnectionError_CouldNotConnectToServer, connectionResult.Error);
|
||||
|
||||
PluginLog.Information("VerifyConnection: 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);
|
||||
|
||||
PluginLog.Information("VerifyConnection: Verification returned no errors.");
|
||||
return Localization.ConnectionSuccessful;
|
||||
}
|
||||
|
||||
internal 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; }
|
||||
public DateTimeOffset ExpiresAt => Claims?.ExpiresAt.Subtract(TimeSpan.FromMinutes(5)) ?? DateTimeOffset.MinValue;
|
||||
public bool IsExpired => ExpiresAt < DateTimeOffset.UtcNow;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,23 @@
|
||||
using Account;
|
||||
using System;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Export;
|
||||
|
||||
namespace Pal.Client.Net
|
||||
namespace Pal.Client.Net;
|
||||
|
||||
internal partial class RemoteApi
|
||||
{
|
||||
internal partial class RemoteApi
|
||||
public async Task<(bool, ExportRoot)> DoExport(CancellationToken cancellationToken = default)
|
||||
{
|
||||
public async Task<(bool, ExportRoot)> DoExport(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!await Connect(cancellationToken))
|
||||
return new(false, new());
|
||||
if (!await Connect(cancellationToken))
|
||||
return new(false, new());
|
||||
|
||||
var exportClient = new ExportService.ExportServiceClient(_channel);
|
||||
var exportReply = await exportClient.ExportAsync(new ExportRequest
|
||||
var exportClient = new ExportService.ExportServiceClient(_channel);
|
||||
var exportReply = await exportClient.ExportAsync(new ExportRequest
|
||||
{
|
||||
ServerUrl = RemoteUrl,
|
||||
}, headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(120), cancellationToken: cancellationToken);
|
||||
return (exportReply.Success, exportReply.Data);
|
||||
}
|
||||
}, headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(120),
|
||||
cancellationToken: cancellationToken);
|
||||
return (exportReply.Success, exportReply.Data);
|
||||
}
|
||||
}
|
||||
|
@ -1,78 +1,95 @@
|
||||
using Palace;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Pal.Client.Database;
|
||||
using Pal.Client.Floors;
|
||||
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<Marker>)> 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))
|
||||
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(CreateMarkerFromNetworkObject).ToList());
|
||||
}
|
||||
|
||||
public async Task<(bool, List<Marker>)> UploadMarker(ushort territoryType, IList<Marker> markers, CancellationToken cancellationToken = default)
|
||||
TerritoryType = territoryType,
|
||||
};
|
||||
uploadRequest.Objects.AddRange(locations.Select(m => new PalaceObject
|
||||
{
|
||||
if (markers.Count == 0)
|
||||
return (true, new());
|
||||
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());
|
||||
}
|
||||
|
||||
if (!await Connect(cancellationToken))
|
||||
return (false, new());
|
||||
public async Task<bool> MarkAsSeen(ushort territoryType, IReadOnlyList<PersistentLocation> locations,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (locations.Count == 0)
|
||||
return true;
|
||||
|
||||
var palaceClient = new PalaceService.PalaceServiceClient(_channel);
|
||||
var uploadRequest = new UploadFloorsRequest
|
||||
{
|
||||
TerritoryType = territoryType,
|
||||
};
|
||||
uploadRequest.Objects.AddRange(markers.Select(m => new PalaceObject
|
||||
{
|
||||
Type = (ObjectType)m.Type,
|
||||
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(CreateMarkerFromNetworkObject).ToList());
|
||||
}
|
||||
if (!await Connect(cancellationToken))
|
||||
return false;
|
||||
|
||||
public async Task<bool> MarkAsSeen(ushort territoryType, IList<Marker> markers, 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 (markers.Count == 0)
|
||||
return true;
|
||||
Type = obj.Type.ToMemoryType(),
|
||||
Position = new Vector3(obj.X, obj.Y, obj.Z),
|
||||
NetworkId = Guid.Parse(obj.NetworkId),
|
||||
Source = ClientLocation.ESource.Download,
|
||||
};
|
||||
}
|
||||
|
||||
if (!await Connect(cancellationToken))
|
||||
return false;
|
||||
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 seenRequest = new MarkObjectsSeenRequest { TerritoryType = territoryType };
|
||||
foreach (var marker in markers)
|
||||
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 Marker CreateMarkerFromNetworkObject(PalaceObject obj) =>
|
||||
new Marker((Marker.EType)obj.Type, new Vector3(obj.X, obj.Y, obj.Z), Guid.Parse(obj.NetworkId));
|
||||
|
||||
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());
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
@ -1,66 +1,57 @@
|
||||
using System;
|
||||
using Dalamud.Logging;
|
||||
using Grpc.Core;
|
||||
using System.Net.Security;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Dalamud.Logging;
|
||||
using Grpc.Core;
|
||||
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()
|
||||
{
|
||||
{ "Authorization", $"Bearer {_loginInfo.AuthToken}" },
|
||||
{ "User-Agent", _userAgent },
|
||||
};
|
||||
private Metadata AuthorizedHeaders() => new()
|
||||
{
|
||||
{ "Authorization", $"Bearer {_loginInfo.AuthToken}" },
|
||||
{ "User-Agent", _userAgent },
|
||||
};
|
||||
|
||||
private SslClientAuthenticationOptions? GetSslClientAuthenticationOptions()
|
||||
{
|
||||
private SslClientAuthenticationOptions? GetSslClientAuthenticationOptions()
|
||||
{
|
||||
#if !DEBUG
|
||||
var secrets = typeof(RemoteApi).Assembly.GetType("Pal.Client.Secrets");
|
||||
if (secrets == null)
|
||||
return null;
|
||||
|
||||
var pass = secrets.GetProperty("CertPassword")?.GetValue(null) as string;
|
||||
if (pass == null)
|
||||
return null;
|
||||
|
||||
var manifestResourceStream = typeof(RemoteApi).Assembly.GetManifestResourceStream("Pal.Client.Certificate.pfx");
|
||||
if (manifestResourceStream == null)
|
||||
return null;
|
||||
|
||||
var bytes = new byte[manifestResourceStream.Length];
|
||||
int read = manifestResourceStream.Read(bytes, 0, bytes.Length);
|
||||
if (read != bytes.Length)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
var certificate = new X509Certificate2(bytes, pass, X509KeyStorageFlags.DefaultKeySet);
|
||||
PluginLog.Debug($"Using client certificate {certificate.GetCertHashString()}");
|
||||
return new SslClientAuthenticationOptions
|
||||
{
|
||||
ClientCertificates = new X509CertificateCollection()
|
||||
{
|
||||
certificate,
|
||||
},
|
||||
};
|
||||
#else
|
||||
PluginLog.Debug("Not using client certificate");
|
||||
var secrets = typeof(RemoteApi).Assembly.GetType("Pal.Client.Secrets");
|
||||
if (secrets == null)
|
||||
return null;
|
||||
#endif
|
||||
}
|
||||
|
||||
public bool HasRoleOnCurrentServer(string role)
|
||||
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
|
||||
{
|
||||
if (Service.Configuration.Mode != Configuration.EMode.Online)
|
||||
return false;
|
||||
|
||||
var account = Account;
|
||||
return account == null || account.CachedRoles.Contains(role);
|
||||
}
|
||||
ClientCertificates = new X509CertificateCollection()
|
||||
{
|
||||
certificate,
|
||||
},
|
||||
};
|
||||
#else
|
||||
_logger.LogDebug("Not using client certificate");
|
||||
return null;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
@ -1,49 +1,51 @@
|
||||
using Dalamud.Logging;
|
||||
using System;
|
||||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Logging;
|
||||
using Grpc.Net.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using Pal.Client.Extensions;
|
||||
using Pal.Client.Configuration;
|
||||
using Pal.Client.DependencyInjection;
|
||||
|
||||
namespace Pal.Client.Net
|
||||
namespace Pal.Client.Net;
|
||||
|
||||
internal sealed partial class RemoteApi : IDisposable
|
||||
{
|
||||
internal partial class RemoteApi : IDisposable
|
||||
{
|
||||
#if DEBUG
|
||||
public static string RemoteUrl { get; } = "http://localhost:5145";
|
||||
public const string RemoteUrl = "http://localhost:5415";
|
||||
#else
|
||||
public static string RemoteUrl { get; } = "https://pal.μ.tv";
|
||||
public const string RemoteUrl = "https://connect.palacepal.com";
|
||||
#endif
|
||||
private readonly string _userAgent = $"{typeof(RemoteApi).Assembly.GetName().Name?.Replace(" ", "")}/{typeof(RemoteApi).Assembly.GetName().Version?.ToString(2)}";
|
||||
private readonly string _userAgent =
|
||||
$"{typeof(RemoteApi).Assembly.GetName().Name?.Replace(" ", "")}/{typeof(RemoteApi).Assembly.GetName().Version?.ToString(2)}";
|
||||
|
||||
private readonly ILoggerFactory _grpcToPluginLogLoggerFactory = LoggerFactory.Create(builder => builder.AddProvider(new GrpcLoggerProvider()).AddFilter("Grpc", LogLevel.Trace));
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly ILogger<RemoteApi> _logger;
|
||||
private readonly Chat _chat;
|
||||
private readonly ConfigurationManager _configurationManager;
|
||||
private readonly IPalacePalConfiguration _configuration;
|
||||
|
||||
private GrpcChannel? _channel;
|
||||
private LoginInfo _loginInfo = new(null);
|
||||
private bool _warnedAboutUpgrade;
|
||||
private GrpcChannel? _channel;
|
||||
private LoginInfo _loginInfo = new(null);
|
||||
private bool _warnedAboutUpgrade;
|
||||
|
||||
public Configuration.AccountInfo? Account
|
||||
{
|
||||
get => Service.Configuration.Accounts.TryGetValue(RemoteUrl, out Configuration.AccountInfo? accountInfo) ? accountInfo : null;
|
||||
set
|
||||
{
|
||||
if (value != null)
|
||||
Service.Configuration.Accounts[RemoteUrl] = value;
|
||||
else
|
||||
Service.Configuration.Accounts.Remove(RemoteUrl);
|
||||
}
|
||||
}
|
||||
public RemoteApi(
|
||||
ILoggerFactory loggerFactory,
|
||||
ILogger<RemoteApi> logger,
|
||||
Chat chat,
|
||||
ConfigurationManager configurationManager,
|
||||
IPalacePalConfiguration configuration)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_logger = logger;
|
||||
_chat = chat;
|
||||
_configurationManager = configurationManager;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public Guid? AccountId => Account?.Id;
|
||||
|
||||
public string? PartialAccountId => Account?.Id?.ToPartialId();
|
||||
|
||||
private string FormattedPartialAccountId => PartialAccountId ?? "[no account id]";
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
PluginLog.Debug("Disposing gRPC channel");
|
||||
_channel?.Dispose();
|
||||
_channel = null;
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
_logger.LogDebug("Disposing gRPC channel");
|
||||
_channel?.Dispose();
|
||||
_channel = null;
|
||||
}
|
||||
}
|
||||
|
@ -1,104 +1,69 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Dalamud.NET.Sdk/9.0.2">
|
||||
<PropertyGroup>
|
||||
<Version>6.0</Version>
|
||||
<AssemblyName>Palace Pal</AssemblyName>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<LangVersion>11.0</LangVersion>
|
||||
<Version>2.15</Version>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<Import Project="..\vendor\LLib\LLib.targets"/>
|
||||
<Import Project="..\vendor\LLib\RenameZip.targets"/>
|
||||
|
||||
<PropertyGroup>
|
||||
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<AssemblyName>Palace Pal</AssemblyName>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
<OutputPath>dist</OutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
<OutputPath>dist</OutputPath>
|
||||
<DebugType>none</DebugType>
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition="'$(Configuration)' == 'Release' And Exists('Certificate.pfx')">
|
||||
<None Remove="Certificate.pfx"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(Configuration)' == 'Release' And Exists('Certificate.pfx')">
|
||||
<None Remove="Certificate.pfx" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(Configuration)' == 'Release' And Exists('Certificate.pfx')">
|
||||
<EmbeddedResource Include="Certificate.pfx"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(Configuration)' == 'Release' And Exists('Certificate.pfx')">
|
||||
<EmbeddedResource Include="Certificate.pfx" />
|
||||
<EmbeddedResource Update="Properties\Localization.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Localization.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dalamud.Extensions.MicrosoftLogging" Version="4.0.1"/>
|
||||
<PackageReference Include="Google.Protobuf" Version="3.27.2" />
|
||||
<PackageReference Include="Grpc.Net.Client" Version="2.63.0"/>
|
||||
<PackageReference Include="Grpc.Tools" Version="2.64.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.6" Condition="'$(Configuration)' == 'EF'">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0"/>
|
||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DalamudPackager" Version="2.1.10" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.21.12" />
|
||||
<PackageReference Include="Grpc.Net.Client" Version="2.51.0" />
|
||||
<PackageReference Include="Grpc.Tools" Version="2.51.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
|
||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Pal.Common\Pal.Common.csproj"/>
|
||||
<ProjectReference Include="..\vendor\ECommons\ECommons\ECommons.csproj"/>
|
||||
<ProjectReference Include="..\vendor\LLib\LLib.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Pal.Common\Pal.Common.csproj" />
|
||||
<ProjectReference Include="..\vendor\ECommons\ECommons\ECommons.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"/>
|
||||
<Protobuf Include="..\Pal.Common\Protos\export.proto" Link="Protos\export.proto" GrpcServices="Client" Access="Internal"/>
|
||||
</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" />
|
||||
<Protobuf Include="..\Pal.Common\Protos\export.proto" Link="Protos\export.proto" GrpcServices="Client" Access="Internal" />
|
||||
</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>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="ImGui.NET">
|
||||
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\ImGui.NET.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="ImGuiScene">
|
||||
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Lumina">
|
||||
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Lumina.Excel">
|
||||
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json">
|
||||
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Newtonsoft.Json.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="FFXIVClientStructs">
|
||||
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\FFXIVClientStructs.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Localization.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Localization.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="RenameLatestZip" AfterTargets="PackagePlugin">
|
||||
<Exec Command="rename "$(OutDir)$(AssemblyName)\latest.zip" "$(AssemblyName)-$(Version).zip"" />
|
||||
</Target>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Localization.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Localization.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
<Compile Update="Properties\Localization.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Localization.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="Clean">
|
||||
<RemoveDir Directories="dist"/>
|
||||
</Target>
|
||||
</Project>
|
||||
|
@ -3,7 +3,12 @@
|
||||
"Author": "Liza Carvelli",
|
||||
"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.",
|
||||
"RepoUrl": "https://github.com/carvelli/PalacePal",
|
||||
"IconUrl": "https://raw.githubusercontent.com/carvelli/Dalamud-Plugins/master/dist/Palace Pal.png",
|
||||
"Tags": [ "potd", "palace", "hoh", "splatoon" ]
|
||||
}
|
||||
"RepoUrl": "https://git.carvel.li/liza/PalacePal",
|
||||
"IconUrl": "https://plugins.carvel.li/icons/PalacePal.png",
|
||||
"Tags": [
|
||||
"potd",
|
||||
"palace",
|
||||
"hoh",
|
||||
"splatoon"
|
||||
]
|
||||
}
|
||||
|
@ -1,699 +1,235 @@
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Game.Command;
|
||||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Logging;
|
||||
using Dalamud.Plugin;
|
||||
using Grpc.Core;
|
||||
using ImGuiNET;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using Pal.Client.Rendering;
|
||||
using Pal.Client.Scheduled;
|
||||
using Pal.Client.Windows;
|
||||
using Pal.Common;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Pal.Client.Extensions;
|
||||
using Pal.Client.Properties;
|
||||
using Dalamud.Extensions.MicrosoftLogging;
|
||||
using Dalamud.Game.Command;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using ECommons;
|
||||
using ECommons.DalamudServices;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Pal.Client.Commands;
|
||||
using Pal.Client.Configuration;
|
||||
using Pal.Client.DependencyInjection;
|
||||
using Pal.Client.Properties;
|
||||
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
|
||||
{
|
||||
public class Plugin : IDalamudPlugin
|
||||
private readonly CancellationTokenSource _initCts = new();
|
||||
|
||||
private readonly IDalamudPluginInterface _pluginInterface;
|
||||
private readonly ICommandManager _commandManager;
|
||||
private readonly IClientState _clientState;
|
||||
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)
|
||||
{
|
||||
internal const uint ColorInvisible = 0;
|
||||
_pluginInterface = pluginInterface;
|
||||
_commandManager = commandManager;
|
||||
_clientState = clientState;
|
||||
_chatGui = chatGui;
|
||||
_framework = framework;
|
||||
_logger = new DalamudLoggerProvider(pluginLog).CreateLogger<Plugin>();
|
||||
|
||||
private LocalizedChatMessages _localizedChatMessages = new();
|
||||
// set up the current UI language before creating anything
|
||||
Localization.Culture = new CultureInfo(_pluginInterface.UiLanguage);
|
||||
|
||||
internal ConcurrentDictionary<ushort, LocalState> FloorMarkers { get; } = new();
|
||||
internal ConcurrentBag<Marker> EphemeralMarkers { get; set; } = new();
|
||||
internal ushort LastTerritory { get; set; }
|
||||
internal SyncState TerritorySyncState { get; set; }
|
||||
internal PomanderState PomanderOfSight { get; private set; } = PomanderState.Inactive;
|
||||
internal PomanderState PomanderOfIntuition { get; private set; } = PomanderState.Inactive;
|
||||
internal string? DebugMessage { get; set; }
|
||||
internal Queue<IQueueOnFrameworkThread> EarlyEventQueue { get; } = new();
|
||||
internal Queue<IQueueOnFrameworkThread> LateEventQueue { get; } = new();
|
||||
internal ConcurrentQueue<nint> NextUpdateObjects { get; } = new();
|
||||
internal IRenderer Renderer { get; private set; } = null!;
|
||||
|
||||
public string Name => Localization.Palace_Pal;
|
||||
|
||||
public Plugin(DalamudPluginInterface pluginInterface, ChatGui chat)
|
||||
_commandManager.AddHandler("/pal", new CommandInfo(OnCommand)
|
||||
{
|
||||
LanguageChanged(pluginInterface.UiLanguage);
|
||||
HelpMessage = Localization.Command_pal_HelpText
|
||||
});
|
||||
|
||||
PluginLog.Information($"Install source: {pluginInterface.SourceRepository}");
|
||||
// Using TickScheduler requires ECommons to at least be partially initialized
|
||||
// ECommonsMain.Dispose leaves this untouched.
|
||||
Svc.Init(pluginInterface);
|
||||
|
||||
#if RELEASE
|
||||
// You're welcome to remove this code in your fork, as long as:
|
||||
// - none of the links accessible within FFXIV open the original repo (e.g. in the plugin installer), and
|
||||
// - you host your own server instance
|
||||
if (!pluginInterface.IsDev
|
||||
&& !pluginInterface.SourceRepository.StartsWith("https://raw.githubusercontent.com/carvelli/")
|
||||
&& !pluginInterface.SourceRepository.StartsWith("https://github.com/carvelli/"))
|
||||
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(() =>
|
||||
{
|
||||
chat.PalError(string.Format(Localization.Error_WrongRepository, "https://github.com/carvelli/Dalamud-Plugins"));
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
#endif
|
||||
|
||||
pluginInterface.Create<Service>();
|
||||
Service.Plugin = this;
|
||||
Service.Configuration = (Configuration?)pluginInterface.GetPluginConfig() ?? pluginInterface.Create<Configuration>()!;
|
||||
Service.Configuration.Migrate();
|
||||
|
||||
ResetRenderer();
|
||||
|
||||
Service.Hooks = new Hooks();
|
||||
|
||||
var agreementWindow = pluginInterface.Create<AgreementWindow>();
|
||||
if (agreementWindow is not null)
|
||||
{
|
||||
agreementWindow.IsOpen = Service.Configuration.FirstUse;
|
||||
Service.WindowSystem.AddWindow(agreementWindow);
|
||||
}
|
||||
|
||||
var configWindow = pluginInterface.Create<ConfigWindow>();
|
||||
if (configWindow is not null)
|
||||
{
|
||||
Service.WindowSystem.AddWindow(configWindow);
|
||||
}
|
||||
|
||||
var statisticsWindow = pluginInterface.Create<StatisticsWindow>();
|
||||
if (statisticsWindow is not null)
|
||||
{
|
||||
Service.WindowSystem.AddWindow(statisticsWindow);
|
||||
}
|
||||
|
||||
pluginInterface.UiBuilder.Draw += Draw;
|
||||
pluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi;
|
||||
pluginInterface.LanguageChanged += LanguageChanged;
|
||||
Service.Framework.Update += OnFrameworkUpdate;
|
||||
Service.Chat.ChatMessage += OnChatMessage;
|
||||
Service.CommandManager.AddHandler("/pal", new CommandInfo(OnCommand)
|
||||
{
|
||||
HelpMessage = Localization.Command_pal_HelpText
|
||||
_pluginInterface.UiBuilder.Draw += Draw;
|
||||
_pluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi;
|
||||
_pluginInterface.LanguageChanged += LanguageChanged;
|
||||
_clientState.Login += Login;
|
||||
});
|
||||
|
||||
ReloadLanguageStrings();
|
||||
_rootScopeCompletionSource.SetResult(_rootScope);
|
||||
_loadState = ELoadState.Loaded;
|
||||
}
|
||||
|
||||
private void OpenConfigUi()
|
||||
catch (Exception e) when (e is ObjectDisposedException
|
||||
or OperationCanceledException
|
||||
or RepoVerification.RepoVerificationFailedException
|
||||
|| (e is FileLoadException && _pluginInterface.IsDev))
|
||||
{
|
||||
Window? configWindow;
|
||||
if (Service.Configuration.FirstUse)
|
||||
configWindow = Service.WindowSystem.GetWindow<AgreementWindow>();
|
||||
else
|
||||
configWindow = Service.WindowSystem.GetWindow<ConfigWindow>();
|
||||
|
||||
if (configWindow != null)
|
||||
configWindow.IsOpen = true;
|
||||
_rootScopeCompletionSource.SetException(e);
|
||||
_loadState = ELoadState.Error;
|
||||
}
|
||||
|
||||
private void OnCommand(string command, string arguments)
|
||||
catch (Exception e)
|
||||
{
|
||||
if (Service.Configuration.FirstUse)
|
||||
{
|
||||
Service.Chat.PalError(Localization.Error_FirstTimeSetupRequired);
|
||||
return;
|
||||
}
|
||||
_rootScopeCompletionSource.SetException(e);
|
||||
_logger.LogError(e, "Async load failed");
|
||||
ShowErrorOnLogin(() =>
|
||||
new Chat(_chatGui).Error(string.Format(Localization.Error_LoadFailed,
|
||||
$"{e.GetType()} - {e.Message}")));
|
||||
|
||||
try
|
||||
{
|
||||
arguments = arguments.Trim();
|
||||
switch (arguments)
|
||||
{
|
||||
case "stats":
|
||||
Task.Run(async () => await FetchFloorStatistics());
|
||||
break;
|
||||
|
||||
case "test-connection":
|
||||
case "tc":
|
||||
var configWindow = Service.WindowSystem.GetWindow<ConfigWindow>();
|
||||
if (configWindow == null)
|
||||
return;
|
||||
|
||||
configWindow.IsOpen = true;
|
||||
configWindow.TestConnection();
|
||||
break;
|
||||
|
||||
#if DEBUG
|
||||
case "update-saves":
|
||||
LocalState.UpdateAll();
|
||||
Service.Chat.Print(Localization.Command_pal_updatesaves);
|
||||
break;
|
||||
#endif
|
||||
|
||||
case "":
|
||||
case "config":
|
||||
Service.WindowSystem.GetWindow<ConfigWindow>()?.Toggle();
|
||||
break;
|
||||
|
||||
case "near":
|
||||
DebugNearest(_ => true);
|
||||
break;
|
||||
|
||||
case "tnear":
|
||||
DebugNearest(m => m.Type == Marker.EType.Trap);
|
||||
break;
|
||||
|
||||
case "hnear":
|
||||
DebugNearest(m => m.Type == Marker.EType.Hoard);
|
||||
break;
|
||||
|
||||
default:
|
||||
Service.Chat.PalError(string.Format(Localization.Command_pal_UnknownSubcommand, arguments, command));
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Service.Chat.PalError(e.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
#region IDisposable Support
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposing) return;
|
||||
|
||||
Service.CommandManager.RemoveHandler("/pal");
|
||||
Service.PluginInterface.UiBuilder.Draw -= Draw;
|
||||
Service.PluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi;
|
||||
Service.PluginInterface.LanguageChanged -= LanguageChanged;
|
||||
Service.Framework.Update -= OnFrameworkUpdate;
|
||||
Service.Chat.ChatMessage -= OnChatMessage;
|
||||
|
||||
Service.WindowSystem.RemoveAllWindows();
|
||||
|
||||
Service.RemoteApi.Dispose();
|
||||
Service.Hooks.Dispose();
|
||||
|
||||
if (Renderer is IDisposable disposable)
|
||||
disposable.Dispose();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
#endregion
|
||||
|
||||
private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString seMessage, ref bool isHandled)
|
||||
{
|
||||
if (Service.Configuration.FirstUse)
|
||||
return;
|
||||
|
||||
if (type != (XivChatType)2105)
|
||||
return;
|
||||
|
||||
string message = seMessage.ToString();
|
||||
if (_localizedChatMessages.FloorChanged.IsMatch(message))
|
||||
{
|
||||
PomanderOfSight = PomanderState.Inactive;
|
||||
|
||||
if (PomanderOfIntuition == PomanderState.FoundOnCurrentFloor)
|
||||
PomanderOfIntuition = PomanderState.Inactive;
|
||||
}
|
||||
else if (message.EndsWith(_localizedChatMessages.MapRevealed))
|
||||
{
|
||||
PomanderOfSight = PomanderState.Active;
|
||||
}
|
||||
else if (message.EndsWith(_localizedChatMessages.AllTrapsRemoved))
|
||||
{
|
||||
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.
|
||||
PomanderOfIntuition = PomanderState.Active;
|
||||
}
|
||||
else if (message.EndsWith(_localizedChatMessages.HoardCofferOpened))
|
||||
{
|
||||
PomanderOfIntuition = PomanderState.FoundOnCurrentFloor;
|
||||
}
|
||||
}
|
||||
|
||||
private void LanguageChanged(string langcode)
|
||||
{
|
||||
Localization.Culture = new CultureInfo(langcode);
|
||||
Service.WindowSystem.Windows.OfType<ILanguageChanged>().Each(w => w.LanguageChanged());
|
||||
}
|
||||
|
||||
private void OnFrameworkUpdate(Framework framework)
|
||||
{
|
||||
if (Service.Configuration.FirstUse)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
bool recreateLayout = false;
|
||||
bool saveMarkers = false;
|
||||
|
||||
while (EarlyEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued))
|
||||
queued.Run(this, ref recreateLayout, ref saveMarkers);
|
||||
|
||||
if (LastTerritory != Service.ClientState.TerritoryType)
|
||||
{
|
||||
LastTerritory = Service.ClientState.TerritoryType;
|
||||
TerritorySyncState = SyncState.NotAttempted;
|
||||
NextUpdateObjects.Clear();
|
||||
|
||||
if (IsInDeepDungeon())
|
||||
GetFloorMarkers(LastTerritory);
|
||||
EphemeralMarkers.Clear();
|
||||
PomanderOfSight = PomanderState.Inactive;
|
||||
PomanderOfIntuition = PomanderState.Inactive;
|
||||
recreateLayout = true;
|
||||
DebugMessage = null;
|
||||
}
|
||||
|
||||
if (!IsInDeepDungeon())
|
||||
return;
|
||||
|
||||
if (Service.Configuration.Mode == Configuration.EMode.Online && TerritorySyncState == SyncState.NotAttempted)
|
||||
{
|
||||
TerritorySyncState = SyncState.Started;
|
||||
Task.Run(async () => await DownloadMarkersForTerritory(LastTerritory));
|
||||
}
|
||||
|
||||
while (LateEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued))
|
||||
queued.Run(this, ref recreateLayout, ref saveMarkers);
|
||||
|
||||
var currentFloor = GetFloorMarkers(LastTerritory);
|
||||
|
||||
IList<Marker> visibleMarkers = GetRelevantGameObjects();
|
||||
HandlePersistentMarkers(currentFloor, visibleMarkers.Where(x => x.IsPermanent()).ToList(), saveMarkers, recreateLayout);
|
||||
HandleEphemeralMarkers(visibleMarkers.Where(x => !x.IsPermanent()).ToList(), recreateLayout);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugMessage = $"{DateTime.Now}\n{e}";
|
||||
}
|
||||
}
|
||||
|
||||
internal LocalState GetFloorMarkers(ushort territoryType)
|
||||
{
|
||||
return FloorMarkers.GetOrAdd(territoryType, tt => LocalState.Load(tt) ?? new LocalState(tt));
|
||||
}
|
||||
|
||||
#region Rendering markers
|
||||
private void HandlePersistentMarkers(LocalState currentFloor, IList<Marker> visibleMarkers, bool saveMarkers, bool recreateLayout)
|
||||
{
|
||||
var config = Service.Configuration;
|
||||
var currentFloorMarkers = currentFloor.Markers;
|
||||
|
||||
bool updateSeenMarkers = false;
|
||||
var partialAccountId = Service.RemoteApi.PartialAccountId;
|
||||
foreach (var visibleMarker in visibleMarkers)
|
||||
{
|
||||
Marker? knownMarker = currentFloorMarkers.SingleOrDefault(x => x == visibleMarker);
|
||||
if (knownMarker != null)
|
||||
{
|
||||
if (!knownMarker.Seen)
|
||||
{
|
||||
knownMarker.Seen = true;
|
||||
saveMarkers = true;
|
||||
}
|
||||
|
||||
// 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 && knownMarker is { NetworkId: { }, RemoteSeenRequested: false } && !knownMarker.RemoteSeenOn.Contains(partialAccountId))
|
||||
updateSeenMarkers = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
currentFloorMarkers.Add(visibleMarker);
|
||||
recreateLayout = true;
|
||||
saveMarkers = true;
|
||||
}
|
||||
|
||||
if (!recreateLayout && currentFloorMarkers.Count > 0 && (config.OnlyVisibleTrapsAfterPomander || config.OnlyVisibleHoardAfterPomander))
|
||||
{
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var marker in currentFloorMarkers)
|
||||
{
|
||||
uint desiredColor = DetermineColor(marker, visibleMarkers);
|
||||
if (marker.RenderElement == null || !marker.RenderElement.IsValid)
|
||||
{
|
||||
recreateLayout = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (marker.RenderElement.Color != desiredColor)
|
||||
marker.RenderElement.Color = desiredColor;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugMessage = $"{DateTime.Now}\n{e}";
|
||||
recreateLayout = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updateSeenMarkers && partialAccountId != null)
|
||||
{
|
||||
var markersToUpdate = currentFloorMarkers.Where(x => x is { Seen: true, NetworkId: { }, RemoteSeenRequested: false } && !x.RemoteSeenOn.Contains(partialAccountId)).ToList();
|
||||
foreach (var marker in markersToUpdate)
|
||||
marker.RemoteSeenRequested = true;
|
||||
Task.Run(async () => await SyncSeenMarkersForTerritory(LastTerritory, markersToUpdate));
|
||||
}
|
||||
|
||||
if (saveMarkers)
|
||||
{
|
||||
currentFloor.Save();
|
||||
|
||||
if (TerritorySyncState == SyncState.Complete)
|
||||
{
|
||||
var markersToUpload = currentFloorMarkers.Where(x => x.IsPermanent() && x.NetworkId == null && !x.UploadRequested).ToList();
|
||||
if (markersToUpload.Count > 0)
|
||||
{
|
||||
foreach (var marker in markersToUpload)
|
||||
marker.UploadRequested = true;
|
||||
Task.Run(async () => await UploadMarkersForTerritory(LastTerritory, markersToUpload));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (recreateLayout)
|
||||
{
|
||||
Renderer.ResetLayer(ELayer.TrapHoard);
|
||||
|
||||
List<IRenderElement> elements = new();
|
||||
foreach (var marker in currentFloorMarkers)
|
||||
{
|
||||
if (marker.Seen || config.Mode == Configuration.EMode.Online || marker is { WasImported: true, Imports.Count: > 0 })
|
||||
{
|
||||
if (marker.Type == Marker.EType.Trap && config.ShowTraps)
|
||||
{
|
||||
CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers));
|
||||
}
|
||||
else if (marker.Type == Marker.EType.Hoard && config.ShowHoard)
|
||||
{
|
||||
CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (elements.Count == 0)
|
||||
return;
|
||||
|
||||
Renderer.SetLayer(ELayer.TrapHoard, elements);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleEphemeralMarkers(IList<Marker> visibleMarkers, bool recreateLayout)
|
||||
{
|
||||
recreateLayout |= EphemeralMarkers.Any(existingMarker => visibleMarkers.All(x => x != existingMarker));
|
||||
recreateLayout |= visibleMarkers.Any(visibleMarker => EphemeralMarkers.All(x => x != visibleMarker));
|
||||
|
||||
if (recreateLayout)
|
||||
{
|
||||
Renderer.ResetLayer(ELayer.RegularCoffers);
|
||||
EphemeralMarkers.Clear();
|
||||
|
||||
var config = Service.Configuration;
|
||||
|
||||
List<IRenderElement> elements = new();
|
||||
foreach (var marker in visibleMarkers)
|
||||
{
|
||||
EphemeralMarkers.Add(marker);
|
||||
|
||||
if (marker.Type == Marker.EType.SilverCoffer && config.ShowSilverCoffers)
|
||||
{
|
||||
CreateRenderElement(marker, elements, DetermineColor(marker, visibleMarkers), config.FillSilverCoffers);
|
||||
}
|
||||
}
|
||||
|
||||
if (elements.Count == 0)
|
||||
return;
|
||||
|
||||
Renderer.SetLayer(ELayer.RegularCoffers, elements);
|
||||
}
|
||||
}
|
||||
|
||||
private uint DetermineColor(Marker marker, IList<Marker> visibleMarkers)
|
||||
{
|
||||
switch (marker.Type)
|
||||
{
|
||||
case Marker.EType.Trap when PomanderOfSight == PomanderState.Inactive || !Service.Configuration.OnlyVisibleTrapsAfterPomander || visibleMarkers.Any(x => x == marker):
|
||||
return ImGui.ColorConvertFloat4ToU32(Service.Configuration.TrapColor);
|
||||
case Marker.EType.Hoard when PomanderOfIntuition == PomanderState.Inactive || !Service.Configuration.OnlyVisibleHoardAfterPomander || visibleMarkers.Any(x => x == marker):
|
||||
return ImGui.ColorConvertFloat4ToU32(Service.Configuration.HoardColor);
|
||||
case Marker.EType.SilverCoffer:
|
||||
return ImGui.ColorConvertFloat4ToU32(Service.Configuration.SilverCofferColor);
|
||||
case Marker.EType.Trap:
|
||||
case Marker.EType.Hoard:
|
||||
return ColorInvisible;
|
||||
default:
|
||||
return ImGui.ColorConvertFloat4ToU32(new Vector4(1, 0.5f, 1, 0.4f));
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateRenderElement(Marker marker, List<IRenderElement> elements, uint color, bool fill = false)
|
||||
{
|
||||
var element = Renderer.CreateElement(marker.Type, marker.Position, color, fill);
|
||||
marker.RenderElement = element;
|
||||
elements.Add(element);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Up-/Download
|
||||
private async Task DownloadMarkersForTerritory(ushort territoryId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var (success, downloadedMarkers) = await Service.RemoteApi.DownloadRemoteMarkers(territoryId);
|
||||
LateEventQueue.Enqueue(new QueuedSyncResponse
|
||||
{
|
||||
Type = SyncType.Download,
|
||||
TerritoryType = territoryId,
|
||||
Success = success,
|
||||
Markers = downloadedMarkers
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugMessage = $"{DateTime.Now}\n{e}";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UploadMarkersForTerritory(ushort territoryId, List<Marker> markersToUpload)
|
||||
{
|
||||
try
|
||||
{
|
||||
var (success, uploadedMarkers) = await Service.RemoteApi.UploadMarker(territoryId, markersToUpload);
|
||||
LateEventQueue.Enqueue(new QueuedSyncResponse
|
||||
{
|
||||
Type = SyncType.Upload,
|
||||
TerritoryType = territoryId,
|
||||
Success = success,
|
||||
Markers = uploadedMarkers
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugMessage = $"{DateTime.Now}\n{e}";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SyncSeenMarkersForTerritory(ushort territoryId, List<Marker> markersToUpdate)
|
||||
{
|
||||
try
|
||||
{
|
||||
var success = await Service.RemoteApi.MarkAsSeen(territoryId, markersToUpdate);
|
||||
LateEventQueue.Enqueue(new QueuedSyncResponse
|
||||
{
|
||||
Type = SyncType.MarkSeen,
|
||||
TerritoryType = territoryId,
|
||||
Success = success,
|
||||
Markers = markersToUpdate,
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugMessage = $"{DateTime.Now}\n{e}";
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Command Handling
|
||||
private async Task FetchFloorStatistics()
|
||||
{
|
||||
if (!Service.RemoteApi.HasRoleOnCurrentServer("statistics:view"))
|
||||
{
|
||||
Service.Chat.PalError(Localization.Command_pal_stats_CurrentFloor);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var (success, floorStatistics) = await Service.RemoteApi.FetchStatistics();
|
||||
if (success)
|
||||
{
|
||||
var statisticsWindow = Service.WindowSystem.GetWindow<StatisticsWindow>()!;
|
||||
statisticsWindow.SetFloorData(floorStatistics);
|
||||
statisticsWindow.IsOpen = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Service.Chat.PalError(Localization.Command_pal_stats_UnableToFetchStatistics);
|
||||
}
|
||||
}
|
||||
catch (RpcException e) when (e.StatusCode == StatusCode.PermissionDenied)
|
||||
{
|
||||
Service.Chat.Print(Localization.Command_pal_stats_CurrentFloor);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Service.Chat.PalError(e.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private void DebugNearest(Predicate<Marker> predicate)
|
||||
{
|
||||
if (!IsInDeepDungeon())
|
||||
return;
|
||||
|
||||
var state = GetFloorMarkers(Service.ClientState.TerritoryType);
|
||||
var playerPosition = Service.ClientState.LocalPlayer?.Position;
|
||||
if (playerPosition == null)
|
||||
return;
|
||||
Service.Chat.Print($"[Palace Pal] {playerPosition}");
|
||||
|
||||
var nearbyMarkers = state.Markers
|
||||
.Where(m => predicate(m))
|
||||
.Where(m => m.RenderElement != null && m.RenderElement.Color != ColorInvisible)
|
||||
.Select(m => new { m, distance = (playerPosition - m.Position)?.Length() ?? float.MaxValue })
|
||||
.OrderBy(m => m.distance)
|
||||
.Take(5)
|
||||
.ToList();
|
||||
foreach (var nearbyMarker in nearbyMarkers)
|
||||
Service.Chat.Print($"{nearbyMarker.distance:F2} - {nearbyMarker.m.Type} {nearbyMarker.m.NetworkId?.ToPartialId(length: 8)} - {nearbyMarker.m.Position}");
|
||||
}
|
||||
#endregion
|
||||
|
||||
private IList<Marker> GetRelevantGameObjects()
|
||||
{
|
||||
List<Marker> result = new();
|
||||
for (int i = 246; i < Service.ObjectTable.Length; i++)
|
||||
{
|
||||
GameObject? obj = Service.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:
|
||||
result.Add(new Marker(Marker.EType.Trap, obj.Position) { Seen = true });
|
||||
break;
|
||||
|
||||
case 2007542:
|
||||
case 2007543:
|
||||
result.Add(new Marker(Marker.EType.Hoard, obj.Position) { Seen = true });
|
||||
break;
|
||||
|
||||
case 2007357:
|
||||
result.Add(new Marker(Marker.EType.SilverCoffer, obj.Position) { Seen = true });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
while (NextUpdateObjects.TryDequeue(out nint address))
|
||||
{
|
||||
var obj = Service.ObjectTable.FirstOrDefault(x => x.Address == address);
|
||||
if (obj != null && obj.Position.Length() > 0.1)
|
||||
result.Add(new Marker(Marker.EType.Trap, obj.Position) { Seen = true });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal bool IsInDeepDungeon() =>
|
||||
Service.ClientState.IsLoggedIn
|
||||
&& Service.Condition[ConditionFlag.InDeepDungeon]
|
||||
&& typeof(ETerritoryType).IsEnumDefined(Service.ClientState.TerritoryType);
|
||||
|
||||
private void ReloadLanguageStrings()
|
||||
{
|
||||
_localizedChatMessages = new LocalizedChatMessages
|
||||
{
|
||||
MapRevealed = GetLocalizedString(7256),
|
||||
AllTrapsRemoved = GetLocalizedString(7255),
|
||||
HoardOnCurrentFloor = GetLocalizedString(7272),
|
||||
HoardNotOnCurrentFloor = GetLocalizedString(7273),
|
||||
HoardCofferOpened = GetLocalizedString(7274),
|
||||
FloorChanged = new Regex("^" + GetLocalizedString(7270).Replace("\u0002 \u0003\ufffd\u0002\u0003", @"(\d+)") + "$"),
|
||||
};
|
||||
}
|
||||
|
||||
internal void ResetRenderer()
|
||||
{
|
||||
if (Renderer is SplatoonRenderer && Service.Configuration.Renderer == Configuration.ERenderer.Splatoon)
|
||||
return;
|
||||
else if (Renderer is SimpleRenderer && Service.Configuration.Renderer == Configuration.ERenderer.Simple)
|
||||
return;
|
||||
|
||||
if (Renderer is IDisposable disposable)
|
||||
disposable.Dispose();
|
||||
|
||||
if (Service.Configuration.Renderer == Configuration.ERenderer.Splatoon)
|
||||
Renderer = new SplatoonRenderer(Service.PluginInterface, this);
|
||||
else
|
||||
Renderer = new SimpleRenderer();
|
||||
}
|
||||
|
||||
private void Draw()
|
||||
{
|
||||
if (Renderer is SimpleRenderer sr)
|
||||
sr.DrawLayers();
|
||||
|
||||
Service.WindowSystem.Draw();
|
||||
}
|
||||
|
||||
private string GetLocalizedString(uint id)
|
||||
{
|
||||
return Service.DataManager.GetExcelSheet<LogMessage>()?.GetRow(id)?.Text?.ToString() ?? "Unknown";
|
||||
}
|
||||
|
||||
public enum PomanderState
|
||||
{
|
||||
Inactive,
|
||||
Active,
|
||||
FoundOnCurrentFloor,
|
||||
PomanderOfSafetyUsed,
|
||||
}
|
||||
|
||||
private 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+)$");
|
||||
_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
|
||||
{
|
||||
rootScope = await _rootScopeCompletionSource.Task;
|
||||
chat = rootScope.ServiceProvider.GetRequiredService<Chat>();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Could not wait for command root scope");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IPalacePalConfiguration configuration =
|
||||
rootScope.ServiceProvider.GetRequiredService<IPalacePalConfiguration>();
|
||||
if (configuration.FirstUse && arguments != "" && arguments != "config")
|
||||
{
|
||||
chat.Error(Localization.Error_FirstTimeSetupRequired);
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
_initCts.Cancel();
|
||||
_rootScope?.Dispose();
|
||||
_dependencyInjectionContext?.Dispose();
|
||||
}
|
||||
|
||||
private enum ELoadState
|
||||
{
|
||||
Initializing,
|
||||
Loaded,
|
||||
Error
|
||||
}
|
||||
}
|
||||
|
76
Pal.Client/Properties/Localization.Designer.cs
generated
76
Pal.Client/Properties/Localization.Designer.cs
generated
@ -1,7 +1,6 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
@ -150,15 +149,6 @@ namespace Pal.Client.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Updated all locally cached marker files to latest version..
|
||||
/// </summary>
|
||||
internal static string Command_pal_updatesaves {
|
||||
get {
|
||||
return ResourceManager.GetString("Command_pal_updatesaves", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to You are NOT in a deep dungeon..
|
||||
/// </summary>
|
||||
@ -186,6 +176,43 @@ namespace Pal.Client.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Gold Coffer color.
|
||||
/// </summary>
|
||||
internal static string Config_GoldCoffer_Color {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_GoldCoffer_Color", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Draw filled.
|
||||
/// </summary>
|
||||
internal static string Config_GoldCoffer_Filled {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_GoldCoffer_Filled", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Show gold coffers on current floor.
|
||||
/// </summary>
|
||||
internal static string Config_GoldCoffer_Show {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_GoldCoffer_Show", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Shows nearby gold coffers (containing pomanders) on the current floor.
|
||||
///This is not synchronized with other players and not saved between floors/runs..
|
||||
/// </summary>
|
||||
internal static string Config_GoldCoffers_ToolTip {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_GoldCoffers_ToolTip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Hoard Coffer color.
|
||||
/// </summary>
|
||||
@ -367,7 +394,7 @@ namespace Pal.Client.Properties {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Shows all the silver coffers visible to you on the current floor.
|
||||
/// Looks up a localized string similar to Shows nearby silver coffers (gear upgrades and magicites) on the current floor.
|
||||
///This is not synchronized with other players and not saved between floors/runs..
|
||||
/// </summary>
|
||||
internal static string Config_SilverCoffers_ToolTip {
|
||||
@ -385,15 +412,6 @@ namespace Pal.Client.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Splatoon Test:.
|
||||
/// </summary>
|
||||
internal static string Config_Splatoon_Test {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_Splatoon_Test", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Start Export.
|
||||
/// </summary>
|
||||
@ -638,6 +656,15 @@ namespace Pal.Client.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Command could not be executed: {0}.
|
||||
/// </summary>
|
||||
internal static string Error_CommandFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("Error_CommandFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Please finish the initial setup first..
|
||||
/// </summary>
|
||||
@ -674,6 +701,15 @@ namespace Pal.Client.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Plugin could not be loaded: {0}.
|
||||
/// </summary>
|
||||
internal static string Error_LoadFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("Error_LoadFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Please install this plugin from the official repository at {0} to continue using it..
|
||||
/// </summary>
|
||||
|
@ -18,12 +18,46 @@
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<!-- Common -->
|
||||
<data name="PalaceOfTheDead" xml:space="preserve">
|
||||
<value>Palast der Toten</value>
|
||||
</data>
|
||||
<data name="HeavenOnHigh" xml:space="preserve">
|
||||
<value>Himmelssäule</value>
|
||||
</data>
|
||||
<data name="Save" xml:space="preserve">
|
||||
<value>Speichern</value>
|
||||
</data>
|
||||
<data name="SaveAndClose" xml:space="preserve">
|
||||
<value>Speichern & Schließen</value>
|
||||
</data>
|
||||
<!-- Generic Errors -->
|
||||
<!-- /pal commands -->
|
||||
<!-- Messages while connecting to the server. These are typically only visible when clicking 'Test connection'. -->
|
||||
<!-- Config Window -->
|
||||
<data name="ConnectionSuccessful" xml:space="preserve">
|
||||
<value>Verbindung erfolgreich.</value>
|
||||
</data>
|
||||
<data name="ConnectionError_NotOnline" xml:space="preserve">
|
||||
<value>Sie sind nicht online.</value>
|
||||
<comment>Shown if you attempt to connect to the server while you have selected 'never upload discoveries, only show traps and coffers i found myself'</comment>
|
||||
</data>
|
||||
<data name="ConnectionError_OldVersion" xml:space="preserve">
|
||||
<value>Ihre Version von Palace Pal ist veraltet, bitte aktualisieren Sie das Plugin mit Hilfe des Plugin Installers.</value>
|
||||
<comment>Shown if the version is too old to create an account or log in.</comment>
|
||||
</data>
|
||||
<!-- Config Window: Deep Dungeons -->
|
||||
<data name="ConfigTab_DeepDungeons" xml:space="preserve">
|
||||
<value>Tiefe Gewölbe</value>
|
||||
</data>
|
||||
<data name="Config_Traps_Show" xml:space="preserve">
|
||||
<value>Fallen anzeigen</value>
|
||||
</data>
|
||||
<!-- Config Window: Community -->
|
||||
<!-- Config Window: Import -->
|
||||
<!-- Config Window: Export -->
|
||||
<!-- Config Window: Renderer -->
|
||||
<!-- Config Window: Debug -->
|
||||
<!-- Statistics Window -->
|
||||
<!-- Agreement Window -->
|
||||
<!-- Import -->
|
||||
<!-- Import (chat messages) -->
|
||||
<!-- Other -->
|
||||
</root>
|
||||
|
@ -35,12 +35,12 @@
|
||||
<value>Enregistrer</value>
|
||||
</data>
|
||||
<data name="SaveAndClose" xml:space="preserve">
|
||||
<value>Sauvegarder et Fermer</value>
|
||||
<value>Enregistrer et Fermer</value>
|
||||
</data>
|
||||
<!-- Generic Errors -->
|
||||
<data name="Error_FirstTimeSetupRequired" xml:space="preserve">
|
||||
<value>Veuillez s'il vous plaît terminer la configuration initiale.</value>
|
||||
<comment>Before using any /pal command, the first-time setup/agreeement needs to be completed.</comment>
|
||||
<comment>Before using any /pal command, the initial setup/agreeement needs to be completed.</comment>
|
||||
</data>
|
||||
<data name="Error_WrongRepository" xml:space="preserve">
|
||||
<value>Veuillez installer ce plugin depuis le dépôt officiel {0} pour continuer à l'utiliser.</value>
|
||||
@ -61,10 +61,6 @@
|
||||
<value>Impossible de récupérer les statistiques.</value>
|
||||
<comment>Shown when /pal stats produces a server-side error, and the statistics window can't be loaded.</comment>
|
||||
</data>
|
||||
<data name="Command_pal_updatesaves" xml:space="preserve">
|
||||
<value>Mise à jour de tous les marqueurs du cache local vers la dernière version.</value>
|
||||
<comment>Shown after /pal update-saves was successful.</comment>
|
||||
</data>
|
||||
<!-- Messages while connecting to the server. These are typically only visible when clicking 'Test connection'. -->
|
||||
<data name="ConnectionSuccessful" xml:space="preserve">
|
||||
<value>Connexion réussie.</value>
|
||||
@ -100,7 +96,7 @@
|
||||
<data name="ConnectionError_CouldNotConnectToServer" xml:space="preserve">
|
||||
<value>Échec de connexion au serveur : {0}</value>
|
||||
</data>
|
||||
<!-- Config Window -->
|
||||
<!-- Config Window: Deep Dungeons -->
|
||||
<data name="ConfigTab_DeepDungeons" xml:space="preserve">
|
||||
<value>Donjons sans fonds</value>
|
||||
</data>
|
||||
@ -145,6 +141,7 @@ Il n'y a pas de synchronisation avec les autres joueurs ni de sauvegarde entre l
|
||||
<value>Remplir</value>
|
||||
<comment>Whether silver coffers should only be drawn with the circle outline or as filled circle.</comment>
|
||||
</data>
|
||||
<!-- Config Window: Community -->
|
||||
<data name="ConfigTab_Community" xml:space="preserve">
|
||||
<value>Communauté</value>
|
||||
</data>
|
||||
@ -155,6 +152,7 @@ Il n'y a pas de synchronisation avec les autres joueurs ni de sauvegarde entre l
|
||||
<value>Test en cours...</value>
|
||||
<comment>When clicking on the 'Test Connection' button, this is shown until a success/error message is available.</comment>
|
||||
</data>
|
||||
<!-- Config Window: Import -->
|
||||
<data name="ConfigTab_Import" xml:space="preserve">
|
||||
<value>Importer</value>
|
||||
</data>
|
||||
@ -192,6 +190,7 @@ Il n'y a pas de synchronisation avec les autres joueurs ni de sauvegarde entre l
|
||||
<data name="Config_UndoImport" xml:space="preserve">
|
||||
<value>Annuler l'importation</value>
|
||||
</data>
|
||||
<!-- Config Window: Export -->
|
||||
<data name="ConfigTab_Export" xml:space="preserve">
|
||||
<value>Exporter</value>
|
||||
</data>
|
||||
@ -204,6 +203,7 @@ Il n'y a pas de synchronisation avec les autres joueurs ni de sauvegarde entre l
|
||||
<data name="Config_StartExport" xml:space="preserve">
|
||||
<value>Démarrer l'exportation</value>
|
||||
</data>
|
||||
<!-- Config Window: Renderer -->
|
||||
<data name="ConfigTab_Renderer" xml:space="preserve">
|
||||
<value>Moteur de rendu</value>
|
||||
<comment>Configuration tab to select Splatoon or Simple as rendering backend</comment>
|
||||
@ -224,13 +224,11 @@ Il n'y a pas de synchronisation avec les autres joueurs ni de sauvegarde entre l
|
||||
<data name="Config_Renderer_Simple_Hint" xml:space="preserve">
|
||||
<value>Expérimental</value>
|
||||
</data>
|
||||
<data name="Config_Splatoon_Test" xml:space="preserve">
|
||||
<value>Test de Splatoon :</value>
|
||||
</data>
|
||||
<data name="Config_Splatoon_DrawCircles" xml:space="preserve">
|
||||
<value>Dessiner les marqueurs des pièges et coffres autour de soi</value>
|
||||
<comment>To test the Splatoon integration, you can draw markers around yourself.</comment>
|
||||
</data>
|
||||
<!-- Config Window: Debug -->
|
||||
<data name="ConfigTab_Debug" xml:space="preserve">
|
||||
<value>Débogage</value>
|
||||
</data>
|
||||
@ -289,9 +287,9 @@ Il n'y a pas de synchronisation avec les autres joueurs ni de sauvegarde entre l
|
||||
</data>
|
||||
<data name="Agreement_PickOneOption" xml:space="preserve">
|
||||
<value>Veuillez choisir une des options ci-dessous.</value>
|
||||
<comment>Shown if neither of the two radio buttons in the first-time setup window are selected.</comment>
|
||||
<comment>Shown if neither of the two radio buttons in the setup setup window are selected.</comment>
|
||||
</data>
|
||||
<!-- Import -->
|
||||
<!-- Import (chat messages) -->
|
||||
<data name="ImportCompleteStatistics" xml:space="preserve">
|
||||
<value>Importation de {0} nouvelles locations de pièges et {1} locations de trésors cachés.</value>
|
||||
<comment>After the import of a *.pal file, the number of traps/hoard coffers is shown as a summary.</comment>
|
||||
|
@ -40,7 +40,7 @@
|
||||
<!-- Generic Errors -->
|
||||
<data name="Error_FirstTimeSetupRequired" xml:space="preserve">
|
||||
<value>最初にセットアップを完了してください。</value>
|
||||
<comment>Before using any /pal command, the first-time setup/agreeement needs to be completed.</comment>
|
||||
<comment>Before using any /pal command, the initial setup/agreeement needs to be completed.</comment>
|
||||
</data>
|
||||
<data name="Error_WrongRepository" xml:space="preserve">
|
||||
<value>引き続き使用する場合は公式リポジトリ {0} からこのプラグインをインストールしてください。</value>
|
||||
@ -61,10 +61,6 @@
|
||||
<value>統計情報を取得できません。</value>
|
||||
<comment>Shown when /pal stats produces a server-side error, and the statistics window can't be loaded.</comment>
|
||||
</data>
|
||||
<data name="Command_pal_updatesaves" xml:space="preserve">
|
||||
<value>保存されたマーカーファイルを更新しました。</value>
|
||||
<comment>Shown after /pal update-saves was successful.</comment>
|
||||
</data>
|
||||
<!-- Messages while connecting to the server. These are typically only visible when clicking 'Test connection'. -->
|
||||
<data name="ConnectionSuccessful" xml:space="preserve">
|
||||
<value>接続に成功しました。</value>
|
||||
@ -100,7 +96,7 @@
|
||||
<data name="ConnectionError_CouldNotConnectToServer" xml:space="preserve">
|
||||
<value>サーバーに接続できません: {0}</value>
|
||||
</data>
|
||||
<!-- Config Window -->
|
||||
<!-- Config Window: Deep Dungeons -->
|
||||
<data name="ConfigTab_DeepDungeons" xml:space="preserve">
|
||||
<value>ディープダンジョン</value>
|
||||
</data>
|
||||
@ -146,6 +142,21 @@
|
||||
<value>塗りつぶす</value>
|
||||
<comment>Whether silver coffers should only be drawn with the circle outline or as filled circle.</comment>
|
||||
</data>
|
||||
<data name="Config_GoldCoffer_Show" xml:space="preserve">
|
||||
<value>金の宝箱を表示</value>
|
||||
</data>
|
||||
<data name="Config_GoldCoffers_ToolTip" xml:space="preserve">
|
||||
<value>現在のフロアにある全ての金の宝箱を表示します。
|
||||
これは他のプレイヤーと同期されず、データは保存されません。</value>
|
||||
</data>
|
||||
<data name="Config_GoldCoffer_Color" xml:space="preserve">
|
||||
<value>金の宝箱の色</value>
|
||||
</data>
|
||||
<data name="Config_GoldCoffer_Filled" xml:space="preserve">
|
||||
<value>塗りつぶす</value>
|
||||
<comment>Whether gold coffers should only be drawn with the circle outline or as filled circle.</comment>
|
||||
</data>
|
||||
<!-- Config Window: Community -->
|
||||
<data name="ConfigTab_Community" xml:space="preserve">
|
||||
<value>コミュニティ</value>
|
||||
</data>
|
||||
@ -156,6 +167,7 @@
|
||||
<value>接続テスト中...</value>
|
||||
<comment>When clicking on the 'Test Connection' button, this is shown until a success/error message is available.</comment>
|
||||
</data>
|
||||
<!-- Config Window: Import -->
|
||||
<data name="ConfigTab_Import" xml:space="preserve">
|
||||
<value>インポート</value>
|
||||
</data>
|
||||
@ -191,6 +203,7 @@
|
||||
<data name="Config_UndoImport" xml:space="preserve">
|
||||
<value>インポートを取り消す</value>
|
||||
</data>
|
||||
<!-- Config Window: Export -->
|
||||
<data name="ConfigTab_Export" xml:space="preserve">
|
||||
<value>エクスポート</value>
|
||||
</data>
|
||||
@ -203,6 +216,7 @@
|
||||
<data name="Config_StartExport" xml:space="preserve">
|
||||
<value>エクスポートの開始</value>
|
||||
</data>
|
||||
<!-- Config Window: Renderer -->
|
||||
<data name="ConfigTab_Renderer" xml:space="preserve">
|
||||
<value>レンダリング</value>
|
||||
<comment>Configuration tab to select Splatoon or Simple as rendering backend</comment>
|
||||
@ -223,13 +237,11 @@
|
||||
<data name="Config_Renderer_Simple_Hint" xml:space="preserve">
|
||||
<value>試験的機能</value>
|
||||
</data>
|
||||
<data name="Config_Splatoon_Test" xml:space="preserve">
|
||||
<value>Splatoonのテスト:</value>
|
||||
</data>
|
||||
<data name="Config_Splatoon_DrawCircles" xml:space="preserve">
|
||||
<value>自分の周りにトラップと宝箱を表示する</value>
|
||||
<comment>To test the Splatoon integration, you can draw markers around yourself.</comment>
|
||||
</data>
|
||||
<!-- Config Window: Debug -->
|
||||
<data name="ConfigTab_Debug" xml:space="preserve">
|
||||
<value>Debug</value>
|
||||
</data>
|
||||
@ -291,9 +303,9 @@
|
||||
</data>
|
||||
<data name="Agreement_PickOneOption" xml:space="preserve">
|
||||
<value>以下のいずれかのオプションを選択してください。</value>
|
||||
<comment>Shown if neither of the two radio buttons in the first-time setup window are selected.</comment>
|
||||
<comment>Shown if neither of the two radio buttons in the setup setup window are selected.</comment>
|
||||
</data>
|
||||
<!-- Import -->
|
||||
<!-- Import (chat messages) -->
|
||||
<data name="ImportCompleteStatistics" xml:space="preserve">
|
||||
<value>{0} 個の新しいトラップの場所と {1} 個の新しい宝箱の場所をインポートしました。</value>
|
||||
<comment>After the import of a *.pal file, the number of traps/hoard coffers is shown as a summary.</comment>
|
||||
|
@ -46,6 +46,10 @@
|
||||
<value>Please finish the initial setup first.</value>
|
||||
<comment>Before using any /pal command, the initial setup/agreeement needs to be completed.</comment>
|
||||
</data>
|
||||
<data name="Error_LoadFailed" xml:space="preserve">
|
||||
<value>Plugin could not be loaded: {0}</value>
|
||||
<comment>Shown when the plugin fails to load, with the placeholder filled with the exception message.</comment>
|
||||
</data>
|
||||
<data name="Error_WrongRepository" xml:space="preserve">
|
||||
<value>Please install this plugin from the official repository at {0} to continue using it.</value>
|
||||
</data>
|
||||
@ -66,11 +70,11 @@
|
||||
<value>Unable to fetch statistics.</value>
|
||||
<comment>Shown when /pal stats produces a server-side error, and the statistics window can't be loaded.</comment>
|
||||
</data>
|
||||
<data name="Command_pal_updatesaves" xml:space="preserve">
|
||||
<value>Updated all locally cached marker files to latest version.</value>
|
||||
<comment>Shown after /pal update-saves was successful.</comment>
|
||||
<data name="Error_CommandFailed" xml:space="preserve">
|
||||
<value>Command could not be executed: {0}</value>
|
||||
<comment>Shown when '/pal ...' fails, with the placeholder filled with the exception message.</comment>
|
||||
</data>
|
||||
|
||||
|
||||
<!-- Messages while connecting to the server. These are typically only visible when clicking 'Test connection'. -->
|
||||
<data name="ConnectionSuccessful" xml:space="preserve">
|
||||
<value>Connection successful.</value>
|
||||
@ -142,7 +146,7 @@ When using a Pomander of Safety, all traps are hidden.</value>
|
||||
<value>Show silver coffers on current floor</value>
|
||||
</data>
|
||||
<data name="Config_SilverCoffers_ToolTip" xml:space="preserve">
|
||||
<value>Shows all the silver coffers visible to you on the current floor.
|
||||
<value>Shows nearby silver coffers (gear upgrades and magicites) on the current floor.
|
||||
This is not synchronized with other players and not saved between floors/runs.</value>
|
||||
</data>
|
||||
<data name="Config_SilverCoffer_Color" xml:space="preserve">
|
||||
@ -152,6 +156,20 @@ This is not synchronized with other players and not saved between floors/runs.</
|
||||
<value>Draw filled</value>
|
||||
<comment>Whether silver coffers should only be drawn with the circle outline or as filled circle.</comment>
|
||||
</data>
|
||||
<data name="Config_GoldCoffer_Show" xml:space="preserve">
|
||||
<value>Show gold coffers on current floor</value>
|
||||
</data>
|
||||
<data name="Config_GoldCoffers_ToolTip" xml:space="preserve">
|
||||
<value>Shows nearby gold coffers (containing pomanders) on the current floor.
|
||||
This is not synchronized with other players and not saved between floors/runs.</value>
|
||||
</data>
|
||||
<data name="Config_GoldCoffer_Color" xml:space="preserve">
|
||||
<value>Gold Coffer color</value>
|
||||
</data>
|
||||
<data name="Config_GoldCoffer_Filled" xml:space="preserve">
|
||||
<value>Draw filled</value>
|
||||
<comment>Whether gold coffers should only be drawn with the circle outline or as filled circle.</comment>
|
||||
</data>
|
||||
|
||||
<!-- Config Window: Community -->
|
||||
<data name="ConfigTab_Community" xml:space="preserve">
|
||||
@ -239,9 +257,6 @@ This is not synchronized with other players and not saved between floors/runs.</
|
||||
<data name="Config_Renderer_Simple_Hint" xml:space="preserve">
|
||||
<value>experimental</value>
|
||||
</data>
|
||||
<data name="Config_Splatoon_Test" xml:space="preserve">
|
||||
<value>Splatoon Test:</value>
|
||||
</data>
|
||||
<data name="Config_Splatoon_DrawCircles" xml:space="preserve">
|
||||
<value>Draw trap & coffer circles around self</value>
|
||||
<comment>To test the Splatoon integration, you can draw markers around yourself.</comment>
|
||||
@ -325,6 +340,5 @@ This is not synchronized with other players and not saved between floors/runs.</
|
||||
<data name="Error_ImportFailed_InvalidFile" xml:space="preserve">
|
||||
<value>Import failed: Invalid file.</value>
|
||||
</data>
|
||||
|
||||
<!-- Other -->
|
||||
</root>
|
||||
|
21
Pal.Client/README.md
Normal file
21
Pal.Client/README.md
Normal file
@ -0,0 +1,21 @@
|
||||
# Palace Pal
|
||||
|
||||
## Client Build Notes
|
||||
|
||||
### Database Migrations
|
||||
|
||||
Since EF core needs all dll files to be present, including Dalamud ones,
|
||||
there's a special `EF` configuration that exempts them from setting
|
||||
`<Private>false</Private>` during the build.
|
||||
|
||||
To use with `dotnet ef` commands, specify it as `-c EF`, for example:
|
||||
|
||||
```shell
|
||||
dotnet ef migrations add MigrationName --configuration EF
|
||||
```
|
||||
|
||||
To rebuild the compiled model:
|
||||
|
||||
```shell
|
||||
dotnet ef dbcontext optimize --output-dir Database/Compiled --namespace Pal.Client.Database.Compiled --configuration EF
|
||||
```
|
@ -1,8 +1,8 @@
|
||||
namespace Pal.Client.Rendering
|
||||
namespace Pal.Client.Rendering;
|
||||
|
||||
internal enum ELayer
|
||||
{
|
||||
internal enum ELayer
|
||||
{
|
||||
TrapHoard,
|
||||
RegularCoffers,
|
||||
}
|
||||
TrapHoard,
|
||||
RegularCoffers,
|
||||
Test,
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace Pal.Client.Rendering
|
||||
{
|
||||
internal interface IDrawDebugItems
|
||||
{
|
||||
void DrawDebugItems(Vector4 trapColor, Vector4 hoardColor);
|
||||
}
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
namespace Pal.Client.Rendering
|
||||
{
|
||||
public interface IRenderElement
|
||||
{
|
||||
bool IsValid { get; }
|
||||
namespace Pal.Client.Rendering;
|
||||
|
||||
uint Color { get; set; }
|
||||
}
|
||||
public interface IRenderElement
|
||||
{
|
||||
bool IsValid { get; }
|
||||
|
||||
bool Enabled { get; set; }
|
||||
}
|
||||
|
@ -1,19 +1,19 @@
|
||||
using ImGuiNET;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Pal.Client.Configuration;
|
||||
using Pal.Client.Floors;
|
||||
|
||||
namespace Pal.Client.Rendering
|
||||
namespace Pal.Client.Rendering;
|
||||
|
||||
internal interface IRenderer
|
||||
{
|
||||
internal interface IRenderer
|
||||
{
|
||||
void SetLayer(ELayer layer, IReadOnlyList<IRenderElement> elements);
|
||||
ERenderer GetConfigValue();
|
||||
|
||||
void ResetLayer(ELayer layer);
|
||||
void SetLayer(ELayer layer, IReadOnlyList<IRenderElement> elements);
|
||||
|
||||
IRenderElement CreateElement(Marker.EType type, Vector3 pos, uint color, bool fill = false);
|
||||
}
|
||||
void ResetLayer(ELayer layer);
|
||||
|
||||
IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, bool enabled, uint color, bool fill = false);
|
||||
|
||||
void DrawDebugItems(uint trapColor, uint hoardColor);
|
||||
}
|
||||
|
@ -1,20 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
using Pal.Client.Floors;
|
||||
|
||||
namespace Pal.Client.Rendering
|
||||
namespace Pal.Client.Rendering;
|
||||
|
||||
internal sealed class MarkerConfig
|
||||
{
|
||||
internal class MarkerConfig
|
||||
private static readonly MarkerConfig EmptyConfig = new();
|
||||
|
||||
private static readonly Dictionary<MemoryLocation.EType, MarkerConfig> MarkerConfigs = new()
|
||||
{
|
||||
private static readonly MarkerConfig EmptyConfig = new();
|
||||
private static readonly Dictionary<Marker.EType, MarkerConfig> MarkerConfigs = new()
|
||||
{
|
||||
{ Marker.EType.Trap, new MarkerConfig { Radius = 1.7f } },
|
||||
{ Marker.EType.Hoard, new MarkerConfig { Radius = 1.7f, OffsetY = -0.03f } },
|
||||
{ Marker.EType.SilverCoffer, new MarkerConfig { Radius = 1f } },
|
||||
};
|
||||
{ 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; set; }
|
||||
public float Radius { get; set; } = 0.25f;
|
||||
public float OffsetY { get; private init; }
|
||||
public float Radius { get; private init; } = 0.25f;
|
||||
|
||||
public static MarkerConfig ForType(Marker.EType type) => MarkerConfigs.GetValueOrDefault(type, EmptyConfig);
|
||||
}
|
||||
public static MarkerConfig ForType(MemoryLocation.EType type) =>
|
||||
MarkerConfigs.GetValueOrDefault(type, EmptyConfig);
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user