Compare commits
164 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 | |||
f7d707073b | |||
a180309815 | |||
65bfb041b4 | |||
b150e5727b | |||
049d5ded9b | |||
cce435cc7b | |||
b23a79629d | |||
f7840ab29b | |||
01d6b7f2dd | |||
db78d35fba | |||
84a6f1e0a1 | |||
f8b77b9aa9 | |||
25709f5530 | |||
7122a07a28 | |||
b13d26a731 | |||
db89966be2 | |||
9263b9edfc | |||
136774eb71 | |||
53bd00285b | |||
209bfa62b8 | |||
51d474099f | |||
c203f5bc95 | |||
ea553a6d8a | |||
b959355121 | |||
74d18c95c6 | |||
8e4af4ef81 | |||
7d45d50f9a | |||
527d8a3bcb | |||
6013b84568 | |||
8a2f071582 | |||
1e83e95f37 | |||
532889f211 | |||
1dd670ff03 | |||
8ad3d598c6 |
9
.dockerignore
Normal file
9
.dockerignore
Normal file
@ -0,0 +1,9 @@
|
||||
*/bin
|
||||
*/obj
|
||||
.git
|
||||
.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
|
@ -3,5 +3,6 @@
|
||||
# Use with:
|
||||
#
|
||||
# $> git config core.hooksPath .githooks
|
||||
#
|
||||
dotnet format --exclude vendor/
|
||||
if ! git diff --cached --quiet -- "*.cs"; then
|
||||
dotnet format --verbosity quiet --exclude vendor/
|
||||
fi
|
||||
|
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>
|
30
Dockerfile
Normal file
30
Dockerfile
Normal file
@ -0,0 +1,30 @@
|
||||
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 Server/Server/Pal.Server.csproj Server/Server/
|
||||
RUN dotnet restore Server/Server/Pal.Server.csproj -a $TARGETARCH
|
||||
|
||||
COPY . ./
|
||||
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
|
||||
|
||||
EXPOSE 5415
|
||||
ENV DOTNET_ENVIRONMENT=Production
|
||||
ENV ASPNETCORE_URLS=
|
||||
ENV DataDirectory=/data
|
||||
ENV UseForwardedIp=true
|
||||
ENV Kestrel__Endpoints__Http2__Url=http://+:5415
|
||||
|
||||
RUN adduser --uid 2000 --disabled-password --group --no-create-home --quiet --system pal
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=build-env /dist .
|
||||
|
||||
USER pal
|
||||
ENTRYPOINT ["dotnet", "Pal.Server.dll"]
|
9
Pal.Client/Commands/ISubCommand.cs
Normal file
9
Pal.Client/Commands/ISubCommand.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Pal.Client.Commands;
|
||||
|
||||
public interface ISubCommand
|
||||
{
|
||||
IReadOnlyDictionary<string, Action<string>> GetHandlers();
|
||||
}
|
39
Pal.Client/Commands/PalConfigCommand.cs
Normal file
39
Pal.Client/Commands/PalConfigCommand.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Pal.Client.Configuration;
|
||||
using Pal.Client.Windows;
|
||||
|
||||
namespace Pal.Client.Commands;
|
||||
|
||||
internal class PalConfigCommand : ISubCommand
|
||||
{
|
||||
private readonly IPalacePalConfiguration _configuration;
|
||||
private readonly AgreementWindow _agreementWindow;
|
||||
private readonly ConfigWindow _configWindow;
|
||||
|
||||
public PalConfigCommand(
|
||||
IPalacePalConfiguration configuration,
|
||||
AgreementWindow agreementWindow,
|
||||
ConfigWindow configWindow)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_agreementWindow = agreementWindow;
|
||||
_configWindow = configWindow;
|
||||
}
|
||||
|
||||
|
||||
public IReadOnlyDictionary<string, Action<string>> GetHandlers()
|
||||
=> new Dictionary<string, Action<string>>
|
||||
{
|
||||
{ "config", _ => Execute() },
|
||||
{ "", _ => Execute() }
|
||||
};
|
||||
|
||||
public void Execute()
|
||||
{
|
||||
if (_configuration.FirstUse)
|
||||
_agreementWindow.IsOpen = true;
|
||||
else
|
||||
_configWindow.Toggle();
|
||||
}
|
||||
}
|
62
Pal.Client/Commands/PalNearCommand.cs
Normal file
62
Pal.Client/Commands/PalNearCommand.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Pal.Client.DependencyInjection;
|
||||
using Pal.Client.Extensions;
|
||||
using Pal.Client.Floors;
|
||||
using Pal.Client.Rendering;
|
||||
|
||||
namespace Pal.Client.Commands;
|
||||
|
||||
internal sealed class PalNearCommand : ISubCommand
|
||||
{
|
||||
private readonly Chat _chat;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly TerritoryState _territoryState;
|
||||
private readonly FloorService _floorService;
|
||||
|
||||
public PalNearCommand(Chat chat, IClientState clientState, TerritoryState territoryState,
|
||||
FloorService floorService)
|
||||
{
|
||||
_chat = chat;
|
||||
_clientState = clientState;
|
||||
_territoryState = territoryState;
|
||||
_floorService = floorService;
|
||||
}
|
||||
|
||||
|
||||
public IReadOnlyDictionary<string, Action<string>> GetHandlers()
|
||||
=> new Dictionary<string, Action<string>>
|
||||
{
|
||||
{ "near", _ => DebugNearest(_ => true) },
|
||||
{ "tnear", _ => DebugNearest(m => m.Type == MemoryLocation.EType.Trap) },
|
||||
{ "hnear", _ => DebugNearest(m => m.Type == MemoryLocation.EType.Hoard) },
|
||||
};
|
||||
|
||||
private void DebugNearest(Predicate<PersistentLocation> predicate)
|
||||
{
|
||||
if (!_territoryState.IsInDeepDungeon())
|
||||
return;
|
||||
|
||||
var state = _floorService.GetTerritoryIfReady(_clientState.TerritoryType);
|
||||
if (state == null)
|
||||
return;
|
||||
|
||||
var playerPosition = _clientState.LocalPlayer?.Position;
|
||||
if (playerPosition == null)
|
||||
return;
|
||||
_chat.Message($"Your position: {playerPosition}");
|
||||
|
||||
var nearbyMarkers = state.Locations
|
||||
.Where(m => predicate(m))
|
||||
.Where(m => m.RenderElement != null && m.RenderElement.Enabled)
|
||||
.Select(m => new { m, distance = (playerPosition.Value - m.Position).Length() })
|
||||
.OrderBy(m => m.distance)
|
||||
.Take(5)
|
||||
.ToList();
|
||||
foreach (var nearbyMarker in nearbyMarkers)
|
||||
_chat.UnformattedMessage(
|
||||
$"{nearbyMarker.distance:F2} - {nearbyMarker.m.Type} {nearbyMarker.m.NetworkId?.ToPartialId(length: 8)} - {nearbyMarker.m.Position}");
|
||||
}
|
||||
}
|
24
Pal.Client/Commands/PalStatsCommand.cs
Normal file
24
Pal.Client/Commands/PalStatsCommand.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Pal.Client.DependencyInjection;
|
||||
|
||||
namespace Pal.Client.Commands;
|
||||
|
||||
internal sealed class PalStatsCommand : ISubCommand
|
||||
{
|
||||
private readonly StatisticsService _statisticsService;
|
||||
|
||||
public PalStatsCommand(StatisticsService statisticsService)
|
||||
{
|
||||
_statisticsService = statisticsService;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, Action<string>> GetHandlers()
|
||||
=> new Dictionary<string, Action<string>>
|
||||
{
|
||||
{ "stats", _ => Execute() },
|
||||
};
|
||||
|
||||
private void Execute()
|
||||
=> _statisticsService.ShowGlobalStatistics();
|
||||
}
|
29
Pal.Client/Commands/PalTestConnectionCommand.cs
Normal file
29
Pal.Client/Commands/PalTestConnectionCommand.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ECommons.Schedulers;
|
||||
using Pal.Client.Windows;
|
||||
|
||||
namespace Pal.Client.Commands;
|
||||
|
||||
internal sealed class PalTestConnectionCommand : ISubCommand
|
||||
{
|
||||
private readonly ConfigWindow _configWindow;
|
||||
|
||||
public PalTestConnectionCommand(ConfigWindow configWindow)
|
||||
{
|
||||
_configWindow = configWindow;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, Action<string>> GetHandlers()
|
||||
=> new Dictionary<string, Action<string>>
|
||||
{
|
||||
{ "test-connection", _ => Execute() },
|
||||
{ "tc", _ => Execute() },
|
||||
};
|
||||
|
||||
private void Execute()
|
||||
{
|
||||
_configWindow.IsOpen = true;
|
||||
var _ = new TickScheduler(() => _configWindow.TestConnection());
|
||||
}
|
||||
}
|
@ -1,193 +0,0 @@
|
||||
using Dalamud.Configuration;
|
||||
using Dalamud.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Pal.Client.Scheduled;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
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; } = 4;
|
||||
|
||||
#region Saved configuration values
|
||||
public bool FirstUse { get; set; } = true;
|
||||
public EMode Mode { get; set; } = EMode.Offline;
|
||||
|
||||
[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();
|
||||
}
|
||||
}
|
||||
#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 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 List<string>();
|
||||
}
|
||||
|
||||
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,5 +0,0 @@
|
||||
#!/bin/sh
|
||||
curl -O https://raw.githubusercontent.com/karashiiro/DalamudPluginProjectTemplate/master/.github/workflows/dotnet.yml
|
||||
mkdir .github
|
||||
mkdir .github/workflows
|
||||
mv dotnet.yml .github/workflows
|
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;
|
||||
}
|
||||
}
|
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,
|
||||
}
|
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,121 +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 JsonSerializerOptions { IncludeFields = true };
|
||||
private static readonly int _currentVersion = 3;
|
||||
|
||||
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),
|
||||
};
|
||||
version = save.Version;
|
||||
}
|
||||
|
||||
localState.ApplyFilters();
|
||||
|
||||
if (version < _currentVersion)
|
||||
localState.Save();
|
||||
|
||||
return localState;
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
string path = GetSaveLocation(TerritoryType);
|
||||
|
||||
ApplyFilters();
|
||||
if (Markers.Count == 0)
|
||||
File.Delete(path);
|
||||
else
|
||||
{
|
||||
File.WriteAllText(path, JsonSerializer.Serialize(new SaveFile
|
||||
{
|
||||
Version = _currentVersion,
|
||||
Markers = new HashSet<Marker>(Markers)
|
||||
}, _jsonSerializerOptions));
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetSaveLocation(uint territoryType) => Path.Join(Service.PluginInterface.GetPluginConfigDirectory(), $"{territoryType}.json");
|
||||
|
||||
public static void UpdateAll()
|
||||
{
|
||||
foreach (ETerritoryType territory in typeof(ETerritoryType).GetEnumValues())
|
||||
{
|
||||
LocalState? localState = Load((ushort)territory);
|
||||
if (localState != null)
|
||||
localState.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(id => importIds.Contains(id));
|
||||
}
|
||||
|
||||
public class SaveFile
|
||||
{
|
||||
public int Version { get; set; }
|
||||
public HashSet<Marker> Markers { get; set; } = new();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
using ECommons.SplatoonAPI;
|
||||
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; } = false;
|
||||
|
||||
/// <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<Guid> RemoteSeenOn { get; set; } = new List<Guid>();
|
||||
|
||||
/// <summary>
|
||||
/// Whether this marker was requested to be seen, to avoid duplicate requests.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool RemoteSeenRequested { get; set; } = false;
|
||||
|
||||
/// <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 List<Guid>();
|
||||
|
||||
public bool WasImported { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public Element? SplatoonElement { 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, (int)Position.X, (int)Position.Y, (int)Position.Z);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is Marker otherMarker && Type == otherMarker.Type && (int)Position.X == (int)otherMarker.Position.X && (int)Position.Y == (int)otherMarker.Position.Y && (int)Position.Z == (int)otherMarker.Position.Z;
|
||||
}
|
||||
|
||||
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,
|
||||
#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,32 +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, "You are not online.");
|
||||
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
|
||||
@ -37,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 {PartialAccountId}");
|
||||
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.PrintError("[Palace Pal] Your version of Palace Pal is outdated, please update the plugin using the Plugin Installer.");
|
||||
_chat.Error(Localization.ConnectionError_OldVersion);
|
||||
_warnedAboutUpgrade = true;
|
||||
}
|
||||
return (false, $"Could not create account ({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, "No account-id after account was attempted to be created.");
|
||||
_logger.LogWarning("No account to login with");
|
||||
return (false, Localization.ConnectionError_CreateAccountReturnedNoId, false);
|
||||
}
|
||||
|
||||
if (!_loginInfo.IsValid)
|
||||
{
|
||||
PluginLog.Information($"TryConnect: Logging in with account id {PartialAccountId}");
|
||||
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: {PartialAccountId}");
|
||||
_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, "Invalid account id.");
|
||||
_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.PrintError("[Palace Pal] Your version of Palace Pal is outdated, please update the plugin using the Plugin Installer.");
|
||||
_chat.Error(Localization.ConnectionError_OldVersion);
|
||||
_warnedAboutUpgrade = true;
|
||||
}
|
||||
return (false, $"Could not log in ({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, "No login information available.");
|
||||
_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)
|
||||
@ -132,19 +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 $"Could not connect to server: {connectionResult.Error}";
|
||||
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);
|
||||
return "Connection successful.";
|
||||
await accountClient.VerifyAsync(new VerifyRequest(), headers: AuthorizedHeaders(),
|
||||
deadline: DateTime.UtcNow.AddSeconds(10), cancellationToken: cancellationToken);
|
||||
|
||||
_logger.LogInformation("Verification returned no errors.");
|
||||
return Localization.ConnectionSuccessful;
|
||||
}
|
||||
|
||||
internal class LoginInfo
|
||||
internal sealed class LoginInfo
|
||||
{
|
||||
public LoginInfo(string? authToken)
|
||||
{
|
||||
@ -152,7 +228,7 @@ namespace Pal.Client.Net
|
||||
{
|
||||
IsLoggedIn = true;
|
||||
AuthToken = authToken;
|
||||
Claims = JwtClaims.FromAuthToken(authToken!);
|
||||
Claims = JwtClaims.FromAuthToken(authToken);
|
||||
}
|
||||
else
|
||||
IsLoggedIn = false;
|
||||
@ -161,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,21 +1,23 @@
|
||||
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 },
|
||||
{ "User-Agent", _userAgent },
|
||||
};
|
||||
|
||||
private Metadata AuthorizedHeaders() => new Metadata
|
||||
private Metadata AuthorizedHeaders() => new()
|
||||
{
|
||||
{ "Authorization", $"Bearer {_loginInfo?.AuthToken}" },
|
||||
{ "User-Agent", UserAgent },
|
||||
{ "Authorization", $"Bearer {_loginInfo.AuthToken}" },
|
||||
{ "User-Agent", _userAgent },
|
||||
};
|
||||
|
||||
private SslClientAuthenticationOptions? GetSslClientAuthenticationOptions()
|
||||
@ -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,47 +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 LoginInfo(null);
|
||||
private bool _warnedAboutUpgrade = false;
|
||||
private LoginInfo _loginInfo = new(null);
|
||||
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;
|
||||
|
||||
private string PartialAccountId =>
|
||||
Account?.Id?.ToString()?.PadRight(14).Substring(0, 13) ?? "[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>10.0</LangVersion>
|
||||
<Version>2.0</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')">
|
||||
@ -31,20 +23,26 @@
|
||||
</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>
|
||||
@ -54,39 +52,18 @@
|
||||
</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>
|
||||
<EmbeddedResource Update="Properties\Localization.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Localization.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
<Compile Update="Properties\Localization.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Localization.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="RenameLatestZip" AfterTargets="PackagePlugin">
|
||||
<Exec Command="rename "$(OutDir)$(AssemblyName)\latest.zip" "$(AssemblyName)-$(Version).zip"" />
|
||||
<Target Name="Clean">
|
||||
<RemoveDir Directories="dist"/>
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
@ -2,7 +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",
|
||||
"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,677 +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 ECommons;
|
||||
using ECommons.Schedulers;
|
||||
using ECommons.SplatoonAPI;
|
||||
using Grpc.Core;
|
||||
using ImGuiNET;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using Pal.Client.Net;
|
||||
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 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)
|
||||
{
|
||||
private const long ON_TERRITORY_CHANGE = -2;
|
||||
private const uint COLOR_INVISIBLE = 0;
|
||||
private const string SPLATOON_TRAP_HOARD = "PalacePal.TrapHoard";
|
||||
private const string SPLATOON_REGULAR_COFFERS = "PalacePal.RegularCoffers";
|
||||
_pluginInterface = pluginInterface;
|
||||
_commandManager = commandManager;
|
||||
_clientState = clientState;
|
||||
_chatGui = chatGui;
|
||||
_framework = framework;
|
||||
_logger = new DalamudLoggerProvider(pluginLog).CreateLogger<Plugin>();
|
||||
|
||||
private readonly static Dictionary<Marker.EType, MarkerConfig> _markerConfig = new Dictionary<Marker.EType, MarkerConfig>
|
||||
// set up the current UI language before creating anything
|
||||
Localization.Culture = new CultureInfo(_pluginInterface.UiLanguage);
|
||||
|
||||
_commandManager.AddHandler("/pal", new CommandInfo(OnCommand)
|
||||
{
|
||||
{ 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 LocalizedChatMessages _localizedChatMessages = new();
|
||||
|
||||
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();
|
||||
|
||||
public string Name => "Palace Pal";
|
||||
|
||||
public Plugin(DalamudPluginInterface pluginInterface, ChatGui chat)
|
||||
{
|
||||
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.PrintError("[Palace Pal] Please install this plugin from the official repository at https://github.com/carvelli/Dalamud-Plugins to continue using it.");
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
#endif
|
||||
|
||||
ECommonsMain.Init(pluginInterface, this, Module.SplatoonAPI);
|
||||
|
||||
pluginInterface.Create<Service>();
|
||||
Service.Plugin = this;
|
||||
Service.Configuration = (Configuration?)pluginInterface.GetPluginConfig() ?? pluginInterface.Create<Configuration>()!;
|
||||
Service.Configuration.Migrate();
|
||||
|
||||
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 += Service.WindowSystem.Draw;
|
||||
pluginInterface.UiBuilder.OpenConfigUi += OnOpenConfigUi;
|
||||
Service.Framework.Update += OnFrameworkUpdate;
|
||||
Service.Chat.ChatMessage += OnChatMessage;
|
||||
Service.CommandManager.AddHandler("/pal", new CommandInfo(OnCommand)
|
||||
{
|
||||
HelpMessage = "Open the configuration/debug window"
|
||||
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)
|
||||
arguments = arguments.Trim();
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
Service.Chat.PrintError("[Palace Pal] Please finish the first-time setup first.");
|
||||
return;
|
||||
}
|
||||
IServiceScope rootScope;
|
||||
Chat chat;
|
||||
|
||||
try
|
||||
{
|
||||
arguments = arguments.Trim();
|
||||
switch (arguments)
|
||||
{
|
||||
case "stats":
|
||||
Task.Run(async () => await FetchFloorStatistics());
|
||||
break;
|
||||
|
||||
case "test-connection":
|
||||
case "tc":
|
||||
var configWindow = Service.WindowSystem.GetWindow<ConfigWindow>();
|
||||
if (configWindow == null)
|
||||
return;
|
||||
|
||||
configWindow.IsOpen = true;
|
||||
configWindow.TestConnection();
|
||||
break;
|
||||
|
||||
#if DEBUG
|
||||
case "update-saves":
|
||||
LocalState.UpdateAll();
|
||||
Service.Chat.Print("Updated all locally cached marker files to latest version.");
|
||||
break;
|
||||
#endif
|
||||
|
||||
case "":
|
||||
case "config":
|
||||
Service.WindowSystem.GetWindow<ConfigWindow>()?.Toggle();
|
||||
break;
|
||||
|
||||
default:
|
||||
Service.Chat.PrintError($"[Palace Pal] Unknown sub-command '{arguments}' for '{command}'.");
|
||||
break;
|
||||
}
|
||||
rootScope = await _rootScopeCompletionSource.Task;
|
||||
chat = rootScope.ServiceProvider.GetRequiredService<Chat>();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Service.Chat.PrintError($"[Palace Pal] {e}");
|
||||
_logger.LogError(e, "Could not wait for command root scope");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#region IDisposable Support
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposing) return;
|
||||
|
||||
Service.CommandManager.RemoveHandler("/pal");
|
||||
Service.PluginInterface.UiBuilder.Draw -= Service.WindowSystem.Draw;
|
||||
Service.PluginInterface.UiBuilder.OpenConfigUi -= OnOpenConfigUi;
|
||||
Service.Framework.Update -= OnFrameworkUpdate;
|
||||
Service.Chat.ChatMessage -= OnChatMessage;
|
||||
|
||||
Service.WindowSystem.RemoveAllWindows();
|
||||
|
||||
Service.RemoteApi.Dispose();
|
||||
|
||||
try
|
||||
{
|
||||
Splatoon.RemoveDynamicElements(SPLATOON_TRAP_HOARD);
|
||||
Splatoon.RemoveDynamicElements(SPLATOON_REGULAR_COFFERS);
|
||||
}
|
||||
catch
|
||||
IPalacePalConfiguration configuration =
|
||||
rootScope.ServiceProvider.GetRequiredService<IPalacePalConfiguration>();
|
||||
if (configuration.FirstUse && arguments != "" && arguments != "config")
|
||||
{
|
||||
// destroyed on territory change either way
|
||||
chat.Error(Localization.Error_FirstTimeSetupRequired);
|
||||
return;
|
||||
}
|
||||
ECommonsMain.Dispose();
|
||||
|
||||
Action<string> commandHandler = rootScope.ServiceProvider
|
||||
.GetRequiredService<IEnumerable<ISubCommand>>()
|
||||
.SelectMany(cmd => cmd.GetHandlers())
|
||||
.Where(cmd => cmd.Key == arguments.ToLowerInvariant())
|
||||
.Select(cmd => cmd.Value)
|
||||
.SingleOrDefault(missingCommand =>
|
||||
{
|
||||
chat.Error(string.Format(Localization.Command_pal_UnknownSubcommand, missingCommand,
|
||||
command));
|
||||
});
|
||||
commandHandler.Invoke(arguments);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Could not execute command '{Command}' with arguments '{Arguments}'", command,
|
||||
arguments);
|
||||
chat.Error(string.Format(Localization.Error_CommandFailed,
|
||||
$"{e.GetType()} - {e.Message}"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void OpenConfigUi()
|
||||
=> _rootScope!.ServiceProvider.GetRequiredService<PalConfigCommand>().Execute();
|
||||
|
||||
private void LanguageChanged(string languageCode)
|
||||
{
|
||||
_logger.LogInformation("Language set to '{Language}'", languageCode);
|
||||
|
||||
Localization.Culture = new CultureInfo(languageCode);
|
||||
_windowSystem!.Windows.OfType<ILanguageChanged>()
|
||||
.Each(w => w.LanguageChanged());
|
||||
}
|
||||
|
||||
private void Draw()
|
||||
{
|
||||
_rootScope!.ServiceProvider.GetRequiredService<RenderAdapter>().DrawLayers();
|
||||
_windowSystem!.Draw();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
#endregion
|
||||
_commandManager.RemoveHandler("/pal");
|
||||
|
||||
private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString seMessage, ref bool isHandled)
|
||||
if (_loadState == ELoadState.Loaded)
|
||||
{
|
||||
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;
|
||||
_pluginInterface.UiBuilder.Draw -= Draw;
|
||||
_pluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi;
|
||||
_pluginInterface.LanguageChanged -= LanguageChanged;
|
||||
_clientState.Login -= Login;
|
||||
}
|
||||
|
||||
private void OnFrameworkUpdate(Framework framework)
|
||||
{
|
||||
if (Service.Configuration.FirstUse)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
bool recreateLayout = false;
|
||||
bool saveMarkers = false;
|
||||
|
||||
while (EarlyEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued))
|
||||
queued?.Run(this, ref recreateLayout, ref saveMarkers);
|
||||
|
||||
if (LastTerritory != Service.ClientState.TerritoryType)
|
||||
{
|
||||
LastTerritory = Service.ClientState.TerritoryType;
|
||||
TerritorySyncState = SyncState.NotAttempted;
|
||||
|
||||
if (IsInDeepDungeon())
|
||||
GetFloorMarkers(LastTerritory);
|
||||
EphemeralMarkers.Clear();
|
||||
PomanderOfSight = PomanderState.Inactive;
|
||||
PomanderOfIntuition = PomanderState.Inactive;
|
||||
recreateLayout = true;
|
||||
DebugMessage = null;
|
||||
_initCts.Cancel();
|
||||
_rootScope?.Dispose();
|
||||
_dependencyInjectionContext?.Dispose();
|
||||
}
|
||||
|
||||
if (!IsInDeepDungeon())
|
||||
return;
|
||||
|
||||
if (Service.Configuration.Mode == Configuration.EMode.Online && TerritorySyncState == SyncState.NotAttempted)
|
||||
private enum ELoadState
|
||||
{
|
||||
TerritorySyncState = SyncState.Started;
|
||||
Task.Run(async () => await DownloadMarkersForTerritory(LastTerritory));
|
||||
}
|
||||
|
||||
while (LateEventQueue.TryDequeue(out IQueueOnFrameworkThread? queued))
|
||||
queued?.Run(this, ref recreateLayout, ref saveMarkers);
|
||||
|
||||
var currentFloor = GetFloorMarkers(LastTerritory);
|
||||
|
||||
IList<Marker> visibleMarkers = GetRelevantGameObjects();
|
||||
HandlePersistentMarkers(currentFloor, visibleMarkers.Where(x => x.IsPermanent()).ToList(), saveMarkers, recreateLayout);
|
||||
HandleEphemeralMarkers(visibleMarkers.Where(x => !x.IsPermanent()).ToList(), recreateLayout);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugMessage = $"{DateTime.Now}\n{e}";
|
||||
}
|
||||
}
|
||||
|
||||
internal LocalState GetFloorMarkers(ushort territoryType)
|
||||
{
|
||||
return FloorMarkers.GetOrAdd(territoryType, tt => LocalState.Load(tt) ?? new LocalState(tt));
|
||||
}
|
||||
|
||||
private void HandlePersistentMarkers(LocalState currentFloor, IList<Marker> visibleMarkers, bool saveMarkers, bool recreateLayout)
|
||||
{
|
||||
var config = Service.Configuration;
|
||||
var currentFloorMarkers = currentFloor.Markers;
|
||||
|
||||
bool updateSeenMarkers = false;
|
||||
var accountId = Service.RemoteApi.AccountId;
|
||||
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 (accountId != null && knownMarker.NetworkId != null && !knownMarker.RemoteSeenRequested && !knownMarker.RemoteSeenOn.Contains(accountId.Value))
|
||||
updateSeenMarkers = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
currentFloorMarkers.Add(visibleMarker);
|
||||
recreateLayout = true;
|
||||
saveMarkers = true;
|
||||
}
|
||||
|
||||
if (!recreateLayout && currentFloorMarkers.Count > 0 && (config.OnlyVisibleTrapsAfterPomander || config.OnlyVisibleHoardAfterPomander))
|
||||
{
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var marker in currentFloorMarkers)
|
||||
{
|
||||
uint desiredColor = DetermineColor(marker, visibleMarkers);
|
||||
if (marker.SplatoonElement == null || !marker.SplatoonElement.IsValid())
|
||||
{
|
||||
recreateLayout = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (marker.SplatoonElement.color != desiredColor)
|
||||
marker.SplatoonElement.color = desiredColor;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugMessage = $"{DateTime.Now}\n{e}";
|
||||
recreateLayout = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updateSeenMarkers && accountId != null)
|
||||
{
|
||||
var markersToUpdate = currentFloorMarkers.Where(x => x.Seen && x.NetworkId != null && !x.RemoteSeenRequested && !x.RemoteSeenOn.Contains(accountId.Value)).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)
|
||||
{
|
||||
Splatoon.RemoveDynamicElements(SPLATOON_TRAP_HOARD);
|
||||
|
||||
List<Element> elements = new List<Element>();
|
||||
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)
|
||||
{
|
||||
var element = CreateSplatoonElement(marker.Type, marker.Position, DetermineColor(marker, visibleMarkers));
|
||||
marker.SplatoonElement = element;
|
||||
elements.Add(element);
|
||||
}
|
||||
else if (marker.Type == Marker.EType.Hoard && config.ShowHoard)
|
||||
{
|
||||
var element = CreateSplatoonElement(marker.Type, marker.Position, DetermineColor(marker, visibleMarkers));
|
||||
marker.SplatoonElement = element;
|
||||
elements.Add(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (elements.Count == 0)
|
||||
return;
|
||||
|
||||
// we need to delay this, as the current framework update could be before splatoon's, in which case it would immediately delete the layout
|
||||
new TickScheduler(delegate
|
||||
{
|
||||
try
|
||||
{
|
||||
Splatoon.AddDynamicElements(SPLATOON_TRAP_HOARD, elements.ToArray(), new long[] { Environment.TickCount64 + 60 * 60 * 1000, ON_TERRITORY_CHANGE });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugMessage = $"{DateTime.Now}\n{e}";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private uint DetermineColor(Marker marker, IList<Marker> visibleMarkers)
|
||||
{
|
||||
if (marker.Type == Marker.EType.Trap)
|
||||
{
|
||||
if (PomanderOfSight == PomanderState.Inactive || !Service.Configuration.OnlyVisibleTrapsAfterPomander || visibleMarkers.Any(x => x == marker))
|
||||
return ImGui.ColorConvertFloat4ToU32(Service.Configuration.TrapColor);
|
||||
else
|
||||
return COLOR_INVISIBLE;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (PomanderOfIntuition == PomanderState.Inactive || !Service.Configuration.OnlyVisibleHoardAfterPomander || visibleMarkers.Any(x => x == marker))
|
||||
return ImGui.ColorConvertFloat4ToU32(Service.Configuration.HoardColor);
|
||||
else
|
||||
return COLOR_INVISIBLE;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleEphemeralMarkers(IList<Marker> visibleMarkers, bool recreateLayout)
|
||||
{
|
||||
recreateLayout |= EphemeralMarkers.Any(existingMarker => !visibleMarkers.Any(x => x == existingMarker));
|
||||
recreateLayout |= visibleMarkers.Any(visibleMarker => !EphemeralMarkers.Any(x => x == visibleMarker));
|
||||
|
||||
if (recreateLayout)
|
||||
{
|
||||
Splatoon.RemoveDynamicElements(SPLATOON_REGULAR_COFFERS);
|
||||
EphemeralMarkers.Clear();
|
||||
|
||||
var config = Service.Configuration;
|
||||
|
||||
List<Element> elements = new List<Element>();
|
||||
foreach (var marker in visibleMarkers)
|
||||
{
|
||||
EphemeralMarkers.Add(marker);
|
||||
|
||||
if (marker.Type == Marker.EType.SilverCoffer && config.ShowSilverCoffers)
|
||||
{
|
||||
var element = CreateSplatoonElement(marker.Type, marker.Position, config.SilverCofferColor, config.FillSilverCoffers);
|
||||
marker.SplatoonElement = element;
|
||||
elements.Add(element);
|
||||
}
|
||||
}
|
||||
|
||||
if (elements.Count == 0)
|
||||
return;
|
||||
|
||||
new TickScheduler(delegate
|
||||
{
|
||||
try
|
||||
{
|
||||
Splatoon.AddDynamicElements(SPLATOON_REGULAR_COFFERS, elements.ToArray(), new long[] { Environment.TickCount64 + 60 * 60 * 1000, ON_TERRITORY_CHANGE });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugMessage = $"{DateTime.Now}\n{e}";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DownloadMarkersForTerritory(ushort territoryId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var (success, downloadedMarkers) = await Service.RemoteApi.DownloadRemoteMarkers(territoryId);
|
||||
LateEventQueue.Enqueue(new QueuedSyncResponse
|
||||
{
|
||||
Type = SyncType.Download,
|
||||
TerritoryType = territoryId,
|
||||
Success = success,
|
||||
Markers = downloadedMarkers
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugMessage = $"{DateTime.Now}\n{e}";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UploadMarkersForTerritory(ushort territoryId, List<Marker> markersToUpload)
|
||||
{
|
||||
try
|
||||
{
|
||||
var (success, uploadedMarkers) = await Service.RemoteApi.UploadMarker(territoryId, markersToUpload);
|
||||
LateEventQueue.Enqueue(new QueuedSyncResponse
|
||||
{
|
||||
Type = SyncType.Upload,
|
||||
TerritoryType = territoryId,
|
||||
Success = success,
|
||||
Markers = uploadedMarkers
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugMessage = $"{DateTime.Now}\n{e}";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SyncSeenMarkersForTerritory(ushort territoryId, List<Marker> markersToUpdate)
|
||||
{
|
||||
try
|
||||
{
|
||||
var success = await Service.RemoteApi.MarkAsSeen(territoryId, markersToUpdate);
|
||||
LateEventQueue.Enqueue(new QueuedSyncResponse
|
||||
{
|
||||
Type = SyncType.MarkSeen,
|
||||
TerritoryType = territoryId,
|
||||
Success = success,
|
||||
Markers = markersToUpdate,
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugMessage = $"{DateTime.Now}\n{e}";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FetchFloorStatistics()
|
||||
{
|
||||
if (!Service.RemoteApi.HasRoleOnCurrentServer("statistics:view"))
|
||||
{
|
||||
Service.Chat.Print("[Palace Pal] You can view statistics for the floor you're currently on by opening the 'Debug' tab in the configuration window.");
|
||||
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.PrintError("[Palace Pal] Unable to fetch statistics.");
|
||||
}
|
||||
}
|
||||
catch (RpcException e) when (e.StatusCode == StatusCode.PermissionDenied)
|
||||
{
|
||||
Service.Chat.Print("[Palace Pal] You can view statistics for the floor you're currently on by opening the 'Debug' tab in the configuration window.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Service.Chat.PrintError($"[Palace Pal] {e}");
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal bool IsInDeepDungeon() =>
|
||||
Service.ClientState.IsLoggedIn
|
||||
&& Service.Condition[ConditionFlag.InDeepDungeon]
|
||||
&& typeof(ETerritoryType).IsEnumDefined(Service.ClientState.TerritoryType);
|
||||
|
||||
internal static Element CreateSplatoonElement(Marker.EType type, Vector3 pos, Vector4 color, bool fill = false)
|
||||
=> CreateSplatoonElement(type, pos, ImGui.ColorConvertFloat4ToU32(color), fill);
|
||||
|
||||
internal static Element CreateSplatoonElement(Marker.EType type, Vector3 pos, uint color, bool fill = false)
|
||||
{
|
||||
return new Element(ElementType.CircleAtFixedCoordinates)
|
||||
{
|
||||
refX = pos.X,
|
||||
refY = pos.Z, // z and y are swapped
|
||||
refZ = pos.Y,
|
||||
offX = 0,
|
||||
offY = 0,
|
||||
offZ = _markerConfig[type].OffsetY,
|
||||
Filled = fill,
|
||||
radius = _markerConfig[type].Radius,
|
||||
FillStep = 1,
|
||||
color = color,
|
||||
thicc = 2,
|
||||
};
|
||||
}
|
||||
|
||||
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+)") + "$"),
|
||||
};
|
||||
}
|
||||
|
||||
private string GetLocalizedString(uint id)
|
||||
{
|
||||
return Service.DataManager.GetExcelSheet<LogMessage>()?.GetRow(id)?.Text?.ToString() ?? "Unknown";
|
||||
}
|
||||
|
||||
public enum PomanderState
|
||||
{
|
||||
Inactive,
|
||||
Active,
|
||||
FoundOnCurrentFloor,
|
||||
PomanderOfSafetyUsed,
|
||||
}
|
||||
|
||||
private class MarkerConfig
|
||||
{
|
||||
public float OffsetY { get; set; } = 0;
|
||||
public float Radius { get; set; } = 0.25f;
|
||||
}
|
||||
|
||||
private class LocalizedChatMessages
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
|
866
Pal.Client/Properties/Localization.Designer.cs
generated
Normal file
866
Pal.Client/Properties/Localization.Designer.cs
generated
Normal file
@ -0,0 +1,866 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace Pal.Client.Properties {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// 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", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Localization {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Localization() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Pal.Client.Properties.Localization", typeof(Localization).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Please chose one of the options above..
|
||||
/// </summary>
|
||||
internal static string Agreement_PickOneOption {
|
||||
get {
|
||||
return ResourceManager.GetString("Agreement_PickOneOption", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to I understand I'm using this plugin on my own risk..
|
||||
/// </summary>
|
||||
internal static string Agreement_UsingThisOnMyOwnRisk {
|
||||
get {
|
||||
return ResourceManager.GetString("Agreement_UsingThisOnMyOwnRisk", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to View plugin & server source code.
|
||||
/// </summary>
|
||||
internal static string Agreement_ViewPluginAndServerSourceCode {
|
||||
get {
|
||||
return ResourceManager.GetString("Agreement_ViewPluginAndServerSourceCode", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to While this is not an automation feature, you're still very likely to break the ToS..
|
||||
/// </summary>
|
||||
internal static string Agreement_Warning1 {
|
||||
get {
|
||||
return ResourceManager.GetString("Agreement_Warning1", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Other players in your party can always see where you're standing/walking..
|
||||
/// </summary>
|
||||
internal static string Agreement_Warning2 {
|
||||
get {
|
||||
return ResourceManager.GetString("Agreement_Warning2", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to As such, please avoid mentioning it in-game and do not share videos/screenshots..
|
||||
/// </summary>
|
||||
internal static string Agreement_Warning3 {
|
||||
get {
|
||||
return ResourceManager.GetString("Agreement_Warning3", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Open the configuration window.
|
||||
/// </summary>
|
||||
internal static string Command_pal_HelpText {
|
||||
get {
|
||||
return ResourceManager.GetString("Command_pal_HelpText", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to You can view statistics for the floor you're currently on by opening the 'Debug' tab in the configuration window..
|
||||
/// </summary>
|
||||
internal static string Command_pal_stats_CurrentFloor {
|
||||
get {
|
||||
return ResourceManager.GetString("Command_pal_stats_CurrentFloor", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Unable to fetch statistics..
|
||||
/// </summary>
|
||||
internal static string Command_pal_stats_UnableToFetchStatistics {
|
||||
get {
|
||||
return ResourceManager.GetString("Command_pal_stats_UnableToFetchStatistics", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Unknown sub-command '{0}' for '{1}'..
|
||||
/// </summary>
|
||||
internal static string Command_pal_UnknownSubcommand {
|
||||
get {
|
||||
return ResourceManager.GetString("Command_pal_UnknownSubcommand", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to You are NOT in a deep dungeon..
|
||||
/// </summary>
|
||||
internal static string Config_Debug_NotInADeepDungeon {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_Debug_NotInADeepDungeon", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Save as:.
|
||||
/// </summary>
|
||||
internal static string Config_Export_SaveAs {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_Export_SaveAs", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Export all markers from {0}:.
|
||||
/// </summary>
|
||||
internal static string Config_ExportSource {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_ExportSource", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
internal static string Config_HoardCoffers_Color {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_HoardCoffers_Color", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Hide hoard coffers not on current floor.
|
||||
/// </summary>
|
||||
internal static string Config_HoardCoffers_HideImpossible {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_HoardCoffers_HideImpossible", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to When using a Pomander of intuition, only the actual hoard coffer location is visible, all other (potential) hoard coffers are hidden..
|
||||
/// </summary>
|
||||
internal static string Config_HoardCoffers_HideImpossible_ToolTip {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_HoardCoffers_HideImpossible_ToolTip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Show hoard coffers.
|
||||
/// </summary>
|
||||
internal static string Config_HoardCoffers_Show {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_HoardCoffers_Show", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Visit GitHub.
|
||||
/// </summary>
|
||||
internal static string Config_Import_VisitGitHub {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_Import_VisitGitHub", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Exports are available from {0} (as *.pal files)..
|
||||
/// </summary>
|
||||
internal static string Config_ImportDownloadLocation {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_ImportDownloadLocation", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Using an export is useful if you're unable to connect to the server, or don't wish to share your findings..
|
||||
/// </summary>
|
||||
internal static string Config_ImportExplanation1 {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_ImportExplanation1", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Exports are (currently) generated manually, and they only include traps and hoard coffers encountered by 5 or more people. This may lead to higher floors having very sporadic coverage, but commonly run floors (e.g. PotD 51-60, HoH 21-30) are closer to complete..
|
||||
/// </summary>
|
||||
internal static string Config_ImportExplanation2 {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_ImportExplanation2", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to If you aren't offline, importing a file won't have any noticeable effect..
|
||||
/// </summary>
|
||||
internal static string Config_ImportExplanation3 {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_ImportExplanation3", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Never upload discoveries, show only traps and coffers I found myself.
|
||||
/// </summary>
|
||||
internal static string Config_NeverUploadDiscoveries_ShowMyTraps {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_NeverUploadDiscoveries_ShowMyTraps", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Simple.
|
||||
/// </summary>
|
||||
internal static string Config_Renderer_Simple {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_Renderer_Simple", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to experimental.
|
||||
/// </summary>
|
||||
internal static string Config_Renderer_Simple_Hint {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_Renderer_Simple_Hint", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Splatoon.
|
||||
/// </summary>
|
||||
internal static string Config_Renderer_Splatoon {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_Renderer_Splatoon", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to default, required Splatoon to be installed.
|
||||
/// </summary>
|
||||
internal static string Config_Renderer_Splatoon_Hint {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_Renderer_Splatoon_Hint", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to File to Import:.
|
||||
/// </summary>
|
||||
internal static string Config_SelectImportFile {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_SelectImportFile", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
internal static string Config_SelectRenderBackend {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_SelectRenderBackend", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Silver Coffer color.
|
||||
/// </summary>
|
||||
internal static string Config_SilverCoffer_Color {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_SilverCoffer_Color", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Draw filled.
|
||||
/// </summary>
|
||||
internal static string Config_SilverCoffer_Filled {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_SilverCoffer_Filled", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Show silver coffers on current floor.
|
||||
/// </summary>
|
||||
internal static string Config_SilverCoffer_Show {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_SilverCoffer_Show", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_SilverCoffers_ToolTip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Draw trap & coffer circles around self.
|
||||
/// </summary>
|
||||
internal static string Config_Splatoon_DrawCircles {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_Splatoon_DrawCircles", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Start Export.
|
||||
/// </summary>
|
||||
internal static string Config_StartExport {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_StartExport", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Start Import.
|
||||
/// </summary>
|
||||
internal static string Config_StartImport {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_StartImport", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Test Connection.
|
||||
/// </summary>
|
||||
internal static string Config_TestConnection {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_TestConnection", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
internal static string Config_Traps_Color {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_Traps_Color", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Hide traps not on current floor.
|
||||
/// </summary>
|
||||
internal static string Config_Traps_HideImpossible {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_Traps_HideImpossible", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to When using a Pomander of Sight, only the actual trap locations are visible, all other traps are hidden.
|
||||
///When using a Pomander of Safety, all traps are hidden..
|
||||
/// </summary>
|
||||
internal static string Config_Traps_HideImpossible_ToolTip {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_Traps_HideImpossible_ToolTip", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Show traps.
|
||||
/// </summary>
|
||||
internal static string Config_Traps_Show {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_Traps_Show", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Undo Import.
|
||||
/// </summary>
|
||||
internal static string Config_UndoImport {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_UndoImport", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Your last import was on {0}, which added the trap/hoard coffer database from {1} created on {2:d}..
|
||||
/// </summary>
|
||||
internal static string Config_UndoImportExplanation1 {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_UndoImportExplanation1", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to If you think that was a mistake, you can remove all locations only found in the import (any location you've seen yourself is not changed)..
|
||||
/// </summary>
|
||||
internal static string Config_UndoImportExplanation2 {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_UndoImportExplanation2", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Upload my discoveries, show traps & coffers other players have discovered.
|
||||
/// </summary>
|
||||
internal static string Config_UploadMyDiscoveries_ShowOtherTraps {
|
||||
get {
|
||||
return ResourceManager.GetString("Config_UploadMyDiscoveries_ShowOtherTraps", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Community.
|
||||
/// </summary>
|
||||
internal static string ConfigTab_Community {
|
||||
get {
|
||||
return ResourceManager.GetString("ConfigTab_Community", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Debug.
|
||||
/// </summary>
|
||||
internal static string ConfigTab_Debug {
|
||||
get {
|
||||
return ResourceManager.GetString("ConfigTab_Debug", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Deep Dungeons.
|
||||
/// </summary>
|
||||
internal static string ConfigTab_DeepDungeons {
|
||||
get {
|
||||
return ResourceManager.GetString("ConfigTab_DeepDungeons", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Export.
|
||||
/// </summary>
|
||||
internal static string ConfigTab_Export {
|
||||
get {
|
||||
return ResourceManager.GetString("ConfigTab_Export", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Import.
|
||||
/// </summary>
|
||||
internal static string ConfigTab_Import {
|
||||
get {
|
||||
return ResourceManager.GetString("ConfigTab_Import", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Renderer.
|
||||
/// </summary>
|
||||
internal static string ConfigTab_Renderer {
|
||||
get {
|
||||
return ResourceManager.GetString("ConfigTab_Renderer", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Could not connect to server: {0}.
|
||||
/// </summary>
|
||||
internal static string ConnectionError_CouldNotConnectToServer {
|
||||
get {
|
||||
return ResourceManager.GetString("ConnectionError_CouldNotConnectToServer", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Could not create account ({0})..
|
||||
/// </summary>
|
||||
internal static string ConnectionError_CreateAccountFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("ConnectionError_CreateAccountFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to No account-id after account was attempted to be created..
|
||||
/// </summary>
|
||||
internal static string ConnectionError_CreateAccountReturnedNoId {
|
||||
get {
|
||||
return ResourceManager.GetString("ConnectionError_CreateAccountReturnedNoId", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Invalid account id..
|
||||
/// </summary>
|
||||
internal static string ConnectionError_InvalidAccountId {
|
||||
get {
|
||||
return ResourceManager.GetString("ConnectionError_InvalidAccountId", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Could not log in ({0})..
|
||||
/// </summary>
|
||||
internal static string ConnectionError_LoginFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("ConnectionError_LoginFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to No login information available..
|
||||
/// </summary>
|
||||
internal static string ConnectionError_LoginReturnedNoToken {
|
||||
get {
|
||||
return ResourceManager.GetString("ConnectionError_LoginReturnedNoToken", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to You are not online..
|
||||
/// </summary>
|
||||
internal static string ConnectionError_NotOnline {
|
||||
get {
|
||||
return ResourceManager.GetString("ConnectionError_NotOnline", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Your version of Palace Pal is outdated, please update the plugin using the Plugin Installer..
|
||||
/// </summary>
|
||||
internal static string ConnectionError_OldVersion {
|
||||
get {
|
||||
return ResourceManager.GetString("ConnectionError_OldVersion", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Connection successful..
|
||||
/// </summary>
|
||||
internal static string ConnectionSuccessful {
|
||||
get {
|
||||
return ResourceManager.GetString("ConnectionSuccessful", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Command could not be executed: {0}.
|
||||
/// </summary>
|
||||
internal static string Error_CommandFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("Error_CommandFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Please finish the initial setup first..
|
||||
/// </summary>
|
||||
internal static string Error_FirstTimeSetupRequired {
|
||||
get {
|
||||
return ResourceManager.GetString("Error_FirstTimeSetupRequired", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Import failed: {0}.
|
||||
/// </summary>
|
||||
internal static string Error_ImportFailed {
|
||||
get {
|
||||
return ResourceManager.GetString("Error_ImportFailed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Import failed: Incompatible version..
|
||||
/// </summary>
|
||||
internal static string Error_ImportFailed_IncompatibleVersion {
|
||||
get {
|
||||
return ResourceManager.GetString("Error_ImportFailed_IncompatibleVersion", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Import failed: Invalid file..
|
||||
/// </summary>
|
||||
internal static string Error_ImportFailed_InvalidFile {
|
||||
get {
|
||||
return ResourceManager.GetString("Error_ImportFailed_InvalidFile", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
internal static string Error_WrongRepository {
|
||||
get {
|
||||
return ResourceManager.GetString("Error_WrongRepository", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Eureka Orthos.
|
||||
/// </summary>
|
||||
internal static string EurekaOrthos {
|
||||
get {
|
||||
return ResourceManager.GetString("EurekaOrthos", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Pal Palace will show where potential trap & hoard coffer locations are..
|
||||
/// </summary>
|
||||
internal static string Explanation_1 {
|
||||
get {
|
||||
return ResourceManager.GetString("Explanation_1", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to To do this, using a pomander to reveal trap or treasure chest locations will save the position of what you see..
|
||||
/// </summary>
|
||||
internal static string Explanation_2 {
|
||||
get {
|
||||
return ResourceManager.GetString("Explanation_2", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Ideally, we want to discover every potential trap and chest location in the game, but doing this alone is very tedious. Floor 51-60 has over 300 trap locations and over 290 coffer locations - and we don't know if that map is complete. Higher floors naturally see fewer runs, making solo attempts to map the place much harder..
|
||||
/// </summary>
|
||||
internal static string Explanation_3 {
|
||||
get {
|
||||
return ResourceManager.GetString("Explanation_3", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to You can decide whether you want to share traps and chests you find with the community, which likewise also will let you see chests and coffers found by other players. This can be changed at any time. No data regarding your FFXIV character or account is ever sent to our server..
|
||||
/// </summary>
|
||||
internal static string Explanation_4 {
|
||||
get {
|
||||
return ResourceManager.GetString("Explanation_4", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Heaven on High.
|
||||
/// </summary>
|
||||
internal static string HeavenOnHigh {
|
||||
get {
|
||||
return ResourceManager.GetString("HeavenOnHigh", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Imported {0} new trap locations and {1} new hoard coffer locations..
|
||||
/// </summary>
|
||||
internal static string ImportCompleteStatistics {
|
||||
get {
|
||||
return ResourceManager.GetString("ImportCompleteStatistics", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Palace Pal.
|
||||
/// </summary>
|
||||
internal static string Palace_Pal {
|
||||
get {
|
||||
return ResourceManager.GetString("Palace_Pal", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Palace of the Dead.
|
||||
/// </summary>
|
||||
internal static string PalaceOfTheDead {
|
||||
get {
|
||||
return ResourceManager.GetString("PalaceOfTheDead", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Save.
|
||||
/// </summary>
|
||||
internal static string Save {
|
||||
get {
|
||||
return ResourceManager.GetString("Save", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Save & Close.
|
||||
/// </summary>
|
||||
internal static string SaveAndClose {
|
||||
get {
|
||||
return ResourceManager.GetString("SaveAndClose", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Statistics.
|
||||
/// </summary>
|
||||
internal static string Statistics {
|
||||
get {
|
||||
return ResourceManager.GetString("Statistics", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Hoard.
|
||||
/// </summary>
|
||||
internal static string Statistics_HoardCoffers {
|
||||
get {
|
||||
return ResourceManager.GetString("Statistics_HoardCoffers", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Instance Name.
|
||||
/// </summary>
|
||||
internal static string Statistics_InstanceName {
|
||||
get {
|
||||
return ResourceManager.GetString("Statistics_InstanceName", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Id.
|
||||
/// </summary>
|
||||
internal static string Statistics_TerritoryId {
|
||||
get {
|
||||
return ResourceManager.GetString("Statistics_TerritoryId", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Traps.
|
||||
/// </summary>
|
||||
internal static string Statistics_Traps {
|
||||
get {
|
||||
return ResourceManager.GetString("Statistics_Traps", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
63
Pal.Client/Properties/Localization.de.resx
Normal file
63
Pal.Client/Properties/Localization.de.resx
Normal file
@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>1.3</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<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'. -->
|
||||
<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 (chat messages) -->
|
||||
<!-- Other -->
|
||||
</root>
|
307
Pal.Client/Properties/Localization.fr.resx
Normal file
307
Pal.Client/Properties/Localization.fr.resx
Normal file
@ -0,0 +1,307 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>1.3</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<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'. -->
|
||||
<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 -->
|
||||
<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>
|
324
Pal.Client/Properties/Localization.ja.resx
Normal file
324
Pal.Client/Properties/Localization.ja.resx
Normal file
@ -0,0 +1,324 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>1.3</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<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>死者の宮殿</value>
|
||||
</data>
|
||||
<data name="HeavenOnHigh" xml:space="preserve">
|
||||
<value>アメノミハシラ</value>
|
||||
</data>
|
||||
<data name="EurekaOrthos" xml:space="preserve">
|
||||
<value>オルト・エウレカ</value>
|
||||
</data>
|
||||
<data name="Save" xml:space="preserve">
|
||||
<value>保存</value>
|
||||
</data>
|
||||
<data name="SaveAndClose" xml:space="preserve">
|
||||
<value>保存して閉じる</value>
|
||||
</data>
|
||||
<!-- Generic Errors -->
|
||||
<data name="Error_FirstTimeSetupRequired" xml:space="preserve">
|
||||
<value>最初にセットアップを完了してください。</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>引き続き使用する場合は公式リポジトリ {0} からこのプラグインをインストールしてください。</value>
|
||||
</data>
|
||||
<!-- /pal commands -->
|
||||
<data name="Command_pal_HelpText" xml:space="preserve">
|
||||
<value>設定を開く</value>
|
||||
<comment>Help text for the /pal command, shown in the Plugin Installer</comment>
|
||||
</data>
|
||||
<data name="Command_pal_UnknownSubcommand" xml:space="preserve">
|
||||
<value>そのコマンドはありません。: '{0}' for '{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>設定からDebugタブを開くことで、現在のフロアの統計情報を表示できます。</value>
|
||||
</data>
|
||||
<data name="Command_pal_stats_UnableToFetchStatistics" xml:space="preserve">
|
||||
<value>統計情報を取得できません。</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'. -->
|
||||
<data name="ConnectionSuccessful" xml:space="preserve">
|
||||
<value>接続に成功しました。</value>
|
||||
</data>
|
||||
<data name="ConnectionError_NotOnline" xml:space="preserve">
|
||||
<value>接続に失敗しました。</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>Palace Palのバージョンが古くなっています。プラグインインストーラを使用してプラグインを更新してください。</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>アカウントを作成できませんでした。({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>アカウントが作成されようとしましたが、アカウントIDがありません。</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>無効なアカウントIDです。</value>
|
||||
<comment>The account id used was not found on the server.</comment>
|
||||
</data>
|
||||
<data name="ConnectionError_LoginFailed" xml:space="preserve">
|
||||
<value>ログインできませんでした 。({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>ログイン情報がありません。</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>サーバーに接続できません: {0}</value>
|
||||
</data>
|
||||
<!-- Config Window: Deep Dungeons -->
|
||||
<data name="ConfigTab_DeepDungeons" xml:space="preserve">
|
||||
<value>ディープダンジョン</value>
|
||||
</data>
|
||||
<data name="Config_Traps_Show" xml:space="preserve">
|
||||
<value>トラップを表示</value>
|
||||
</data>
|
||||
<data name="Config_Traps_Color" xml:space="preserve">
|
||||
<value>トラップの色</value>
|
||||
</data>
|
||||
<data name="Config_Traps_HideImpossible" xml:space="preserve">
|
||||
<value>現在のフロアに存在しないトラップを非表示</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>サイトロを使用した場合、存在するトラップの位置のみが表示されます。
|
||||
呪印解除を使用した場合、全てのトラップが非表示になります。</value>
|
||||
</data>
|
||||
<data name="Config_HoardCoffers_Show" xml:space="preserve">
|
||||
<value>埋もれた財宝を表示</value>
|
||||
</data>
|
||||
<data name="Config_HoardCoffers_Color" xml:space="preserve">
|
||||
<value>埋もれた財宝の色</value>
|
||||
</data>
|
||||
<data name="Config_HoardCoffers_HideImpossible" xml:space="preserve">
|
||||
<value>現在のフロアに存在しない埋もれた財宝を非表示</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>財宝感知を使用した場合、現在のフロアに埋もれた財宝が存在する場合は表示されます。
|
||||
存在しない場合は非表示になります。</value>
|
||||
</data>
|
||||
<data name="Config_SilverCoffer_Show" xml:space="preserve">
|
||||
<value>銀の宝箱を表示</value>
|
||||
</data>
|
||||
<data name="Config_SilverCoffers_ToolTip" xml:space="preserve">
|
||||
<value>現在のフロアにある全ての銀の宝箱を表示します。
|
||||
これは他のプレイヤーと同期されず、データは保存されません。</value>
|
||||
</data>
|
||||
<data name="Config_SilverCoffer_Color" xml:space="preserve">
|
||||
<value>銀の宝箱の色</value>
|
||||
</data>
|
||||
<data name="Config_SilverCoffer_Filled" xml:space="preserve">
|
||||
<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>
|
||||
<data name="Config_ImportExplanation1" xml:space="preserve">
|
||||
<value>エクスポートを使用すると、サーバーに接続できない場合や、情報を共有したくない場合に便利です。</value>
|
||||
</data>
|
||||
<data name="Config_ImportExplanation2" xml:space="preserve">
|
||||
<value>エクスポートは(現在)手動で生成され、5人以上が遭遇したトラップ/宝箱のみが含まれています。
|
||||
深層の情報は不安要素が多くなりますが、多くのプレイヤーが挑戦するフロア(死者の宮殿51F-60F、アメノミハシラ21F-30F)は完成に近付いています。</value>
|
||||
</data>
|
||||
<data name="Config_ImportExplanation3" xml:space="preserve">
|
||||
<value>オフラインモードで利用していない場合は、ファイルをインポートしても顕著な効果を得られません。</value>
|
||||
</data>
|
||||
<data name="Config_ImportDownloadLocation" xml:space="preserve">
|
||||
<value>エクスポートは {0} (*.pal ファイルとして) から入手できます。</value>
|
||||
</data>
|
||||
<data name="Config_Import_VisitGitHub" xml:space="preserve">
|
||||
<value>GitHubを開く</value>
|
||||
</data>
|
||||
<data name="Config_SelectImportFile" xml:space="preserve">
|
||||
<value>ファイルを選択</value>
|
||||
</data>
|
||||
<data name="Config_StartImport" xml:space="preserve">
|
||||
<value>インポート</value>
|
||||
</data>
|
||||
<data name="Config_UndoImportExplanation1" xml:space="preserve">
|
||||
<value>あなたが最後にインポートしたファイルは{0}です。
|
||||
{1}が{2:d}に作成したデータベースです。</value>
|
||||
</data>
|
||||
<data name="Config_UndoImportExplanation2" xml:space="preserve">
|
||||
<value>間違いだと思われる場合は、インポートで見つかったすべての場所を削除することができます(自分で見た場所は変更されません)。</value>
|
||||
</data>
|
||||
<data name="Config_UndoImport" xml:space="preserve">
|
||||
<value>インポートを取り消す</value>
|
||||
</data>
|
||||
<!-- Config Window: Export -->
|
||||
<data name="ConfigTab_Export" xml:space="preserve">
|
||||
<value>エクスポート</value>
|
||||
</data>
|
||||
<data name="Config_ExportSource" xml:space="preserve">
|
||||
<value>{0} から全てのマーカーをエクスポート</value>
|
||||
</data>
|
||||
<data name="Config_Export_SaveAs" xml:space="preserve">
|
||||
<value>名前をつけて保存</value>
|
||||
</data>
|
||||
<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>
|
||||
</data>
|
||||
<data name="Config_SelectRenderBackend" xml:space="preserve">
|
||||
<value>マーカーに使用するバックエンドを選択:</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>デフォルトではSpatoonをインストールする必要があります</value>
|
||||
</data>
|
||||
<data name="Config_Renderer_Simple" xml:space="preserve">
|
||||
<value>シンプル表示</value>
|
||||
</data>
|
||||
<data name="Config_Renderer_Simple_Hint" xml:space="preserve">
|
||||
<value>試験的機能</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>
|
||||
<data name="Config_Debug_NotInADeepDungeon" xml:space="preserve">
|
||||
<value>あなたはディープダンジョンにいません。</value>
|
||||
</data>
|
||||
<!-- Statistics Window -->
|
||||
<data name="Statistics" xml:space="preserve">
|
||||
<value>統計</value>
|
||||
</data>
|
||||
<data name="Statistics_TerritoryId" xml:space="preserve">
|
||||
<value>エリアID</value>
|
||||
</data>
|
||||
<data name="Statistics_InstanceName" xml:space="preserve">
|
||||
<value>インスタンス名</value>
|
||||
</data>
|
||||
<data name="Statistics_Traps" xml:space="preserve">
|
||||
<value>トラップ</value>
|
||||
</data>
|
||||
<data name="Statistics_HoardCoffers" xml:space="preserve">
|
||||
<value>埋もれた財宝</value>
|
||||
</data>
|
||||
<!-- Agreement Window -->
|
||||
<data name="Explanation_1" xml:space="preserve">
|
||||
<value>Pal Palaceは、潜在的なトラップ/宝箱がある場所を表示します。</value>
|
||||
</data>
|
||||
<data name="Explanation_2" xml:space="preserve">
|
||||
<value>これを行うには、土器を使用してトラップや宝箱の場所を明らかにすると、あなたが見ているものの位置が保存されます。</value>
|
||||
</data>
|
||||
<data name="Explanation_3" xml:space="preserve">
|
||||
<value>理想的には、我々はゲーム内のすべての潜在的なトラップと宝箱の位置を発見したいです。しかし、これを1人で行うことは非常に退屈です。フロア51-60には300箇所以上のトラップがあり、290箇所以上の宝箱があります。 フロアが高ければ高いほど、試行回数が少なくなり、ソロではマッピングがより難しくなります。</value>
|
||||
</data>
|
||||
<data name="Explanation_4" xml:space="preserve">
|
||||
<value>トラップと宝箱をコミュニティと共有するかどうかを選択できます。
|
||||
他のプレイヤーが見つけたトラップや宝箱も表示できます。
|
||||
これはいつでも変更することができます。
|
||||
あなたのFFXIVキャラクターに関するデータやアカウント情報は送信されることはありません。</value>
|
||||
</data>
|
||||
<data name="Config_UploadMyDiscoveries_ShowOtherTraps" xml:space="preserve">
|
||||
<value>自分で見つけたトラップ/宝箱の情報をアップロードし、他のプレイヤーが発見したトラップ/宝箱を表示します</value>
|
||||
</data>
|
||||
<data name="Config_NeverUploadDiscoveries_ShowMyTraps" xml:space="preserve">
|
||||
<value>データをアップロードせず、自分で発見したトラップと埋もれた財宝だけを表示する</value>
|
||||
</data>
|
||||
<data name="Agreement_Warning1" xml:space="preserve">
|
||||
<value>これは自動化機能ではありませんが、ToSを壊す可能性は非常に高いです。</value>
|
||||
</data>
|
||||
<data name="Agreement_Warning2" xml:space="preserve">
|
||||
<value>あなたのパーティーの他のプレイヤーは、あなたが立っている/歩く場所をいつでも見ることができます。</value>
|
||||
</data>
|
||||
<data name="Agreement_Warning3" xml:space="preserve">
|
||||
<value>そのため、ゲーム内で言及することは避けて、ビデオ/スクリーンショットを共有しないでください。</value>
|
||||
</data>
|
||||
<data name="Agreement_UsingThisOnMyOwnRisk" xml:space="preserve">
|
||||
<value>私は自分の責任でこのプラグインを使用していることを理解しています.</value>
|
||||
</data>
|
||||
<data name="Agreement_ViewPluginAndServerSourceCode" xml:space="preserve">
|
||||
<value>プラグインとサーバーのソースコードを表示</value>
|
||||
</data>
|
||||
<data name="Agreement_PickOneOption" xml:space="preserve">
|
||||
<value>以下のいずれかのオプションを選択してください。</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>{0} 個の新しいトラップの場所と {1} 個の新しい宝箱の場所をインポートしました。</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>インポートに失敗しました。
|
||||
{0}</value>
|
||||
</data>
|
||||
<data name="Error_ImportFailed_IncompatibleVersion" xml:space="preserve">
|
||||
<value>インポートに失敗しました: 互換性のないバージョンです。</value>
|
||||
</data>
|
||||
<data name="Error_ImportFailed_InvalidFile" xml:space="preserve">
|
||||
<value>インポートに失敗しました: 無効なファイルです。</value>
|
||||
</data>
|
||||
<!-- Other -->
|
||||
</root>
|
344
Pal.Client/Properties/Localization.resx
Normal file
344
Pal.Client/Properties/Localization.resx
Normal file
@ -0,0 +1,344 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>1.3</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<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>Palace of the Dead</value>
|
||||
</data>
|
||||
<data name="HeavenOnHigh" xml:space="preserve">
|
||||
<value>Heaven on High</value>
|
||||
</data>
|
||||
<data name="EurekaOrthos" xml:space="preserve">
|
||||
<value>Eureka Orthos</value>
|
||||
</data>
|
||||
|
||||
<data name="Save" xml:space="preserve">
|
||||
<value>Save</value>
|
||||
</data>
|
||||
<data name="SaveAndClose" xml:space="preserve">
|
||||
<value>Save & Close</value>
|
||||
</data>
|
||||
|
||||
<!-- Generic Errors -->
|
||||
<data name="Error_FirstTimeSetupRequired" xml:space="preserve">
|
||||
<value>Please finish the initial setup first.</value>
|
||||
<comment>Before using any /pal command, the initial setup/agreeement needs to be completed.</comment>
|
||||
</data>
|
||||
<data name="Error_LoadFailed" xml:space="preserve">
|
||||
<value>Plugin could not be loaded: {0}</value>
|
||||
<comment>Shown when the plugin fails to load, with the placeholder filled with the exception message.</comment>
|
||||
</data>
|
||||
<data name="Error_WrongRepository" xml:space="preserve">
|
||||
<value>Please install this plugin from the official repository at {0} to continue using it.</value>
|
||||
</data>
|
||||
|
||||
<!-- /pal commands -->
|
||||
<data name="Command_pal_HelpText" xml:space="preserve">
|
||||
<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">
|
||||
<value>Unknown sub-command '{0}' for '{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>You can view statistics for the floor you're currently on by opening the 'Debug' tab in the configuration window.</value>
|
||||
</data>
|
||||
<data name="Command_pal_stats_UnableToFetchStatistics" xml:space="preserve">
|
||||
<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="Error_CommandFailed" xml:space="preserve">
|
||||
<value>Command could not be executed: {0}</value>
|
||||
<comment>Shown when '/pal ...' fails, with the placeholder filled with the exception message.</comment>
|
||||
</data>
|
||||
|
||||
<!-- Messages while connecting to the server. These are typically only visible when clicking 'Test connection'. -->
|
||||
<data name="ConnectionSuccessful" xml:space="preserve">
|
||||
<value>Connection successful.</value>
|
||||
</data>
|
||||
<data name="ConnectionError_NotOnline" xml:space="preserve">
|
||||
<value>You are not 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>Your version of Palace Pal is outdated, please update the plugin using the Plugin Installer.</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>Could not create account ({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>No account-id after account was attempted to be created.</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>Invalid account id.</value>
|
||||
<comment>The account id used was not found on the server.</comment>
|
||||
</data>
|
||||
<data name="ConnectionError_LoginFailed" xml:space="preserve">
|
||||
<value>Could not log in ({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>No login information available.</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>Could not connect to server: {0}</value>
|
||||
</data>
|
||||
|
||||
<!-- Config Window: Deep Dungeons -->
|
||||
<data name="ConfigTab_DeepDungeons" xml:space="preserve">
|
||||
<value>Deep Dungeons</value>
|
||||
</data>
|
||||
<data name="Config_Traps_Show" xml:space="preserve">
|
||||
<value>Show traps</value>
|
||||
</data>
|
||||
<data name="Config_Traps_Color" xml:space="preserve">
|
||||
<value>Trap color</value>
|
||||
</data>
|
||||
<data name="Config_Traps_HideImpossible" xml:space="preserve">
|
||||
<value>Hide traps not on current floor</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>When using a Pomander of Sight, only the actual trap locations are visible, all other traps are hidden.
|
||||
When using a Pomander of Safety, all traps are hidden.</value>
|
||||
</data>
|
||||
<data name="Config_HoardCoffers_Show" xml:space="preserve">
|
||||
<value>Show hoard coffers</value>
|
||||
</data>
|
||||
<data name="Config_HoardCoffers_Color" xml:space="preserve">
|
||||
<value>Hoard Coffer color</value>
|
||||
</data>
|
||||
<data name="Config_HoardCoffers_HideImpossible" xml:space="preserve">
|
||||
<value>Hide hoard coffers not on current floor</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>When using a Pomander of intuition, only the actual hoard coffer location is visible, all other (potential) hoard coffers are hidden.</value>
|
||||
</data>
|
||||
<data name="Config_SilverCoffer_Show" xml:space="preserve">
|
||||
<value>Show silver coffers on current floor</value>
|
||||
</data>
|
||||
<data name="Config_SilverCoffers_ToolTip" xml:space="preserve">
|
||||
<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">
|
||||
<value>Silver Coffer color</value>
|
||||
</data>
|
||||
<data name="Config_SilverCoffer_Filled" xml:space="preserve">
|
||||
<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>
|
||||
<data name="Config_ImportExplanation1" xml:space="preserve">
|
||||
<value>Using an export is useful if you're unable to connect to the server, or don't wish to share your findings.</value>
|
||||
</data>
|
||||
<data name="Config_ImportExplanation2" xml:space="preserve">
|
||||
<value>Exports are (currently) generated manually, and they only include traps and hoard coffers encountered by 5 or more people. This may lead to higher floors having very sporadic coverage, but commonly run floors (e.g. PotD 51-60, HoH 21-30) are closer to complete.</value>
|
||||
</data>
|
||||
<data name="Config_ImportExplanation3" xml:space="preserve">
|
||||
<value>If you aren't offline, importing a file won't have any noticeable effect.</value>
|
||||
</data>
|
||||
<data name="Config_ImportDownloadLocation" xml:space="preserve">
|
||||
<value>Exports are available from {0} (as *.pal files).</value>
|
||||
</data>
|
||||
<data name="Config_Import_VisitGitHub" xml:space="preserve">
|
||||
<value>Visit GitHub</value>
|
||||
</data>
|
||||
<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>
|
||||
<data name="Config_UndoImportExplanation1" xml:space="preserve">
|
||||
<value>Your last import was on {0}, which added the trap/hoard coffer database from {1} created on {2:d}.</value>
|
||||
</data>
|
||||
<data name="Config_UndoImportExplanation2" xml:space="preserve">
|
||||
<value>If you think that was a mistake, you can remove all locations only found in the import (any location you've seen yourself is not changed).</value>
|
||||
</data>
|
||||
<data name="Config_UndoImport" xml:space="preserve">
|
||||
<value>Undo Import</value>
|
||||
</data>
|
||||
|
||||
<!-- Config Window: Export -->
|
||||
<data name="ConfigTab_Export" xml:space="preserve">
|
||||
<value>Export</value>
|
||||
</data>
|
||||
<data name="Config_ExportSource" xml:space="preserve">
|
||||
<value>Export all markers from {0}:</value>
|
||||
</data>
|
||||
<data name="Config_Export_SaveAs" xml:space="preserve">
|
||||
<value>Save as:</value>
|
||||
</data>
|
||||
<data name="Config_StartExport" xml:space="preserve">
|
||||
<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>
|
||||
</data>
|
||||
<data name="Config_SelectRenderBackend" xml:space="preserve">
|
||||
<value>Select which render backend to use for markers:</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>default, required Splatoon to be installed</value>
|
||||
</data>
|
||||
<data name="Config_Renderer_Simple" xml:space="preserve">
|
||||
<value>Simple</value>
|
||||
</data>
|
||||
<data name="Config_Renderer_Simple_Hint" xml:space="preserve">
|
||||
<value>experimental</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>
|
||||
<data name="Config_Debug_NotInADeepDungeon" xml:space="preserve">
|
||||
<value>You are NOT in a deep dungeon.</value>
|
||||
</data>
|
||||
|
||||
<!-- Statistics Window -->
|
||||
<data name="Statistics" xml:space="preserve">
|
||||
<value>Statistics</value>
|
||||
</data>
|
||||
<data name="Statistics_TerritoryId" xml:space="preserve">
|
||||
<value>Id</value>
|
||||
</data>
|
||||
<data name="Statistics_InstanceName" xml:space="preserve">
|
||||
<value>Instance Name</value>
|
||||
</data>
|
||||
<data name="Statistics_Traps" xml:space="preserve">
|
||||
<value>Traps</value>
|
||||
</data>
|
||||
<data name="Statistics_HoardCoffers" xml:space="preserve">
|
||||
<value>Hoard</value>
|
||||
</data>
|
||||
|
||||
<!-- Agreement Window -->
|
||||
<data name="Explanation_1" xml:space="preserve">
|
||||
<value>Pal Palace will show where potential trap & hoard coffer locations are.</value>
|
||||
</data>
|
||||
<data name="Explanation_2" xml:space="preserve">
|
||||
<value>To do this, using a pomander to reveal trap or treasure chest locations will save the position of what you see.</value>
|
||||
</data>
|
||||
<data name="Explanation_3" xml:space="preserve">
|
||||
<value>Ideally, we want to discover every potential trap and chest location in the game, but doing this alone is very tedious. Floor 51-60 has over 300 trap locations and over 290 coffer locations - and we don't know if that map is complete. Higher floors naturally see fewer runs, making solo attempts to map the place much harder.</value>
|
||||
</data>
|
||||
<data name="Explanation_4" xml:space="preserve">
|
||||
<value>You can decide whether you want to share traps and chests you find with the community, which likewise also will let you see chests and coffers found by other players. This can be changed at any time. No data regarding your FFXIV character or account is ever sent to our server.</value>
|
||||
</data>
|
||||
<data name="Config_UploadMyDiscoveries_ShowOtherTraps" xml:space="preserve">
|
||||
<value>Upload my discoveries, show traps & coffers other players have discovered</value>
|
||||
</data>
|
||||
<data name="Config_NeverUploadDiscoveries_ShowMyTraps" xml:space="preserve">
|
||||
<value>Never upload discoveries, show only traps and coffers I found myself</value>
|
||||
</data>
|
||||
<data name="Agreement_Warning1" xml:space="preserve">
|
||||
<value>While this is not an automation feature, you're still very likely to break the ToS.</value>
|
||||
</data>
|
||||
<data name="Agreement_Warning2" xml:space="preserve">
|
||||
<value>Other players in your party can always see where you're standing/walking.</value>
|
||||
</data>
|
||||
<data name="Agreement_Warning3" xml:space="preserve">
|
||||
<value>As such, please avoid mentioning it in-game and do not share videos/screenshots.</value>
|
||||
</data>
|
||||
<data name="Agreement_UsingThisOnMyOwnRisk" xml:space="preserve">
|
||||
<value>I understand I'm using this plugin on my own risk.</value>
|
||||
</data>
|
||||
<data name="Agreement_ViewPluginAndServerSourceCode" xml:space="preserve">
|
||||
<value>View plugin & server source code</value>
|
||||
</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 setup setup window are selected.</comment>
|
||||
</data>
|
||||
|
||||
<!-- 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>
|
||||
</data>
|
||||
<data name="Error_ImportFailed" xml:space="preserve">
|
||||
<value>Import failed: {0}</value>
|
||||
</data>
|
||||
<data name="Error_ImportFailed_IncompatibleVersion" xml:space="preserve">
|
||||
<value>Import failed: Incompatible version.</value>
|
||||
</data>
|
||||
<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
|
||||
```
|
8
Pal.Client/Rendering/ELayer.cs
Normal file
8
Pal.Client/Rendering/ELayer.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Pal.Client.Rendering;
|
||||
|
||||
internal enum ELayer
|
||||
{
|
||||
TrapHoard,
|
||||
RegularCoffers,
|
||||
Test,
|
||||
}
|
8
Pal.Client/Rendering/IRenderElement.cs
Normal file
8
Pal.Client/Rendering/IRenderElement.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Pal.Client.Rendering;
|
||||
|
||||
public interface IRenderElement
|
||||
{
|
||||
bool IsValid { get; }
|
||||
|
||||
bool Enabled { get; set; }
|
||||
}
|
19
Pal.Client/Rendering/IRenderer.cs
Normal file
19
Pal.Client/Rendering/IRenderer.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Pal.Client.Configuration;
|
||||
using Pal.Client.Floors;
|
||||
|
||||
namespace Pal.Client.Rendering;
|
||||
|
||||
internal interface IRenderer
|
||||
{
|
||||
ERenderer GetConfigValue();
|
||||
|
||||
void SetLayer(ELayer layer, IReadOnlyList<IRenderElement> elements);
|
||||
|
||||
void ResetLayer(ELayer layer);
|
||||
|
||||
IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, bool enabled, uint color, bool fill = false);
|
||||
|
||||
void DrawDebugItems(uint trapColor, uint hoardColor);
|
||||
}
|
23
Pal.Client/Rendering/MarkerConfig.cs
Normal file
23
Pal.Client/Rendering/MarkerConfig.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
using Pal.Client.Floors;
|
||||
|
||||
namespace Pal.Client.Rendering;
|
||||
|
||||
internal sealed class MarkerConfig
|
||||
{
|
||||
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; private init; }
|
||||
public float Radius { get; private init; } = 0.25f;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
6
Pal.Client/Rendering/RenderData.cs
Normal file
6
Pal.Client/Rendering/RenderData.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Pal.Client.Rendering;
|
||||
|
||||
internal static class RenderData
|
||||
{
|
||||
public static readonly long TestLayerTimeout = 10_000;
|
||||
}
|
206
Pal.Client/Rendering/SimpleRenderer.cs
Normal file
206
Pal.Client/Rendering/SimpleRenderer.cs
Normal file
@ -0,0 +1,206 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Plugin.Services;
|
||||
using ImGuiNET;
|
||||
using Pal.Client.Configuration;
|
||||
using Pal.Client.Floors;
|
||||
|
||||
namespace Pal.Client.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Simple renderer that only draws basic stuff.
|
||||
///
|
||||
/// This is based on what SliceIsRight uses, and what PalacePal used before it was
|
||||
/// remade into PalacePal (which is the third or fourth iteration on the same idea
|
||||
/// I made, just with a clear vision).
|
||||
/// </summary>
|
||||
internal sealed class SimpleRenderer : IRenderer, IDisposable
|
||||
{
|
||||
private const int SegmentCount = 20;
|
||||
|
||||
private readonly IClientState _clientState;
|
||||
private readonly IGameGui _gameGui;
|
||||
private readonly IPalacePalConfiguration _configuration;
|
||||
private readonly TerritoryState _territoryState;
|
||||
private readonly ConcurrentDictionary<ELayer, SimpleLayer> _layers = new();
|
||||
|
||||
public SimpleRenderer(IClientState clientState, IGameGui gameGui, IPalacePalConfiguration configuration,
|
||||
TerritoryState territoryState)
|
||||
{
|
||||
_clientState = clientState;
|
||||
_gameGui = gameGui;
|
||||
_configuration = configuration;
|
||||
_territoryState = territoryState;
|
||||
}
|
||||
|
||||
public void SetLayer(ELayer layer, IReadOnlyList<IRenderElement> elements)
|
||||
{
|
||||
_layers[layer] = new SimpleLayer
|
||||
{
|
||||
TerritoryType = _clientState.TerritoryType,
|
||||
Elements = elements.Cast<SimpleElement>().ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public void ResetLayer(ELayer layer)
|
||||
{
|
||||
if (_layers.Remove(layer, out var l))
|
||||
l.Dispose();
|
||||
}
|
||||
|
||||
public IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, bool enabled, uint color,
|
||||
bool fill = false)
|
||||
{
|
||||
var config = MarkerConfig.ForType(type);
|
||||
return new SimpleElement
|
||||
{
|
||||
Type = type,
|
||||
Position = pos + new Vector3(0, config.OffsetY, 0),
|
||||
Enabled = enabled,
|
||||
Color = color,
|
||||
Radius = config.Radius,
|
||||
Fill = fill,
|
||||
};
|
||||
}
|
||||
|
||||
public void DrawDebugItems(uint trapColor, uint hoardColor)
|
||||
{
|
||||
_layers[ELayer.Test] = new SimpleLayer
|
||||
{
|
||||
TerritoryType = _clientState.TerritoryType,
|
||||
Elements = new List<SimpleElement>
|
||||
{
|
||||
(SimpleElement)CreateElement(
|
||||
MemoryLocation.EType.Trap,
|
||||
_clientState.LocalPlayer?.Position ?? default,
|
||||
true,
|
||||
trapColor),
|
||||
(SimpleElement)CreateElement(
|
||||
MemoryLocation.EType.Hoard,
|
||||
_clientState.LocalPlayer?.Position ?? default,
|
||||
true,
|
||||
hoardColor)
|
||||
},
|
||||
ExpiresAt = Environment.TickCount64 + RenderData.TestLayerTimeout
|
||||
};
|
||||
}
|
||||
|
||||
public void DrawLayers()
|
||||
{
|
||||
if (_layers.Count == 0)
|
||||
return;
|
||||
|
||||
ImGuiHelpers.ForceNextWindowMainViewport();
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero);
|
||||
ImGuiHelpers.SetNextWindowPosRelativeMainViewport(Vector2.Zero, ImGuiCond.None, Vector2.Zero);
|
||||
ImGui.SetNextWindowSize(ImGuiHelpers.MainViewport.Size);
|
||||
if (ImGui.Begin("###PalacePalSimpleRender",
|
||||
ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoBackground |
|
||||
ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoSavedSettings |
|
||||
ImGuiWindowFlags.AlwaysUseWindowPadding))
|
||||
{
|
||||
foreach (var layer in _layers.Values.Where(l => l.IsValid(_clientState)))
|
||||
{
|
||||
foreach (var e in layer.Elements)
|
||||
Draw(e);
|
||||
}
|
||||
|
||||
foreach (var key in _layers.Where(l => !l.Value.IsValid(_clientState))
|
||||
.Select(l => l.Key)
|
||||
.ToList())
|
||||
ResetLayer(key);
|
||||
|
||||
ImGui.End();
|
||||
}
|
||||
|
||||
ImGui.PopStyleVar();
|
||||
}
|
||||
|
||||
private void Draw(SimpleElement e)
|
||||
{
|
||||
if (!e.Enabled)
|
||||
return;
|
||||
|
||||
switch (e.Type)
|
||||
{
|
||||
case MemoryLocation.EType.Hoard:
|
||||
// ignore distance if this is a found hoard coffer
|
||||
if (_territoryState.PomanderOfIntuition == PomanderState.Active &&
|
||||
_configuration.DeepDungeons.HoardCoffers.OnlyVisibleAfterPomander)
|
||||
break;
|
||||
|
||||
goto case MemoryLocation.EType.Trap;
|
||||
|
||||
case MemoryLocation.EType.Trap:
|
||||
var playerPos = _clientState.LocalPlayer?.Position;
|
||||
if (playerPos == null)
|
||||
return;
|
||||
|
||||
if ((playerPos.Value - e.Position).Length() > 65)
|
||||
return;
|
||||
break;
|
||||
}
|
||||
|
||||
bool onScreen = false;
|
||||
for (int index = 0; index < 2 * SegmentCount; ++index)
|
||||
{
|
||||
onScreen |= _gameGui.WorldToScreen(new Vector3(
|
||||
e.Position.X + e.Radius * (float)Math.Sin(Math.PI / SegmentCount * index),
|
||||
e.Position.Y,
|
||||
e.Position.Z + e.Radius * (float)Math.Cos(Math.PI / SegmentCount * index)),
|
||||
out Vector2 vector2);
|
||||
|
||||
ImGui.GetWindowDrawList().PathLineTo(vector2);
|
||||
}
|
||||
|
||||
if (onScreen)
|
||||
{
|
||||
if (e.Fill)
|
||||
ImGui.GetWindowDrawList().PathFillConvex(e.Color);
|
||||
else
|
||||
ImGui.GetWindowDrawList().PathStroke(e.Color, ImDrawFlags.Closed, 2);
|
||||
}
|
||||
else
|
||||
ImGui.GetWindowDrawList().PathClear();
|
||||
}
|
||||
|
||||
public ERenderer GetConfigValue()
|
||||
=> ERenderer.Simple;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var l in _layers.Values)
|
||||
l.Dispose();
|
||||
}
|
||||
|
||||
public sealed class SimpleLayer : IDisposable
|
||||
{
|
||||
public required ushort TerritoryType { get; init; }
|
||||
public required IReadOnlyList<SimpleElement> Elements { get; init; }
|
||||
public long ExpiresAt { get; init; } = long.MaxValue;
|
||||
|
||||
public bool IsValid(IClientState clientState) =>
|
||||
TerritoryType == clientState.TerritoryType && ExpiresAt >= Environment.TickCount64;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var e in Elements)
|
||||
e.IsValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SimpleElement : IRenderElement
|
||||
{
|
||||
public bool IsValid { get; set; } = true;
|
||||
public required MemoryLocation.EType Type { get; init; }
|
||||
public required Vector3 Position { get; init; }
|
||||
public required bool Enabled { get; set; }
|
||||
public required uint Color { get; set; }
|
||||
public required float Radius { get; init; }
|
||||
public required bool Fill { get; init; }
|
||||
}
|
||||
}
|
196
Pal.Client/Rendering/SplatoonRenderer.cs
Normal file
196
Pal.Client/Rendering/SplatoonRenderer.cs
Normal file
@ -0,0 +1,196 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using ECommons;
|
||||
using ECommons.Reflection;
|
||||
using ECommons.Schedulers;
|
||||
using ECommons.SplatoonAPI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Pal.Client.Configuration;
|
||||
using Pal.Client.DependencyInjection;
|
||||
using Pal.Client.Floors;
|
||||
|
||||
namespace Pal.Client.Rendering;
|
||||
|
||||
internal sealed class SplatoonRenderer : IRenderer, IDisposable
|
||||
{
|
||||
private const long OnTerritoryChange = -2;
|
||||
|
||||
private readonly ILogger<SplatoonRenderer> _logger;
|
||||
private readonly DebugState _debugState;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly Chat _chat;
|
||||
|
||||
public SplatoonRenderer(
|
||||
ILogger<SplatoonRenderer> logger,
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
IDalamudPlugin dalamudPlugin,
|
||||
DebugState debugState,
|
||||
IClientState clientState,
|
||||
Chat chat)
|
||||
{
|
||||
_logger = logger;
|
||||
_debugState = debugState;
|
||||
_clientState = clientState;
|
||||
_chat = chat;
|
||||
|
||||
_logger.LogInformation("Initializing splatoon");
|
||||
ECommonsMain.Init(pluginInterface, dalamudPlugin, ECommons.Module.SplatoonAPI);
|
||||
}
|
||||
|
||||
private bool IsDisposed { get; set; }
|
||||
|
||||
public void SetLayer(ELayer layer, IReadOnlyList<IRenderElement> elements)
|
||||
{
|
||||
// we need to delay this, as the current framework update could be before splatoon's, in which case it would immediately delete the layout
|
||||
_ = new TickScheduler(delegate
|
||||
{
|
||||
try
|
||||
{
|
||||
Splatoon.AddDynamicElements(ToLayerName(layer),
|
||||
elements.Cast<SplatoonElement>().Select(x => x.Delegate).ToArray(),
|
||||
new[] { Environment.TickCount64 + 60 * 60 * 1000, OnTerritoryChange });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Could not create splatoon layer {Layer} with {Count} elements", layer,
|
||||
elements.Count);
|
||||
_debugState.SetFromException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void ResetLayer(ELayer layer)
|
||||
{
|
||||
try
|
||||
{
|
||||
Splatoon.RemoveDynamicElements(ToLayerName(layer));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Could not reset splatoon layer {Layer}", layer);
|
||||
}
|
||||
}
|
||||
|
||||
private string ToLayerName(ELayer layer)
|
||||
=> $"PalacePal.{layer}";
|
||||
|
||||
public IRenderElement CreateElement(MemoryLocation.EType type, Vector3 pos, bool enabled, uint color, bool fill = false)
|
||||
{
|
||||
MarkerConfig config = MarkerConfig.ForType(type);
|
||||
Element element = new Element(ElementType.CircleAtFixedCoordinates)
|
||||
{
|
||||
refX = pos.X,
|
||||
refY = pos.Z, // z and y are swapped
|
||||
refZ = pos.Y,
|
||||
offX = 0,
|
||||
offY = 0,
|
||||
offZ = config.OffsetY,
|
||||
Filled = fill,
|
||||
radius = config.Radius,
|
||||
FillStep = 1,
|
||||
color = color,
|
||||
thicc = 2,
|
||||
Enabled = enabled,
|
||||
};
|
||||
return new SplatoonElement(this, element);
|
||||
}
|
||||
|
||||
public void DrawDebugItems(uint trapColor, uint hoardColor)
|
||||
{
|
||||
try
|
||||
{
|
||||
Vector3? pos = _clientState.LocalPlayer?.Position;
|
||||
if (pos != null)
|
||||
{
|
||||
ResetLayer(ELayer.Test);
|
||||
|
||||
var elements = new List<IRenderElement>
|
||||
{
|
||||
CreateElement(MemoryLocation.EType.Trap, pos.Value, true, trapColor),
|
||||
CreateElement(MemoryLocation.EType.Hoard, pos.Value, true, hoardColor),
|
||||
};
|
||||
|
||||
if (!Splatoon.AddDynamicElements(ToLayerName(ELayer.Test),
|
||||
elements.Cast<SplatoonElement>().Select(x => x.Delegate).ToArray(),
|
||||
new[] { Environment.TickCount64 + RenderData.TestLayerTimeout }))
|
||||
{
|
||||
_chat.Message("Could not draw markers :(");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pluginManager = DalamudReflector.GetPluginManager();
|
||||
IList installedPlugins =
|
||||
pluginManager.GetType().GetProperty("InstalledPlugins")?.GetValue(pluginManager) as IList ??
|
||||
new List<object>();
|
||||
|
||||
foreach (var t in installedPlugins)
|
||||
{
|
||||
AssemblyName? assemblyName =
|
||||
(AssemblyName?)t.GetType().GetProperty("AssemblyName")?.GetValue(t);
|
||||
string? pluginName = (string?)t.GetType().GetProperty("Name")?.GetValue(t);
|
||||
if (assemblyName?.Name == "Splatoon" && pluginName != "Splatoon")
|
||||
{
|
||||
_chat.Error(
|
||||
$"Splatoon is installed under the plugin name '{pluginName}', which is incompatible with the Splatoon API.");
|
||||
_chat.Message(
|
||||
"You need to install Splatoon from the official repository at https://github.com/NightmareXIV/MyDalamudPlugins.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// not relevant
|
||||
}
|
||||
|
||||
_chat.Error("Could not draw markers, is Splatoon installed and enabled?");
|
||||
}
|
||||
}
|
||||
|
||||
public ERenderer GetConfigValue()
|
||||
=> ERenderer.Splatoon;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_logger.LogInformation("Disposing splatoon");
|
||||
|
||||
IsDisposed = true;
|
||||
|
||||
ResetLayer(ELayer.TrapHoard);
|
||||
ResetLayer(ELayer.RegularCoffers);
|
||||
ResetLayer(ELayer.Test);
|
||||
|
||||
ECommonsMain.Dispose();
|
||||
}
|
||||
|
||||
private sealed class SplatoonElement : IRenderElement
|
||||
{
|
||||
private readonly SplatoonRenderer _renderer;
|
||||
|
||||
public SplatoonElement(SplatoonRenderer renderer, Element element)
|
||||
{
|
||||
_renderer = renderer;
|
||||
Delegate = element;
|
||||
}
|
||||
|
||||
public Element Delegate { get; }
|
||||
|
||||
public bool IsValid => !_renderer.IsDisposed && Delegate.IsValid();
|
||||
|
||||
public bool Enabled
|
||||
{
|
||||
get => Delegate.Enabled;
|
||||
set => Delegate.Enabled = value;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Reflection.Metadata;
|
||||
using Dalamud.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Pal.Client.Scheduled;
|
||||
|
||||
namespace Pal.Client.Scheduled
|
||||
{
|
||||
internal interface IQueueOnFrameworkThread
|
||||
{
|
||||
void Run(Plugin plugin, ref bool recreateLayout, ref bool saveMarkers);
|
||||
internal interface IHandler
|
||||
{
|
||||
void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout);
|
||||
}
|
||||
|
||||
internal abstract class Handler<T> : IHandler
|
||||
where T : IQueueOnFrameworkThread
|
||||
{
|
||||
protected readonly ILogger<Handler<T>> _logger;
|
||||
|
||||
protected Handler(ILogger<Handler<T>> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected abstract void Run(T queued, ref bool recreateLayout);
|
||||
|
||||
public void RunIfCompatible(IQueueOnFrameworkThread queued, ref bool recreateLayout)
|
||||
{
|
||||
if (queued is T t)
|
||||
{
|
||||
_logger.LogDebug("Handling {QueuedType}", queued.GetType());
|
||||
Run(t, ref recreateLayout);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Could not use queue handler {QueuedType}", queued.GetType());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user