Compare commits
130 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 | |||
b81575de84 | |||
2a32cf5c8d | |||
8857b93a19 | |||
f9cbf0494a | |||
a39eaa11f1 | |||
9c0699bf2c | |||
3c3ebba645 | |||
ea9feb9599 | |||
58e0e8d44c | |||
16bc4fc34f | |||
2bfcfee753 | |||
12de110b04 | |||
0bec8cf759 | |||
c7b8e69732 | |||
3cd75ff9a5 |
@ -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
|
28
.github/workflows/server.yml
vendored
28
.github/workflows/server.yml
vendored
@ -1,28 +0,0 @@
|
||||
name: Server
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'Pal.Common/**'
|
||||
- 'Pal.Server/**'
|
||||
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>
|
16
Dockerfile
16
Dockerfile
@ -1,13 +1,19 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build-env
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env
|
||||
ARG TARGETARCH
|
||||
WORKDIR /build
|
||||
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 -c Release -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=
|
||||
|
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 Vector4(1, 0, 0, 0.4f);
|
||||
public bool OnlyVisibleTrapsAfterPomander { get; set; } = true;
|
||||
|
||||
public bool ShowHoard { get; set; } = true;
|
||||
public Vector4 HoardColor { get; set; } = new Vector4(0, 1, 1, 0.4f);
|
||||
public bool OnlyVisibleHoardAfterPomander { get; set; } = true;
|
||||
|
||||
public bool ShowSilverCoffers { get; set; } = false;
|
||||
public Vector4 SilverCofferColor { get; set; } = new Vector4(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
|
||||
}
|
@ -15,6 +15,7 @@
|
||||
OutputPath="$(OutputPath)"
|
||||
AssemblyName="$(AssemblyName)"
|
||||
MakeZip="true"
|
||||
VersionComponents="2"/>
|
||||
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,10 +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}");
|
||||
}
|
12
Pal.Client/Extensions/GuidExtensions.cs
Normal file
12
Pal.Client/Extensions/GuidExtensions.cs
Normal file
@ -0,0 +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();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
12
Pal.Client/ILanguageChanged.cs
Normal file
12
Pal.Client/ILanguageChanged.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Pal.Client;
|
||||
|
||||
internal interface ILanguageChanged
|
||||
{
|
||||
void LanguageChanged();
|
||||
}
|
@ -1,154 +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;
|
||||
|
||||
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.PadRight(14).Substring(0, 13)).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 NullScope();
|
||||
|
||||
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,14 +1,12 @@
|
||||
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
|
||||
{
|
||||
internal class JwtClaims
|
||||
namespace Pal.Client.Net;
|
||||
|
||||
internal sealed class JwtClaims
|
||||
{
|
||||
[JsonPropertyName("nameid")]
|
||||
public Guid NameId { get; set; }
|
||||
@ -42,13 +40,14 @@ namespace Pal.Client.Net
|
||||
payload += "=";
|
||||
|
||||
string content = Encoding.UTF8.GetString(Convert.FromBase64String(payload));
|
||||
return JsonSerializer.Deserialize<JwtClaims>(content) ?? throw new InvalidOperationException("token deserialization returned null");
|
||||
return JsonSerializer.Deserialize<JwtClaims>(content) ??
|
||||
throw new InvalidOperationException("token deserialization returned null");
|
||||
}
|
||||
}
|
||||
|
||||
internal class JwtRoleConverter : JsonConverter<List<string>>
|
||||
internal sealed class JwtRoleConverter : JsonConverter<List<string>>
|
||||
{
|
||||
public override List<string>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
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") };
|
||||
@ -75,21 +74,6 @@ namespace Pal.Client.Net
|
||||
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();
|
||||
}
|
||||
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,34 +1,53 @@
|
||||
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
|
||||
{
|
||||
private async Task<(bool Success, string Error)> TryConnect(CancellationToken cancellationToken, ILoggerFactory? loggerFactory = null, bool retry = true)
|
||||
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)
|
||||
{
|
||||
if (Service.Configuration.Mode != Configuration.EMode.Online)
|
||||
{
|
||||
PluginLog.Debug("TryConnect: Not Online, not attempting to establish a connection");
|
||||
return (false, Localization.ConnectionError_NotOnline);
|
||||
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);
|
||||
}
|
||||
|
||||
if (_channel == null || !(_channel.State == ConnectivityState.Ready || _channel.State == ConnectivityState.Idle))
|
||||
private async Task<(bool Success, string Error, bool ShouldRetry)> TryConnectImpl(
|
||||
CancellationToken cancellationToken,
|
||||
ILoggerFactory? loggerFactory)
|
||||
{
|
||||
if (_configuration.Mode != EMode.Online)
|
||||
{
|
||||
_logger.LogDebug("Not Online, not attempting to establish a connection");
|
||||
return (false, Localization.ConnectionError_NotOnline, false);
|
||||
}
|
||||
|
||||
if (_channel == null ||
|
||||
!(_channel.State == ConnectivityState.Ready || _channel.State == ConnectivityState.Idle))
|
||||
{
|
||||
Dispose();
|
||||
|
||||
PluginLog.Information("TryConnect: Creating new gRPC channel");
|
||||
_logger.LogInformation("Creating new gRPC channel");
|
||||
_channel = GrpcChannel.ForAddress(RemoteUrl, new GrpcChannelOptions
|
||||
{
|
||||
HttpHandler = new SocketsHttpHandler
|
||||
@ -39,91 +58,141 @@ namespace Pal.Client.Net
|
||||
LoggerFactory = loggerFactory,
|
||||
});
|
||||
|
||||
PluginLog.Information($"TryConnect: Connecting to upstream service at {RemoteUrl}");
|
||||
_logger.LogInformation("Connecting to upstream service at {Url}", RemoteUrl);
|
||||
await _channel.ConnectAsync(cancellationToken);
|
||||
}
|
||||
|
||||
var accountClient = new AccountService.AccountServiceClient(_channel);
|
||||
if (AccountId == null)
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
_logger.LogTrace("Acquiring connect lock");
|
||||
await _connectLock.WaitAsync(cancellationToken);
|
||||
_logger.LogTrace("Obtained connect lock");
|
||||
|
||||
try
|
||||
{
|
||||
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);
|
||||
var accountClient = new AccountService.AccountServiceClient(_channel);
|
||||
IAccountConfiguration? configuredAccount = _configuration.FindAccount(RemoteUrl);
|
||||
if (configuredAccount == null)
|
||||
{
|
||||
_logger.LogInformation("No account information saved for {Url}, creating new account", RemoteUrl);
|
||||
var createAccountReply = await accountClient.CreateAccountAsync(new CreateAccountRequest
|
||||
{
|
||||
Version = new()
|
||||
{
|
||||
Major = PluginVersion.Major,
|
||||
Minor = PluginVersion.Minor,
|
||||
},
|
||||
},
|
||||
headers: UnauthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(10),
|
||||
cancellationToken: cancellationToken);
|
||||
if (createAccountReply.Success)
|
||||
{
|
||||
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);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_logger.LogTrace("Releasing connectLock");
|
||||
_connectLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> Connect(CancellationToken cancellationToken)
|
||||
@ -134,21 +203,24 @@ namespace Pal.Client.Net
|
||||
|
||||
public async Task<string> VerifyConnection(CancellationToken cancellationToken = default)
|
||||
{
|
||||
using IDisposable? logScope = _logger.BeginScope("VerifyConnection");
|
||||
|
||||
_warnedAboutUpgrade = false;
|
||||
|
||||
var connectionResult = await TryConnect(cancellationToken, loggerFactory: _grpcToPluginLogLoggerFactory);
|
||||
var connectionResult = await TryConnect(cancellationToken, loggerFactory: _loggerFactory);
|
||||
if (!connectionResult.Success)
|
||||
return string.Format(Localization.ConnectionError_CouldNotConnectToServer, connectionResult.Error);
|
||||
|
||||
PluginLog.Information("VerifyConnection: Connection established, trying to verify auth token");
|
||||
_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);
|
||||
await accountClient.VerifyAsync(new VerifyRequest(), headers: AuthorizedHeaders(),
|
||||
deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken);
|
||||
|
||||
PluginLog.Information("VerifyConnection: Verification returned no errors.");
|
||||
_logger.LogInformation("Verification returned no errors.");
|
||||
return Localization.ConnectionSuccessful;
|
||||
}
|
||||
|
||||
internal class LoginInfo
|
||||
internal sealed class LoginInfo
|
||||
{
|
||||
public LoginInfo(string? authToken)
|
||||
{
|
||||
@ -156,7 +228,7 @@ namespace Pal.Client.Net
|
||||
{
|
||||
IsLoggedIn = true;
|
||||
AuthToken = authToken;
|
||||
Claims = JwtClaims.FromAuthToken(authToken!);
|
||||
Claims = JwtClaims.FromAuthToken(authToken);
|
||||
}
|
||||
else
|
||||
IsLoggedIn = false;
|
||||
@ -165,10 +237,12 @@ namespace Pal.Client.Net
|
||||
public bool IsLoggedIn { get; }
|
||||
public string? AuthToken { get; }
|
||||
public JwtClaims? Claims { get; }
|
||||
public DateTimeOffset ExpiresAt => Claims?.ExpiresAt.Subtract(TimeSpan.FromMinutes(5)) ?? DateTimeOffset.MinValue;
|
||||
|
||||
private DateTimeOffset ExpiresAt =>
|
||||
Claims?.ExpiresAt.Subtract(TimeSpan.FromMinutes(5)) ?? DateTimeOffset.MinValue;
|
||||
|
||||
public bool IsExpired => ExpiresAt < DateTimeOffset.UtcNow;
|
||||
|
||||
public bool IsValid => IsLoggedIn && !IsExpired;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
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
|
||||
{
|
||||
public async Task<(bool, ExportRoot)> DoExport(CancellationToken cancellationToken = default)
|
||||
@ -16,8 +16,8 @@ namespace Pal.Client.Net
|
||||
var exportReply = await exportClient.ExportAsync(new ExportRequest
|
||||
{
|
||||
ServerUrl = RemoteUrl,
|
||||
}, headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(120), cancellationToken: cancellationToken);
|
||||
}, headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(120),
|
||||
cancellationToken: cancellationToken);
|
||||
return (exportReply.Success, exportReply.Data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,29 +1,34 @@
|
||||
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
|
||||
{
|
||||
public async Task<(bool, List<Marker>)> DownloadRemoteMarkers(ushort territoryId, CancellationToken cancellationToken = default)
|
||||
public async Task<(bool, List<PersistentLocation>)> DownloadRemoteMarkers(ushort territoryId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!await Connect(cancellationToken))
|
||||
return (false, new());
|
||||
|
||||
var palaceClient = new PalaceService.PalaceServiceClient(_channel);
|
||||
var downloadReply = await palaceClient.DownloadFloorsAsync(new DownloadFloorsRequest { TerritoryType = territoryId }, headers: AuthorizedHeaders(), cancellationToken: cancellationToken);
|
||||
return (downloadReply.Success, downloadReply.Objects.Select(o => CreateMarkerFromNetworkObject(o)).ToList());
|
||||
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<Marker>)> UploadMarker(ushort territoryType, IList<Marker> markers, CancellationToken cancellationToken = default)
|
||||
public async Task<(bool, List<PersistentLocation>)> UploadLocations(ushort territoryType,
|
||||
IReadOnlyList<PersistentLocation> locations, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (markers.Count == 0)
|
||||
if (locations.Count == 0)
|
||||
return (true, new());
|
||||
|
||||
if (!await Connect(cancellationToken))
|
||||
@ -34,20 +39,22 @@ namespace Pal.Client.Net
|
||||
{
|
||||
TerritoryType = territoryType,
|
||||
};
|
||||
uploadRequest.Objects.AddRange(markers.Select(m => new PalaceObject
|
||||
uploadRequest.Objects.AddRange(locations.Select(m => new PalaceObject
|
||||
{
|
||||
Type = (ObjectType)m.Type,
|
||||
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(o => CreateMarkerFromNetworkObject(o)).ToList());
|
||||
var uploadReply = await palaceClient.UploadFloorsAsync(uploadRequest, headers: AuthorizedHeaders(),
|
||||
cancellationToken: cancellationToken);
|
||||
return (uploadReply.Success, uploadReply.Objects.Select(CreateLocationFromNetworkObject).ToList());
|
||||
}
|
||||
|
||||
public async Task<bool> MarkAsSeen(ushort territoryType, IList<Marker> markers, CancellationToken cancellationToken = default)
|
||||
public async Task<bool> MarkAsSeen(ushort territoryType, IReadOnlyList<PersistentLocation> locations,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (markers.Count == 0)
|
||||
if (locations.Count == 0)
|
||||
return true;
|
||||
|
||||
if (!await Connect(cancellationToken))
|
||||
@ -55,15 +62,24 @@ namespace Pal.Client.Net
|
||||
|
||||
var palaceClient = new PalaceService.PalaceServiceClient(_channel);
|
||||
var seenRequest = new MarkObjectsSeenRequest { TerritoryType = territoryType };
|
||||
foreach (var marker in markers)
|
||||
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);
|
||||
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));
|
||||
private PersistentLocation CreateLocationFromNetworkObject(PalaceObject obj)
|
||||
{
|
||||
return new PersistentLocation
|
||||
{
|
||||
Type = obj.Type.ToMemoryType(),
|
||||
Position = new Vector3(obj.X, obj.Y, obj.Z),
|
||||
NetworkId = Guid.Parse(obj.NetworkId),
|
||||
Source = ClientLocation.ESource.Download,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<(bool, List<FloorStatistics>)> FetchStatistics(CancellationToken cancellationToken = default)
|
||||
{
|
||||
@ -71,8 +87,9 @@ namespace Pal.Client.Net
|
||||
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);
|
||||
var statisticsReply = await palaceClient.FetchStatisticsAsync(new StatisticsRequest(),
|
||||
headers: AuthorizedHeaders(), deadline: DateTime.UtcNow.AddSeconds(30),
|
||||
cancellationToken: cancellationToken);
|
||||
return (statisticsReply.Success, statisticsReply.FloorStatistics.ToList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,22 @@
|
||||
using Dalamud.Logging;
|
||||
using Grpc.Core;
|
||||
using System;
|
||||
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
|
||||
{
|
||||
private Metadata UnauthorizedHeaders() => new Metadata
|
||||
private Metadata UnauthorizedHeaders() => new()
|
||||
{
|
||||
{ "User-Agent", _userAgent },
|
||||
};
|
||||
|
||||
private Metadata AuthorizedHeaders() => new Metadata
|
||||
private Metadata AuthorizedHeaders() => new()
|
||||
{
|
||||
{ "Authorization", $"Bearer {_loginInfo?.AuthToken}" },
|
||||
{ "Authorization", $"Bearer {_loginInfo.AuthToken}" },
|
||||
{ "User-Agent", _userAgent },
|
||||
};
|
||||
|
||||
@ -34,10 +36,12 @@ namespace Pal.Client.Net
|
||||
return null;
|
||||
|
||||
var bytes = new byte[manifestResourceStream.Length];
|
||||
manifestResourceStream.Read(bytes, 0, bytes.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()}");
|
||||
_logger.LogDebug("Using client certificate {CertificateHash}", certificate.GetCertHashString());
|
||||
return new SslClientAuthenticationOptions
|
||||
{
|
||||
ClientCertificates = new X509CertificateCollection()
|
||||
@ -46,18 +50,8 @@ namespace Pal.Client.Net
|
||||
},
|
||||
};
|
||||
#else
|
||||
PluginLog.Debug("Not using client certificate");
|
||||
_logger.LogDebug("Not using client certificate");
|
||||
return null;
|
||||
#endif
|
||||
}
|
||||
|
||||
public bool HasRoleOnCurrentServer(string role)
|
||||
{
|
||||
if (Service.Configuration.Mode != Configuration.EMode.Online)
|
||||
return false;
|
||||
|
||||
var account = Account;
|
||||
return account == null || account.CachedRoles.Contains(role);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,48 +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.Configuration;
|
||||
using Pal.Client.DependencyInjection;
|
||||
|
||||
namespace Pal.Client.Net
|
||||
{
|
||||
internal partial class RemoteApi : IDisposable
|
||||
namespace Pal.Client.Net;
|
||||
|
||||
internal sealed 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 = false;
|
||||
private bool _warnedAboutUpgrade;
|
||||
|
||||
public Configuration.AccountInfo? Account
|
||||
public RemoteApi(
|
||||
ILoggerFactory loggerFactory,
|
||||
ILogger<RemoteApi> logger,
|
||||
Chat chat,
|
||||
ConfigurationManager configurationManager,
|
||||
IPalacePalConfiguration configuration)
|
||||
{
|
||||
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);
|
||||
_loggerFactory = loggerFactory;
|
||||
_logger = logger;
|
||||
_chat = chat;
|
||||
_configurationManager = configurationManager;
|
||||
_configuration = configuration;
|
||||
}
|
||||
}
|
||||
|
||||
public Guid? AccountId => Account?.Id;
|
||||
|
||||
public string? PartialAccountId => Account?.Id?.ToString()?.PadRight(14).Substring(0, 13);
|
||||
|
||||
private string FormattedPartialAccountId => PartialAccountId ?? "[no account id]";
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
PluginLog.Debug("Disposing gRPC channel");
|
||||
_logger.LogDebug("Disposing gRPC channel");
|
||||
_channel?.Dispose();
|
||||
_channel = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<Project Sdk="Dalamud.NET.Sdk/9.0.2">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<LangVersion>11.0</LangVersion>
|
||||
<Version>2.12</Version>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<Version>6.0</Version>
|
||||
<AssemblyName>Palace Pal</AssemblyName>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="..\vendor\LLib\LLib.targets"/>
|
||||
<Import Project="..\vendor\LLib\RenameZip.targets"/>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
<OutputPath>dist</OutputPath>
|
||||
<DebugType>none</DebugType>
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition="'$(Configuration)' == 'Release' And Exists('Certificate.pfx')">
|
||||
@ -28,27 +20,29 @@
|
||||
|
||||
<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="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">
|
||||
<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.Extensions.Logging" Version="7.0.0" />
|
||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="7.0.0" />
|
||||
<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>
|
||||
<ProjectReference Include="..\Pal.Common\Pal.Common.csproj"/>
|
||||
<ProjectReference Include="..\vendor\ECommons\ECommons\ECommons.csproj"/>
|
||||
<ProjectReference Include="..\vendor\LLib\LLib.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -58,38 +52,10 @@
|
||||
</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>
|
||||
<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>
|
||||
@ -97,8 +63,7 @@
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="RenameLatestZip" AfterTargets="PackagePlugin">
|
||||
<Exec Command="rename "$(OutDir)$(AssemblyName)\latest.zip" "$(AssemblyName)-$(Version).zip"" />
|
||||
<Target Name="Clean">
|
||||
<RemoveDir Directories="dist"/>
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
@ -2,8 +2,13 @@
|
||||
"Name": "Palace Pal",
|
||||
"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. Requires Splatoon to be installed.",
|
||||
"RepoUrl": "https://github.com/carvelli/PalacePal",
|
||||
"IconUrl": "https://raw.githubusercontent.com/carvelli/Dalamud-Plugins/master/dist/Palace Pal.png",
|
||||
"Tags": [ "potd", "palace", "hoh", "splatoon" ]
|
||||
"Description": "Shows possible trap & hoard coffer locations in Palace of the Dead & Heaven on High.\n\nThe default configuration requires Splatoon to be installed. If you do not wish to install Splatoon, you can switch to the experimental 'Simple' renderer in the configuration.",
|
||||
"RepoUrl": "https://git.carvel.li/liza/PalacePal",
|
||||
"IconUrl": "https://plugins.carvel.li/icons/PalacePal.png",
|
||||
"Tags": [
|
||||
"potd",
|
||||
"palace",
|
||||
"hoh",
|
||||
"splatoon"
|
||||
]
|
||||
}
|
@ -1,697 +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 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; }
|
||||
public SyncState TerritorySyncState { get; set; }
|
||||
public PomanderState PomanderOfSight { get; set; } = PomanderState.Inactive;
|
||||
public PomanderState PomanderOfIntuition { get; set; } = PomanderState.Inactive;
|
||||
public 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)
|
||||
{
|
||||
LanguageChanged(pluginInterface.UiLanguage);
|
||||
|
||||
PluginLog.Information($"Install source: {pluginInterface.SourceRepository}");
|
||||
|
||||
#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/"))
|
||||
{
|
||||
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 += OnOpenConfigUi;
|
||||
pluginInterface.LanguageChanged += LanguageChanged;
|
||||
Service.Framework.Update += OnFrameworkUpdate;
|
||||
Service.Chat.ChatMessage += OnChatMessage;
|
||||
Service.CommandManager.AddHandler("/pal", new CommandInfo(OnCommand)
|
||||
_commandManager.AddHandler("/pal", new CommandInfo(OnCommand)
|
||||
{
|
||||
HelpMessage = Localization.Command_pal_HelpText
|
||||
});
|
||||
|
||||
ReloadLanguageStrings();
|
||||
// Using TickScheduler requires ECommons to at least be partially initialized
|
||||
// ECommonsMain.Dispose leaves this untouched.
|
||||
Svc.Init(pluginInterface);
|
||||
|
||||
Task.Run(async () => await CreateDependencyContext());
|
||||
}
|
||||
|
||||
public void OnOpenConfigUi()
|
||||
private async Task CreateDependencyContext()
|
||||
{
|
||||
Window? configWindow;
|
||||
if (Service.Configuration.FirstUse)
|
||||
configWindow = Service.WindowSystem.GetWindow<AgreementWindow>();
|
||||
else
|
||||
configWindow = Service.WindowSystem.GetWindow<ConfigWindow>();
|
||||
try
|
||||
{
|
||||
_dependencyInjectionContext = _pluginInterface.Create<DependencyInjectionContext>(this)
|
||||
?? throw new Exception("Could not create DI root context class");
|
||||
var serviceProvider = _dependencyInjectionContext.BuildServiceContainer();
|
||||
_initCts.Token.ThrowIfCancellationRequested();
|
||||
|
||||
if (configWindow != null)
|
||||
configWindow.IsOpen = true;
|
||||
_logger = serviceProvider.GetRequiredService<ILogger<Plugin>>();
|
||||
_windowSystem = serviceProvider.GetRequiredService<WindowSystem>();
|
||||
_rootScope = serviceProvider.CreateScope();
|
||||
|
||||
var loader = _rootScope.ServiceProvider.GetRequiredService<DependencyContextInitializer>();
|
||||
await loader.InitializeAsync(_initCts.Token);
|
||||
|
||||
await _framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
_pluginInterface.UiBuilder.Draw += Draw;
|
||||
_pluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi;
|
||||
_pluginInterface.LanguageChanged += LanguageChanged;
|
||||
_clientState.Login += Login;
|
||||
});
|
||||
_rootScopeCompletionSource.SetResult(_rootScope);
|
||||
_loadState = ELoadState.Loaded;
|
||||
}
|
||||
catch (Exception e) when (e is ObjectDisposedException
|
||||
or OperationCanceledException
|
||||
or RepoVerification.RepoVerificationFailedException
|
||||
|| (e is FileLoadException && _pluginInterface.IsDev))
|
||||
{
|
||||
_rootScopeCompletionSource.SetException(e);
|
||||
_loadState = ELoadState.Error;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_rootScopeCompletionSource.SetException(e);
|
||||
_logger.LogError(e, "Async load failed");
|
||||
ShowErrorOnLogin(() =>
|
||||
new Chat(_chatGui).Error(string.Format(Localization.Error_LoadFailed,
|
||||
$"{e.GetType()} - {e.Message}")));
|
||||
|
||||
_loadState = ELoadState.Error;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowErrorOnLogin(Action? loginAction)
|
||||
{
|
||||
if (_clientState.IsLoggedIn)
|
||||
{
|
||||
loginAction?.Invoke();
|
||||
_loginAction = null;
|
||||
}
|
||||
else
|
||||
_loginAction = loginAction;
|
||||
}
|
||||
|
||||
private void Login()
|
||||
{
|
||||
_loginAction?.Invoke();
|
||||
_loginAction = null;
|
||||
}
|
||||
|
||||
private void OnCommand(string command, string arguments)
|
||||
{
|
||||
if (Service.Configuration.FirstUse)
|
||||
{
|
||||
Service.Chat.PalError(Localization.Error_FirstTimeSetupRequired);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
arguments = arguments.Trim();
|
||||
switch (arguments)
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
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(m => 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 -= OnOpenConfigUi;
|
||||
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;
|
||||
}
|
||||
else
|
||||
return;
|
||||
}
|
||||
|
||||
private void LanguageChanged(string langcode)
|
||||
=> Localization.Culture = new CultureInfo(langcode);
|
||||
|
||||
private void OnFrameworkUpdate(Framework framework)
|
||||
{
|
||||
if (Service.Configuration.FirstUse)
|
||||
return;
|
||||
IServiceScope rootScope;
|
||||
Chat chat;
|
||||
|
||||
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);
|
||||
rootScope = await _rootScopeCompletionSource.Task;
|
||||
chat = rootScope.ServiceProvider.GetRequiredService<Chat>();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugMessage = $"{DateTime.Now}\n{e}";
|
||||
_logger.LogError(e, "Could not wait for command root scope");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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.NetworkId != null && !knownMarker.RemoteSeenRequested && !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)
|
||||
IPalacePalConfiguration configuration =
|
||||
rootScope.ServiceProvider.GetRequiredService<IPalacePalConfiguration>();
|
||||
if (configuration.FirstUse && arguments != "" && arguments != "config")
|
||||
{
|
||||
uint desiredColor = DetermineColor(marker, visibleMarkers);
|
||||
if (marker.RenderElement == null || !marker.RenderElement.IsValid)
|
||||
{
|
||||
recreateLayout = true;
|
||||
break;
|
||||
chat.Error(Localization.Error_FirstTimeSetupRequired);
|
||||
return;
|
||||
}
|
||||
|
||||
if (marker.RenderElement.Color != desiredColor)
|
||||
marker.RenderElement.Color = desiredColor;
|
||||
}
|
||||
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)
|
||||
{
|
||||
DebugMessage = $"{DateTime.Now}\n{e}";
|
||||
recreateLayout = true;
|
||||
_logger.LogError(e, "Could not execute command '{Command}' with arguments '{Arguments}'", command,
|
||||
arguments);
|
||||
chat.Error(string.Format(Localization.Error_CommandFailed,
|
||||
$"{e.GetType()} - {e.Message}"));
|
||||
}
|
||||
}
|
||||
|
||||
if (updateSeenMarkers && partialAccountId != null)
|
||||
{
|
||||
var markersToUpdate = currentFloorMarkers.Where(x => x.Seen && x.NetworkId != null && !x.RemoteSeenRequested && !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.WasImported && marker.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 void OpenConfigUi()
|
||||
=> _rootScope!.ServiceProvider.GetRequiredService<PalConfigCommand>().Execute();
|
||||
|
||||
private async Task SyncSeenMarkersForTerritory(ushort territoryId, List<Marker> markersToUpdate)
|
||||
private void LanguageChanged(string languageCode)
|
||||
{
|
||||
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
|
||||
_logger.LogInformation("Language set to '{Language}'", languageCode);
|
||||
|
||||
#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 = 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?.ToString()?.Substring(0, 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();
|
||||
Localization.Culture = new CultureInfo(languageCode);
|
||||
_windowSystem!.Windows.OfType<ILanguageChanged>()
|
||||
.Each(w => w.LanguageChanged());
|
||||
}
|
||||
|
||||
private void Draw()
|
||||
{
|
||||
if (Renderer is SimpleRenderer sr)
|
||||
sr.DrawLayers();
|
||||
|
||||
Service.WindowSystem.Draw();
|
||||
_rootScope!.ServiceProvider.GetRequiredService<RenderAdapter>().DrawLayers();
|
||||
_windowSystem!.Draw();
|
||||
}
|
||||
|
||||
private string GetLocalizedString(uint id)
|
||||
public void Dispose()
|
||||
{
|
||||
return Service.DataManager.GetExcelSheet<LogMessage>()?.GetRow(id)?.Text?.ToString() ?? "Unknown";
|
||||
_commandManager.RemoveHandler("/pal");
|
||||
|
||||
if (_loadState == ELoadState.Loaded)
|
||||
{
|
||||
_pluginInterface.UiBuilder.Draw -= Draw;
|
||||
_pluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi;
|
||||
_pluginInterface.LanguageChanged -= LanguageChanged;
|
||||
_clientState.Login -= Login;
|
||||
}
|
||||
|
||||
public enum PomanderState
|
||||
{
|
||||
Inactive,
|
||||
Active,
|
||||
FoundOnCurrentFloor,
|
||||
PomanderOfSafetyUsed,
|
||||
_initCts.Cancel();
|
||||
_rootScope?.Dispose();
|
||||
_dependencyInjectionContext?.Dispose();
|
||||
}
|
||||
|
||||
private class LocalizedChatMessages
|
||||
private enum ELoadState
|
||||
{
|
||||
public string MapRevealed { get; set; } = "???"; //"The map for this floor has been revealed!";
|
||||
public string AllTrapsRemoved { get; set; } = "???"; // "All the traps on this floor have disappeared!";
|
||||
public string HoardOnCurrentFloor { get; set; } = "???"; // "You sense the Accursed Hoard calling you...";
|
||||
public string HoardNotOnCurrentFloor { get; set; } = "???"; // "You do not sense the call of the Accursed Hoard on this floor...";
|
||||
public string HoardCofferOpened { get; set; } = "???"; // "You discover a piece of the Accursed Hoard!";
|
||||
public Regex FloorChanged { get; set; } = new Regex(@"This isn't a game message, but will be replaced"); // new Regex(@"^Floor (\d+)$");
|
||||
}
|
||||
Initializing,
|
||||
Loaded,
|
||||
Error
|
||||
}
|
||||
}
|
||||
|
99
Pal.Client/Properties/Localization.Designer.cs
generated
99
Pal.Client/Properties/Localization.Designer.cs
generated
@ -18,7 +18,7 @@ namespace Pal.Client.Properties {
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Localization {
|
||||
@ -114,7 +114,7 @@ namespace Pal.Client.Properties {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Open the configuration/debug window.
|
||||
/// Looks up a localized string similar to Open the configuration window.
|
||||
/// </summary>
|
||||
internal static string Command_pal_HelpText {
|
||||
get {
|
||||
@ -149,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>
|
||||
@ -185,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>
|
||||
@ -320,6 +348,15 @@ namespace Pal.Client.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Path to *.pal file.
|
||||
/// </summary>
|
||||
internal static string Config_SelectImportFile_Hint {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_SelectImportFile_Hint", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Select which render backend to use for markers:.
|
||||
/// </summary>
|
||||
@ -357,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 {
|
||||
@ -375,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>
|
||||
@ -411,6 +439,15 @@ namespace Pal.Client.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Testing....
|
||||
/// </summary>
|
||||
internal static string Config_TestConnection_Connecting {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_TestConnection_Connecting", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Trap color.
|
||||
/// </summary>
|
||||
@ -620,7 +657,16 @@ namespace Pal.Client.Properties {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Please finish the first-time setup first..
|
||||
/// 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>
|
||||
internal static string Error_FirstTimeSetupRequired {
|
||||
get {
|
||||
@ -655,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>
|
||||
|
@ -18,12 +18,290 @@
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<!-- Common -->
|
||||
<data name="Palace_Pal" xml:space="preserve">
|
||||
<value>Palace Pal</value>
|
||||
<comment>Plugin Name</comment>
|
||||
</data>
|
||||
<data name="PalaceOfTheDead" xml:space="preserve">
|
||||
<value>Palais des morts</value>
|
||||
</data>
|
||||
<data name="HeavenOnHigh" xml:space="preserve">
|
||||
<value>Palais des Cieux</value>
|
||||
</data>
|
||||
<data name="EurekaOrthos" xml:space="preserve">
|
||||
<value>Eurêka Orthos</value>
|
||||
</data>
|
||||
<data name="Save" xml:space="preserve">
|
||||
<value>Enregistrer</value>
|
||||
</data>
|
||||
<data name="SaveAndClose" xml:space="preserve">
|
||||
<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 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>
|
||||
</data>
|
||||
<!-- /pal commands -->
|
||||
<data name="Command_pal_HelpText" xml:space="preserve">
|
||||
<value>Ouvrir la fenêtre de configuration / débogage</value>
|
||||
<comment>Help text for the /pal command, shown in the Plugin Installer</comment>
|
||||
</data>
|
||||
<data name="Command_pal_UnknownSubcommand" xml:space="preserve">
|
||||
<value>Paramètre inconnu "{0} pour "{1}".</value>
|
||||
<comment>Error shown when using '/pal xxx' with an unknown argument 'xxx'.</comment>
|
||||
</data>
|
||||
<data name="Command_pal_stats_CurrentFloor" xml:space="preserve">
|
||||
<value>Vous pouvez voir les statistiques de l'étage actuel en ouvrant l'onglet "Débogage" dans la fenêtre de configuration.</value>
|
||||
</data>
|
||||
<data name="Command_pal_stats_UnableToFetchStatistics" xml:space="preserve">
|
||||
<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>
|
||||
<!-- Messages while connecting to the server. These are typically only visible when clicking 'Test connection'. -->
|
||||
<!-- Config Window -->
|
||||
<data name="ConnectionSuccessful" xml:space="preserve">
|
||||
<value>Connexion réussie.</value>
|
||||
</data>
|
||||
<data name="ConnectionError_NotOnline" xml:space="preserve">
|
||||
<value>Vous n'êtes pas connecté.</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>Votre version de Palace Pal est dépréciée, veuillez faire la mise à jour via l'installateur de plugin.</value>
|
||||
<comment>Shown if the version is too old to create an account or log in.</comment>
|
||||
</data>
|
||||
<data name="ConnectionError_CreateAccountFailed" xml:space="preserve">
|
||||
<value>Échec de la création de compte ({0}).</value>
|
||||
<comment>Creating an account failed with an error code, but there is no (translated) error message for it.</comment>
|
||||
</data>
|
||||
<data name="ConnectionError_CreateAccountReturnedNoId" xml:space="preserve">
|
||||
<value>Pas d'ID de compte suite à la tentative de création d'un compte.</value>
|
||||
<comment>If the creation of an account was successful, we expect an account-id to be returned so we can log in. If this happens, the server sent an invalid response.</comment>
|
||||
</data>
|
||||
<data name="ConnectionError_InvalidAccountId" xml:space="preserve">
|
||||
<value>Identifiant de compte invalide.</value>
|
||||
<comment>The account id used was not found on the server.</comment>
|
||||
</data>
|
||||
<data name="ConnectionError_LoginFailed" xml:space="preserve">
|
||||
<value>Échec de connexion ({0}).</value>
|
||||
<comment>Logging in failed with an error code, but there is no (translated) error message for it.</comment>
|
||||
</data>
|
||||
<data name="ConnectionError_LoginReturnedNoToken" xml:space="preserve">
|
||||
<value>Informations de connexion non disponibles.</value>
|
||||
<comment>The server sent no valid client token, even though the login was successful.</comment>
|
||||
</data>
|
||||
<data name="ConnectionError_CouldNotConnectToServer" xml:space="preserve">
|
||||
<value>Échec de connexion au serveur : {0}</value>
|
||||
</data>
|
||||
<!-- Config Window: Deep Dungeons -->
|
||||
<data name="ConfigTab_DeepDungeons" xml:space="preserve">
|
||||
<value>Donjons sans fonds</value>
|
||||
</data>
|
||||
<data name="Config_Traps_Show" xml:space="preserve">
|
||||
<value>Afficher les pièges</value>
|
||||
</data>
|
||||
<data name="Config_Traps_Color" xml:space="preserve">
|
||||
<value>Couleur pièges</value>
|
||||
</data>
|
||||
<data name="Config_Traps_HideImpossible" xml:space="preserve">
|
||||
<value>Masquer les pièges de l'étage actuel</value>
|
||||
<comment>When a trap has no chance of appearing (after a pomander of safety/sight is used), hide it.</comment>
|
||||
</data>
|
||||
<data name="Config_Traps_HideImpossible_ToolTip" xml:space="preserve">
|
||||
<value>Lors de l'utilisation d'une poterie magique de localisation, seul les locations confirmées sont affichées, les locations potentielles sont masquées.
|
||||
Lors de l'utilisation d'une poterie magique de désamorçage, tous les pièges sont masqués.</value>
|
||||
</data>
|
||||
<data name="Config_HoardCoffers_Show" xml:space="preserve">
|
||||
<value>Afficher les trésors cachés</value>
|
||||
</data>
|
||||
<data name="Config_HoardCoffers_Color" xml:space="preserve">
|
||||
<value>Couleur trésors cachés</value>
|
||||
</data>
|
||||
<data name="Config_HoardCoffers_HideImpossible" xml:space="preserve">
|
||||
<value>Masquer les trésors cachés ne se trouvent pas sur l'étage actuel</value>
|
||||
<comment>When a hoard coffer has no chance of appearing (after a pomander of safety/sight is used), hide it.</comment>
|
||||
</data>
|
||||
<data name="Config_HoardCoffers_HideImpossible_ToolTip" xml:space="preserve">
|
||||
<value>Lors de l'utilisation d'une poterie magique d'intuition, seul les trésors cachés confirmés sont visibles, les locations potentielles sont masquées.</value>
|
||||
</data>
|
||||
<data name="Config_SilverCoffer_Show" xml:space="preserve">
|
||||
<value>Afficher les coffres en argent de l'étage actuel</value>
|
||||
</data>
|
||||
<data name="Config_SilverCoffers_ToolTip" xml:space="preserve">
|
||||
<value>Afficher tous les coffres en argent actuellement visible sur l'étage actuel.
|
||||
Il n'y a pas de synchronisation avec les autres joueurs ni de sauvegarde entre les étages / tentatives.</value>
|
||||
</data>
|
||||
<data name="Config_SilverCoffer_Color" xml:space="preserve">
|
||||
<value>Couleur coffres argent</value>
|
||||
</data>
|
||||
<data name="Config_SilverCoffer_Filled" xml:space="preserve">
|
||||
<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>
|
||||
<data name="Config_TestConnection" xml:space="preserve">
|
||||
<value>Test de connexion</value>
|
||||
</data>
|
||||
<data name="Config_TestConnection_Connecting" xml:space="preserve">
|
||||
<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>
|
||||
<data name="Config_ImportExplanation1" xml:space="preserve">
|
||||
<value>Utiliser un fichier d'importation peut être utilisé si vous n'arriver pas à vous connecter au serveur ou bien que vous ne souhaitez pas partager vos découvertes.</value>
|
||||
</data>
|
||||
<data name="Config_ImportExplanation2" xml:space="preserve">
|
||||
<value>Les exports sont (actuellement) générées manuellement. Ils incluent actuellement les trésors cachés et pièges rencontrés par 5 personnes ou plus. Cela peut amener des résultats sporadiques pour les étages supérieurs, cependant les étages communs (tel que PdM 51-60, PdC 21-30) sont proches d'être complets.</value>
|
||||
</data>
|
||||
<data name="Config_ImportExplanation3" xml:space="preserve">
|
||||
<value>Si vous n'êtes pas connecté, importer un fichier n'aura aucun impact.</value>
|
||||
</data>
|
||||
<data name="Config_ImportDownloadLocation" xml:space="preserve">
|
||||
<value>Les exports sont disponibles depuis {0} (en tant que fichiers *.pal).</value>
|
||||
</data>
|
||||
<data name="Config_Import_VisitGitHub" xml:space="preserve">
|
||||
<value>Visiter GitHub</value>
|
||||
</data>
|
||||
<data name="Config_SelectImportFile" xml:space="preserve">
|
||||
<value>Fichier à importer :</value>
|
||||
</data>
|
||||
<data name="Config_SelectImportFile_Hint" xml:space="preserve">
|
||||
<value>Chemin vers le fichier *.pal</value>
|
||||
<comment>When importing a file, this is the hint that shows up in the 'path' input box while no file has been selected.</comment>
|
||||
</data>
|
||||
<data name="Config_StartImport" xml:space="preserve">
|
||||
<value>Démarrer l'importation</value>
|
||||
</data>
|
||||
<data name="Config_UndoImportExplanation1" xml:space="preserve">
|
||||
<value>Votre dernière importation était le {0}, qui a ajoutée la base de données de pièges et trésors cachés depuis {1} créée le {2:d}.</value>
|
||||
</data>
|
||||
<data name="Config_UndoImportExplanation2" xml:space="preserve">
|
||||
<value>Si vous pensez que c'est une erreur, vous pouvez supprimer tous les emplacements trouvés dans l'import (aucune des locations que vous avez trouvées ne sont modifiées).</value>
|
||||
</data>
|
||||
<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>
|
||||
<data name="Config_ExportSource" xml:space="preserve">
|
||||
<value>Exporter tous les marqueurs depuis {0} :</value>
|
||||
</data>
|
||||
<data name="Config_Export_SaveAs" xml:space="preserve">
|
||||
<value>Enregistrer sous :</value>
|
||||
</data>
|
||||
<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>
|
||||
</data>
|
||||
<data name="Config_SelectRenderBackend" xml:space="preserve">
|
||||
<value>Choisissez le moteur de rendu pour les marqueurs :</value>
|
||||
</data>
|
||||
<data name="Config_Renderer_Splatoon" xml:space="preserve">
|
||||
<value>Splatoon</value>
|
||||
<comment>Splatoon plugin. Do not localize.</comment>
|
||||
</data>
|
||||
<data name="Config_Renderer_Splatoon_Hint" xml:space="preserve">
|
||||
<value>Par défaut, requiert d'avoir installé Splatoon</value>
|
||||
</data>
|
||||
<data name="Config_Renderer_Simple" xml:space="preserve">
|
||||
<value>Simple</value>
|
||||
</data>
|
||||
<data name="Config_Renderer_Simple_Hint" xml:space="preserve">
|
||||
<value>Expérimental</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>
|
||||
<data name="Config_Debug_NotInADeepDungeon" xml:space="preserve">
|
||||
<value>Vous nêtes PAS dans un donjon sans fond.</value>
|
||||
</data>
|
||||
<!-- Statistics Window -->
|
||||
<data name="Statistics" xml:space="preserve">
|
||||
<value>Statistiques</value>
|
||||
</data>
|
||||
<data name="Statistics_TerritoryId" xml:space="preserve">
|
||||
<value>Id</value>
|
||||
</data>
|
||||
<data name="Statistics_InstanceName" xml:space="preserve">
|
||||
<value>Nom de l'instance</value>
|
||||
</data>
|
||||
<data name="Statistics_Traps" xml:space="preserve">
|
||||
<value>Pièges</value>
|
||||
</data>
|
||||
<data name="Statistics_HoardCoffers" xml:space="preserve">
|
||||
<value>Trésor</value>
|
||||
</data>
|
||||
<!-- Agreement Window -->
|
||||
<!-- Import -->
|
||||
<data name="Explanation_1" xml:space="preserve">
|
||||
<value>Pal Palace affiche la location des potentiels pièges et trésors cachés.</value>
|
||||
</data>
|
||||
<data name="Explanation_2" xml:space="preserve">
|
||||
<value>Pour ce faire, l'utilisation d'une poterie magique révélant l'emplacement d'un piège ou trésor caché sauvera la location révélée.</value>
|
||||
</data>
|
||||
<data name="Explanation_3" xml:space="preserve">
|
||||
<value>Idéalement, nous voulons découvrir l'emplacement de chaque piège et trésor potentiel dans le jeu, mais le faire seul peut s'avérer très fastidieux. Les étages 51-60 plus de 300 locations de piège et plus de 290 locations de coffres et nous ne savons pas si la carte est complète. Les étages plus lointains on naturellement moins de tentatives, ce qui rends la cartographie plus complexe.</value>
|
||||
</data>
|
||||
<data name="Explanation_4" xml:space="preserve">
|
||||
<value>Vous pouvez décider si vous souhaitez partager la location des pièges coffres que vous trouvez avec la communauté, ce qui vous permets également de voir les locations trouvées par les autres joueurs. Cette option peut être modifiée à tout moment. Aucune donnée concernant votre personnage FFXIV ou compte n'est envoyée vers notre serveur.</value>
|
||||
</data>
|
||||
<data name="Config_UploadMyDiscoveries_ShowOtherTraps" xml:space="preserve">
|
||||
<value>Envoyer mes découvertes, afficher les pièges et coffres que les autres joueurs ont découvert</value>
|
||||
</data>
|
||||
<data name="Config_NeverUploadDiscoveries_ShowMyTraps" xml:space="preserve">
|
||||
<value>Ne jamais envoyer mes découvertes, afficher uniquement les pièges et trésors que j'ai découverts moi-même</value>
|
||||
</data>
|
||||
<data name="Agreement_Warning1" xml:space="preserve">
|
||||
<value>Bien que ce ne soit pas de l'automatisation, vous enfreignez certainement les conditions d'utilisation.</value>
|
||||
</data>
|
||||
<data name="Agreement_Warning2" xml:space="preserve">
|
||||
<value>Les autres joueurs de votre équipe peuvent toujours voir vos actions.</value>
|
||||
</data>
|
||||
<data name="Agreement_Warning3" xml:space="preserve">
|
||||
<value>De ce fait, veuillez éviter de mentionner ce plugin en jeu et de ne pas partager de vidéos / captures d'écrans.</value>
|
||||
</data>
|
||||
<data name="Agreement_UsingThisOnMyOwnRisk" xml:space="preserve">
|
||||
<value>Je comprends que j'utilise ce plugin à mes propres risques.</value>
|
||||
</data>
|
||||
<data name="Agreement_ViewPluginAndServerSourceCode" xml:space="preserve">
|
||||
<value>Voir le code source du plugin et du serveur</value>
|
||||
</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 setup setup window are selected.</comment>
|
||||
</data>
|
||||
<!-- 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>
|
||||
</data>
|
||||
<data name="Error_ImportFailed" xml:space="preserve">
|
||||
<value>Échec de l'import : {0}</value>
|
||||
</data>
|
||||
<data name="Error_ImportFailed_IncompatibleVersion" xml:space="preserve">
|
||||
<value>Échec de l'import : version incompatible.</value>
|
||||
</data>
|
||||
<data name="Error_ImportFailed_InvalidFile" xml:space="preserve">
|
||||
<value>Échec de l'import : fichier invalide.</value>
|
||||
</data>
|
||||
<!-- Other -->
|
||||
</root>
|
||||
|
@ -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,12 +142,32 @@
|
||||
<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>
|
||||
<data name="Config_TestConnection" xml:space="preserve">
|
||||
<value>接続をテストする</value>
|
||||
</data>
|
||||
<data name="Config_TestConnection_Connecting" xml:space="preserve">
|
||||
<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>
|
||||
@ -159,7 +175,7 @@
|
||||
<value>エクスポートを使用すると、サーバーに接続できない場合や、情報を共有したくない場合に便利です。</value>
|
||||
</data>
|
||||
<data name="Config_ImportExplanation2" xml:space="preserve">
|
||||
<value>エクスポートは(現在)手動で生成され、トラップが含まれており、5人以上が遭遇した宝箱のみが含まれています。
|
||||
<value>エクスポートは(現在)手動で生成され、5人以上が遭遇したトラップ/宝箱のみが含まれています。
|
||||
深層の情報は不安要素が多くなりますが、多くのプレイヤーが挑戦するフロア(死者の宮殿51F-60F、アメノミハシラ21F-30F)は完成に近付いています。</value>
|
||||
</data>
|
||||
<data name="Config_ImportExplanation3" xml:space="preserve">
|
||||
@ -187,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>
|
||||
@ -199,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>
|
||||
@ -219,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>
|
||||
@ -287,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>
|
||||
|
@ -43,8 +43,12 @@
|
||||
|
||||
<!-- Generic Errors -->
|
||||
<data name="Error_FirstTimeSetupRequired" xml:space="preserve">
|
||||
<value>Please finish the first-time setup first.</value>
|
||||
<comment>Before using any /pal command, the first-time setup/agreeement needs to be completed.</comment>
|
||||
<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>
|
||||
@ -52,7 +56,7 @@
|
||||
|
||||
<!-- /pal commands -->
|
||||
<data name="Command_pal_HelpText" xml:space="preserve">
|
||||
<value>Open the configuration/debug window</value>
|
||||
<value>Open the configuration window</value>
|
||||
<comment>Help text for the /pal command, shown in the Plugin Installer</comment>
|
||||
</data>
|
||||
<data name="Command_pal_UnknownSubcommand" xml:space="preserve">
|
||||
@ -66,9 +70,9 @@
|
||||
<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'. -->
|
||||
@ -107,7 +111,7 @@
|
||||
<value>Could not connect to server: {0}</value>
|
||||
</data>
|
||||
|
||||
<!-- Config Window -->
|
||||
<!-- Config Window: Deep Dungeons -->
|
||||
<data name="ConfigTab_DeepDungeons" xml:space="preserve">
|
||||
<value>Deep Dungeons</value>
|
||||
</data>
|
||||
@ -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,14 +156,34 @@ 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">
|
||||
<value>Community</value>
|
||||
</data>
|
||||
<data name="Config_TestConnection" xml:space="preserve">
|
||||
<value>Test Connection</value>
|
||||
</data>
|
||||
<data name="Config_TestConnection_Connecting" xml:space="preserve">
|
||||
<value>Testing...</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>Import</value>
|
||||
</data>
|
||||
@ -181,6 +205,10 @@ This is not synchronized with other players and not saved between floors/runs.</
|
||||
<data name="Config_SelectImportFile" xml:space="preserve">
|
||||
<value>File to Import:</value>
|
||||
</data>
|
||||
<data name="Config_SelectImportFile_Hint" xml:space="preserve">
|
||||
<value>Path to *.pal file</value>
|
||||
<comment>When importing a file, this is the hint that shows up in the 'path' input box while no file has been selected.</comment>
|
||||
</data>
|
||||
<data name="Config_StartImport" xml:space="preserve">
|
||||
<value>Start Import</value>
|
||||
</data>
|
||||
@ -194,6 +222,7 @@ This is not synchronized with other players and not saved between floors/runs.</
|
||||
<value>Undo Import</value>
|
||||
</data>
|
||||
|
||||
<!-- Config Window: Export -->
|
||||
<data name="ConfigTab_Export" xml:space="preserve">
|
||||
<value>Export</value>
|
||||
</data>
|
||||
@ -207,6 +236,7 @@ This is not synchronized with other players and not saved between floors/runs.</
|
||||
<value>Start Export</value>
|
||||
</data>
|
||||
|
||||
<!-- Config Window: Renderer -->
|
||||
<data name="ConfigTab_Renderer" xml:space="preserve">
|
||||
<value>Renderer</value>
|
||||
<comment>Configuration tab to select Splatoon or Simple as rendering backend</comment>
|
||||
@ -227,14 +257,12 @@ 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>
|
||||
</data>
|
||||
|
||||
<!-- Config Window: Debug -->
|
||||
<data name="ConfigTab_Debug" xml:space="preserve">
|
||||
<value>Debug</value>
|
||||
</data>
|
||||
@ -295,10 +323,10 @@ This is not synchronized with other players and not saved between floors/runs.</
|
||||
</data>
|
||||
<data name="Agreement_PickOneOption" xml:space="preserve">
|
||||
<value>Please chose one of the options above.</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>Imported {0} new trap locations and {1} new hoard coffer locations.</value>
|
||||
<comment>After the import of a *.pal file, the number of traps/hoard coffers is shown as a summary.</comment>
|
||||
@ -312,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
|
||||
{
|
||||
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
|
||||
{
|
||||
namespace Pal.Client.Rendering;
|
||||
|
||||
public interface IRenderElement
|
||||
{
|
||||
bool IsValid { get; }
|
||||
|
||||
uint Color { get; set; }
|
||||
}
|
||||
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
|
||||
{
|
||||
ERenderer GetConfigValue();
|
||||
|
||||
void SetLayer(ELayer layer, IReadOnlyList<IRenderElement> elements);
|
||||
|
||||
void ResetLayer(ELayer layer);
|
||||
|
||||
IRenderElement CreateElement(Marker.EType type, Vector3 pos, uint color, bool fill = false);
|
||||
}
|
||||
IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, bool enabled, uint color, bool fill = false);
|
||||
|
||||
void DrawDebugItems(uint trapColor, uint hoardColor);
|
||||
}
|
||||
|
@ -1,20 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
using Pal.Client.Floors;
|
||||
|
||||
namespace Pal.Client.Rendering
|
||||
{
|
||||
internal class MarkerConfig
|
||||
{
|
||||
namespace Pal.Client.Rendering;
|
||||
|
||||
private readonly static Dictionary<Marker.EType, MarkerConfig> _markerConfig = new Dictionary<Marker.EType, MarkerConfig>
|
||||
internal sealed class MarkerConfig
|
||||
{
|
||||
{ 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 } },
|
||||
private static readonly MarkerConfig EmptyConfig = new();
|
||||
|
||||
private static readonly Dictionary<MemoryLocation.EType, MarkerConfig> MarkerConfigs = new()
|
||||
{
|
||||
{ MemoryLocation.EType.Trap, new MarkerConfig { Radius = 1.7f } },
|
||||
{ MemoryLocation.EType.Hoard, new MarkerConfig { Radius = 1.7f, OffsetY = -0.03f } },
|
||||
{ MemoryLocation.EType.SilverCoffer, new MarkerConfig { Radius = 1f } },
|
||||
{ MemoryLocation.EType.GoldCoffer, new MarkerConfig { Radius = 1f } },
|
||||
};
|
||||
|
||||
public float OffsetY { get; set; } = 0;
|
||||
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) => _markerConfig[type] ?? new MarkerConfig();
|
||||
}
|
||||
public static MarkerConfig ForType(MemoryLocation.EType type) =>
|
||||
MarkerConfigs.GetValueOrDefault(type, EmptyConfig);
|
||||
}
|
||||
|
78
Pal.Client/Rendering/RenderAdapter.cs
Normal file
78
Pal.Client/Rendering/RenderAdapter.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Pal.Client.Configuration;
|
||||
using Pal.Client.Floors;
|
||||
|
||||
namespace Pal.Client.Rendering;
|
||||
|
||||
internal sealed class RenderAdapter : IRenderer, IDisposable
|
||||
{
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly ILogger<RenderAdapter> _logger;
|
||||
private readonly IPalacePalConfiguration _configuration;
|
||||
|
||||
private IServiceScope? _renderScope;
|
||||
private IRenderer _implementation;
|
||||
|
||||
public RenderAdapter(IServiceScopeFactory serviceScopeFactory, ILogger<RenderAdapter> logger,
|
||||
IPalacePalConfiguration configuration)
|
||||
{
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_logger = logger;
|
||||
_configuration = configuration;
|
||||
|
||||
_implementation = Recreate(null);
|
||||
}
|
||||
|
||||
public bool RequireRedraw { get; set; }
|
||||
|
||||
private IRenderer Recreate(ERenderer? currentRenderer)
|
||||
{
|
||||
ERenderer targetRenderer = _configuration.Renderer.SelectedRenderer;
|
||||
if (targetRenderer == currentRenderer)
|
||||
return _implementation;
|
||||
|
||||
_renderScope?.Dispose();
|
||||
|
||||
_logger.LogInformation("Selected new renderer: {Renderer}", _configuration.Renderer.SelectedRenderer);
|
||||
_renderScope = _serviceScopeFactory.CreateScope();
|
||||
if (targetRenderer == ERenderer.Splatoon)
|
||||
return _renderScope.ServiceProvider.GetRequiredService<SplatoonRenderer>();
|
||||
else
|
||||
return _renderScope.ServiceProvider.GetRequiredService<SimpleRenderer>();
|
||||
}
|
||||
|
||||
public void ConfigUpdated()
|
||||
{
|
||||
_implementation = Recreate(_implementation.GetConfigValue());
|
||||
RequireRedraw = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
=> _renderScope?.Dispose();
|
||||
|
||||
public void SetLayer(ELayer layer, IReadOnlyList<IRenderElement> elements)
|
||||
=> _implementation.SetLayer(layer, elements);
|
||||
|
||||
public void ResetLayer(ELayer layer)
|
||||
=> _implementation.ResetLayer(layer);
|
||||
|
||||
public IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, bool enabled, uint color,
|
||||
bool fill = false)
|
||||
=> _implementation.CreateElement(type, pos, enabled, color, fill);
|
||||
|
||||
public ERenderer GetConfigValue()
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public void DrawDebugItems(uint trapColor, uint hoardColor)
|
||||
=> _implementation.DrawDebugItems(trapColor, hoardColor);
|
||||
|
||||
public void DrawLayers()
|
||||
{
|
||||
if (_implementation is SimpleRenderer sr)
|
||||
sr.DrawLayers();
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user