Compare commits
173 Commits
clr-fix-at
...
animatedhe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
812fbaec78 | ||
|
|
e11135bb23 | ||
| ad0254a812 | |||
|
|
1c4c73327f | ||
|
|
7b74fa7c4e | ||
|
|
2a670b3e64 | ||
|
|
f225989a00 | ||
| f2b17120fa | |||
| 79539e3db8 | |||
| bcf6aea89d | |||
| 779ff06981 | |||
|
|
54530cb16d | ||
| 03105e0755 | |||
| b99f68a891 | |||
| 7c7a98f770 | |||
| ab369d008e | |||
|
|
e5fa477eee | ||
|
|
ac8270e4ad | ||
|
|
4d0bf2d57e | ||
| 7f74f88302 | |||
|
|
934cdfbcf0 | ||
|
|
d2a68e6533 | ||
| 20008f904d | |||
|
|
54b50886c0 | ||
|
|
234fe5d360 | ||
|
|
05770d9a5b | ||
|
|
68dc8aef2f | ||
|
|
56143c5f3d | ||
| 91739536bf | |||
|
|
ae9df103f3 | ||
|
|
bade5ab6f5 | ||
| 5e22f3bff0 | |||
|
|
ed099f322d | ||
|
|
116e65b220 | ||
|
|
ee175efe41 | ||
|
|
6ca491ac30 | ||
| 4ffc2247b2 | |||
| 7b4e42c487 | |||
|
|
5e2afc8bfe | ||
| 6d57813ef2 | |||
|
|
8b75063b9d | ||
|
|
99b49762bb | ||
|
|
35e35591f5 | ||
|
|
e3c04e31e7 | ||
|
|
f7fb609c71 | ||
|
|
d766c2c42e | ||
| 1d212437f5 | |||
|
|
9d1d6783ce | ||
| f47df8fac2 | |||
| ecc1e7107f | |||
| 1cc8339307 | |||
|
|
6522b586d5 | ||
|
|
8b9e35283d | ||
|
|
755bae1294 | ||
|
|
a41f419076 | ||
|
|
dec6c4900b | ||
|
|
5dabd23d93 | ||
|
|
0dd520d926 | ||
|
|
4e4d19ad00 | ||
|
|
d5c11cd22f | ||
| 4444a88746 | |||
|
|
bdfcf254a8 | ||
|
|
eb11ff0b4c | ||
|
|
ee1fcb5661 | ||
|
|
44e91bef8f | ||
|
|
6891424b0d | ||
|
|
6395b1eb52 | ||
| 0671c46e5d | |||
|
|
09b78e1896 | ||
|
|
1b2db4c698 | ||
| 6cf0e3daed | |||
|
|
2e14fc2f8f | ||
|
|
675918624d | ||
|
|
25f0d41581 | ||
|
|
1cb326070b | ||
|
|
b444782b76 | ||
|
|
feec5e8ff3 | ||
|
|
cc1f381687 | ||
|
|
69b504c42f | ||
| 541d17132d | |||
| 1c36db97dc | |||
|
|
c335489cee | ||
|
|
6734021b89 | ||
|
|
46a8fc72cb | ||
|
|
962567fbfe | ||
|
|
0e076f6290 | ||
|
|
72cd5006db | ||
| 023ca2013e | |||
| a77261a096 | |||
| febc47442a | |||
|
|
481bc99dcd | ||
|
|
9d6a0a1257 | ||
| 8076d63ce2 | |||
| ba5c8b588e | |||
| 91393bf4a1 | |||
|
|
e0e2304253 | ||
|
|
a9181d2592 | ||
|
|
cab13874d8 | ||
|
|
04cd09cbb9 | ||
|
|
0b36c1bdc2 | ||
|
|
1e88fe0cf3 | ||
| 740b58afc4 | |||
|
|
9e12725f89 | ||
|
|
aa04ab05ab | ||
| 28967d6e17 | |||
| d995afcf48 | |||
| 5ab67c70d6 | |||
| 8cc83bce79 | |||
| 1cdc0a90f9 | |||
|
|
e350e8007a | ||
|
|
7a9ade95c3 | ||
|
|
01607c275a | ||
|
|
1e6109d1e6 | ||
|
|
961092ab87 | ||
|
|
36166f1399 | ||
| d057c638ab | |||
| 28d9110cb0 | |||
| ef592032b3 | |||
|
|
9c794137c1 | ||
|
|
4a256f7807 | ||
|
|
25756561b9 | ||
|
|
e8c546c128 | ||
| d4ba1cf437 | |||
| e0d1f98c70 | |||
|
|
1862689b1b | ||
| 325dc8947d | |||
|
|
95e7f2daa7 | ||
|
|
41a303dc91 | ||
|
|
25b03aea15 | ||
|
|
b6564156f0 | ||
|
|
f89ce900c7 | ||
| 299abc21ee | |||
|
|
c02a8ed2ee | ||
|
|
8692e877cf | ||
|
|
7de72471bb | ||
|
|
d7182e9d57 | ||
| 2b02de731a | |||
|
|
e9082ab8d0 | ||
| 2a06a11cbc | |||
|
|
557121a9b7 | ||
| b22140a8d4 | |||
| 4db468a480 | |||
| 8d8f8d20cd | |||
| 3722b79615 | |||
| cf97e7e800 | |||
|
|
1d672d2552 | ||
|
|
35636f27f6 | ||
|
|
1b686e45dc | ||
|
|
b6aa2bebb1 | ||
|
|
cfc9f60176 | ||
|
|
d4dca455ba | ||
| 76c2777f00 | |||
|
|
0af2a6134b | ||
|
|
6e3c60f627 | ||
|
|
5feb74c1c0 | ||
|
|
c1770528f3 | ||
|
|
bf139c128b | ||
|
|
b3cc41382f | ||
|
|
7c4d0fd5e9 | ||
|
|
c37e3badf1 | ||
| f4478f653a | |||
|
|
3f85852618 | ||
|
|
3e626c5e47 | ||
|
|
9a846a37d4 | ||
|
|
177534d78b | ||
|
|
de75b90703 | ||
|
|
c16891021c | ||
| d19d1c0a3a | |||
|
|
cabc4ec0fe | ||
|
|
8bccdc5ef1 | ||
|
|
ce5f8a43a2 | ||
|
|
437731749f | ||
|
|
55e78e088a |
@@ -9,8 +9,7 @@ env:
|
|||||||
DOTNET_VERSION: |
|
DOTNET_VERSION: |
|
||||||
10.x.x
|
10.x.x
|
||||||
9.x.x
|
9.x.x
|
||||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tag-and-release:
|
tag-and-release:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
@@ -33,14 +32,16 @@ jobs:
|
|||||||
|
|
||||||
- name: Download Dalamud
|
- name: Download Dalamud
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/.xlcore/dalamud/Hooks/dev
|
cd /
|
||||||
|
mkdir -p root/.xlcore/dalamud/Hooks/dev
|
||||||
curl -O https://goatcorp.github.io/dalamud-distrib/stg/latest.zip
|
curl -O https://goatcorp.github.io/dalamud-distrib/stg/latest.zip
|
||||||
unzip latest.zip -d ~/.xlcore/dalamud/Hooks/dev
|
unzip latest.zip -d /root/.xlcore/dalamud/Hooks/dev
|
||||||
|
|
||||||
- name: Lets Build Lightless!
|
- name: Lets Build Lightless!
|
||||||
run: |
|
run: |
|
||||||
dotnet publish --configuration Release
|
dotnet restore
|
||||||
mv LightlessSync/bin/x64/Release/LightlessSync/latest.zip LightlessClient.zip
|
dotnet build --configuration Release --no-restore
|
||||||
|
dotnet publish --configuration Release --no-build
|
||||||
|
|
||||||
- name: Get version
|
- name: Get version
|
||||||
id: package_version
|
id: package_version
|
||||||
@@ -52,6 +53,19 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "Version: ${{ steps.package_version.outputs.version }}"
|
echo "Version: ${{ steps.package_version.outputs.version }}"
|
||||||
|
|
||||||
|
- name: Prepare Lightless Client
|
||||||
|
run: |
|
||||||
|
PUBLISH_PATH="/workspace/Lightless-Sync/LightlessClient/LightlessSync/bin/x64/Release/publish/"
|
||||||
|
if [ -d "$PUBLISH_PATH" ]; then
|
||||||
|
rm -rf "$PUBLISH_PATH"
|
||||||
|
echo "Removed $PUBLISH_PATH"
|
||||||
|
else
|
||||||
|
echo "$PUBLISH_PATH does not exist, nothing to remove."
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p output
|
||||||
|
(cd /workspace/Lightless-Sync/LightlessClient/LightlessSync/bin/x64/Release/ && zip -r $OLDPWD/output/LightlessClient.zip *)
|
||||||
|
|
||||||
- name: Create Git tag if not exists (master)
|
- name: Create Git tag if not exists (master)
|
||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/master'
|
||||||
run: |
|
run: |
|
||||||
@@ -148,7 +162,14 @@ jobs:
|
|||||||
echo "release_id=$release_id"
|
echo "release_id=$release_id"
|
||||||
echo "release_id=$release_id" >> $GITHUB_OUTPUT || echo "::set-output name=release_id::$release_id"
|
echo "release_id=$release_id" >> $GITHUB_OUTPUT || echo "::set-output name=release_id::$release_id"
|
||||||
echo "RELEASE_ID=$release_id" >> $GITHUB_ENV
|
echo "RELEASE_ID=$release_id" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Check asset exists
|
||||||
|
run: |
|
||||||
|
if [ ! -f output/LightlessClient.zip ]; then
|
||||||
|
echo "output/LightlessClient.zip does not exist!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Upload Assets to release
|
- name: Upload Assets to release
|
||||||
env:
|
env:
|
||||||
RELEASE_ID: ${{ env.RELEASE_ID }}
|
RELEASE_ID: ${{ env.RELEASE_ID }}
|
||||||
@@ -156,7 +177,7 @@ jobs:
|
|||||||
echo "Uploading to release ID: $RELEASE_ID"
|
echo "Uploading to release ID: $RELEASE_ID"
|
||||||
curl --fail-with-body -s -X POST \
|
curl --fail-with-body -s -X POST \
|
||||||
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
||||||
-F "attachment=@LightlessClient.zip" \
|
-F "attachment=@output/LightlessClient.zip" \
|
||||||
"https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases/$RELEASE_ID/assets"
|
"https://git.lightless-sync.org/api/v1/repos/${GITHUB_REPOSITORY}/releases/$RELEASE_ID/assets"
|
||||||
|
|
||||||
- name: Clone plugin hosting repo
|
- name: Clone plugin hosting repo
|
||||||
@@ -165,7 +186,7 @@ jobs:
|
|||||||
cd LightlessSyncRepo
|
cd LightlessSyncRepo
|
||||||
git clone https://git.lightless-sync.org/${{ gitea.repository_owner }}/LightlessSync.git
|
git clone https://git.lightless-sync.org/${{ gitea.repository_owner }}/LightlessSync.git
|
||||||
env:
|
env:
|
||||||
GIT_TERMINAL_PROMPT: 0
|
GIT_TERMINAL_PROMPT: 0
|
||||||
|
|
||||||
- name: Update plogonmaster.json with version (master)
|
- name: Update plogonmaster.json with version (master)
|
||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/master'
|
||||||
@@ -261,8 +282,8 @@ jobs:
|
|||||||
- name: Commit and push to LightlessSync
|
- name: Commit and push to LightlessSync
|
||||||
run: |
|
run: |
|
||||||
cd LightlessSyncRepo/LightlessSync
|
cd LightlessSyncRepo/LightlessSync
|
||||||
git config user.name "Gitea-Automation"
|
git config user.name "github-actions"
|
||||||
git config user.email "aaa@aaaaaaa.aaa"
|
git config user.email "github-actions@github.com"
|
||||||
git add .
|
git add .
|
||||||
git diff-index --quiet HEAD || git commit -m "Update ${{ env.PLUGIN_NAME }} to ${{ steps.package_version.outputs.version }}"
|
git diff-index --quiet HEAD || git commit -m "Update ${{ env.PLUGIN_NAME }} to ${{ steps.package_version.outputs.version }}"
|
||||||
git push https://x-access-token:${{ secrets.AUTOMATION_TOKEN }}@git.lightless-sync.org/${{ gitea.repository_owner }}/LightlessSync.git HEAD:main
|
git push https://x-access-token:${{ secrets.AUTOMATION_TOKEN }}@git.lightless-sync.org/${{ gitea.repository_owner }}/LightlessSync.git HEAD:main
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -348,6 +348,3 @@ MigrationBackup/
|
|||||||
|
|
||||||
# Ionide (cross platform F# VS Code tools) working folder
|
# Ionide (cross platform F# VS Code tools) working folder
|
||||||
.ionide/
|
.ionide/
|
||||||
|
|
||||||
# idea
|
|
||||||
/.idea
|
|
||||||
|
|||||||
Submodule LightlessAPI updated: 4ecd5375e6...8e4432af45
@@ -1,18 +0,0 @@
|
|||||||
namespace LightlessSync.FileCache;
|
|
||||||
|
|
||||||
public interface ICompactorContext
|
|
||||||
{
|
|
||||||
bool UseCompactor { get; }
|
|
||||||
string CacheFolder { get; }
|
|
||||||
bool IsWine { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface ICompactionExecutor
|
|
||||||
{
|
|
||||||
bool TryCompact(string filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class NoopCompactionExecutor : ICompactionExecutor
|
|
||||||
{
|
|
||||||
public bool TryCompact(string filePath) => false;
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<LangVersion>latest</LangVersion>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>WinExe</OutputType>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<LangVersion>latest</LangVersion>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\LightlessCompactor\LightlessCompactor.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.1" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,270 +0,0 @@
|
|||||||
using LightlessSync.FileCache;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO.Pipes;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
internal sealed class WorkerCompactorContext : ICompactorContext
|
|
||||||
{
|
|
||||||
public WorkerCompactorContext(string cacheFolder, bool isWine)
|
|
||||||
{
|
|
||||||
CacheFolder = cacheFolder;
|
|
||||||
IsWine = isWine;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool UseCompactor => true;
|
|
||||||
public string CacheFolder { get; }
|
|
||||||
public bool IsWine { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class WorkerOptions
|
|
||||||
{
|
|
||||||
public string? FilePath { get; init; }
|
|
||||||
public bool IsWine { get; init; }
|
|
||||||
public string CacheFolder { get; init; } = string.Empty;
|
|
||||||
public LogLevel LogLevel { get; init; } = LogLevel.Information;
|
|
||||||
public string PipeName { get; init; } = "LightlessCompactor";
|
|
||||||
public int? ParentProcessId { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static class Program
|
|
||||||
{
|
|
||||||
public static async Task<int> Main(string[] args)
|
|
||||||
{
|
|
||||||
var options = ParseOptions(args, out var error);
|
|
||||||
if (options is null)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(error ?? "Invalid arguments.");
|
|
||||||
Console.Error.WriteLine("Usage: LightlessCompactorWorker --file <path> [--wine] [--cache-folder <path>] [--verbose]");
|
|
||||||
Console.Error.WriteLine(" or: LightlessCompactorWorker --pipe <name> [--wine] [--parent <pid>] [--verbose]");
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
TrySetLowPriority();
|
|
||||||
|
|
||||||
using var loggerFactory = LoggerFactory.Create(builder =>
|
|
||||||
{
|
|
||||||
builder.SetMinimumLevel(options.LogLevel);
|
|
||||||
builder.AddSimpleConsole(o =>
|
|
||||||
{
|
|
||||||
o.SingleLine = true;
|
|
||||||
o.TimestampFormat = "HH:mm:ss.fff ";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
var logger = loggerFactory.CreateLogger<FileCompactor>();
|
|
||||||
var context = new WorkerCompactorContext(options.CacheFolder, options.IsWine);
|
|
||||||
|
|
||||||
using var compactor = new FileCompactor(logger, context, new NoopCompactionExecutor());
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(options.FilePath))
|
|
||||||
{
|
|
||||||
var success = compactor.TryCompactFile(options.FilePath!);
|
|
||||||
return success ? 0 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
var serverLogger = loggerFactory.CreateLogger("CompactorWorker");
|
|
||||||
return await RunServerAsync(compactor, options, serverLogger).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<int> RunServerAsync(FileCompactor compactor, WorkerOptions options, ILogger serverLogger)
|
|
||||||
{
|
|
||||||
using var cts = new CancellationTokenSource();
|
|
||||||
var token = cts.Token;
|
|
||||||
|
|
||||||
if (options.ParentProcessId.HasValue)
|
|
||||||
{
|
|
||||||
_ = Task.Run(() => MonitorParent(options.ParentProcessId.Value, cts));
|
|
||||||
}
|
|
||||||
|
|
||||||
serverLogger.LogInformation("Compactor worker listening on pipe {pipe}", options.PipeName);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
while (!token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
var server = new NamedPipeServerStream(
|
|
||||||
options.PipeName,
|
|
||||||
PipeDirection.InOut,
|
|
||||||
NamedPipeServerStream.MaxAllowedServerInstances,
|
|
||||||
PipeTransmissionMode.Byte,
|
|
||||||
PipeOptions.Asynchronous);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await server.WaitForConnectionAsync(token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
server.Dispose();
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = Task.Run(() => HandleClientAsync(server, compactor, cts));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
// shutdown requested
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
serverLogger.LogWarning(ex, "Compactor worker terminated unexpectedly.");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task HandleClientAsync(NamedPipeServerStream pipe, FileCompactor compactor, CancellationTokenSource shutdownCts)
|
|
||||||
{
|
|
||||||
await using var _ = pipe;
|
|
||||||
using var reader = new StreamReader(pipe);
|
|
||||||
using var writer = new StreamWriter(pipe) { AutoFlush = true };
|
|
||||||
|
|
||||||
var line = await reader.ReadLineAsync().ConfigureAwait(false);
|
|
||||||
if (string.IsNullOrWhiteSpace(line))
|
|
||||||
return;
|
|
||||||
|
|
||||||
CompactorRequest? request = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
request = JsonSerializer.Deserialize<CompactorRequest>(line);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
CompactorResponse response;
|
|
||||||
if (request is null)
|
|
||||||
{
|
|
||||||
response = new CompactorResponse { Success = false, Error = "Invalid request." };
|
|
||||||
}
|
|
||||||
else if (string.Equals(request.Type, "shutdown", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
shutdownCts.Cancel();
|
|
||||||
response = new CompactorResponse { Success = true };
|
|
||||||
}
|
|
||||||
else if (string.Equals(request.Type, "compact", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
var success = compactor.TryCompactFile(request.Path ?? string.Empty);
|
|
||||||
response = new CompactorResponse { Success = success };
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
response = new CompactorResponse { Success = false, Error = "Unknown request type." };
|
|
||||||
}
|
|
||||||
|
|
||||||
await writer.WriteLineAsync(JsonSerializer.Serialize(response)).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void MonitorParent(int parentPid, CancellationTokenSource shutdownCts)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var parent = Process.GetProcessById(parentPid);
|
|
||||||
parent.WaitForExit();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// parent missing
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
shutdownCts.Cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static WorkerOptions? ParseOptions(string[] args, out string? error)
|
|
||||||
{
|
|
||||||
string? filePath = null;
|
|
||||||
bool isWine = false;
|
|
||||||
string cacheFolder = string.Empty;
|
|
||||||
var logLevel = LogLevel.Information;
|
|
||||||
string pipeName = "LightlessCompactor";
|
|
||||||
int? parentPid = null;
|
|
||||||
|
|
||||||
for (int i = 0; i < args.Length; i++)
|
|
||||||
{
|
|
||||||
var arg = args[i];
|
|
||||||
switch (arg)
|
|
||||||
{
|
|
||||||
case "--file":
|
|
||||||
if (i + 1 >= args.Length)
|
|
||||||
{
|
|
||||||
error = "Missing value for --file.";
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
filePath = args[++i];
|
|
||||||
break;
|
|
||||||
case "--cache-folder":
|
|
||||||
if (i + 1 >= args.Length)
|
|
||||||
{
|
|
||||||
error = "Missing value for --cache-folder.";
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
cacheFolder = args[++i];
|
|
||||||
break;
|
|
||||||
case "--pipe":
|
|
||||||
if (i + 1 >= args.Length)
|
|
||||||
{
|
|
||||||
error = "Missing value for --pipe.";
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
pipeName = args[++i];
|
|
||||||
break;
|
|
||||||
case "--parent":
|
|
||||||
if (i + 1 >= args.Length || !int.TryParse(args[++i], out var pid))
|
|
||||||
{
|
|
||||||
error = "Invalid value for --parent.";
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
parentPid = pid;
|
|
||||||
break;
|
|
||||||
case "--wine":
|
|
||||||
isWine = true;
|
|
||||||
break;
|
|
||||||
case "--verbose":
|
|
||||||
logLevel = LogLevel.Trace;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
error = null;
|
|
||||||
return new WorkerOptions
|
|
||||||
{
|
|
||||||
FilePath = filePath,
|
|
||||||
IsWine = isWine,
|
|
||||||
CacheFolder = cacheFolder,
|
|
||||||
LogLevel = logLevel,
|
|
||||||
PipeName = pipeName,
|
|
||||||
ParentProcessId = parentPid
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void TrySetLowPriority()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (OperatingSystem.IsWindows())
|
|
||||||
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.BelowNormal;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class CompactorRequest
|
|
||||||
{
|
|
||||||
public string Type { get; init; } = "compact";
|
|
||||||
public string? Path { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class CompactorResponse
|
|
||||||
{
|
|
||||||
public bool Success { get; init; }
|
|
||||||
public string? Error { get; init; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -22,10 +22,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OtterGui", "OtterGui\OtterG
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pictomancy", "ffxiv_pictomancy\Pictomancy\Pictomancy.csproj", "{825F17D8-2704-24F6-DF8B-2542AC92C765}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pictomancy", "ffxiv_pictomancy\Pictomancy\Pictomancy.csproj", "{825F17D8-2704-24F6-DF8B-2542AC92C765}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightlessCompactor", "LightlessCompactor\LightlessCompactor.csproj", "{01F31917-9F1E-426D-BDAE-17268CBF9523}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightlessCompactorWorker", "LightlessCompactorWorker\LightlessCompactorWorker.csproj", "{72BE3664-CD0E-4DA4-B040-91338A2798E0}"
|
|
||||||
EndProject
|
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -120,30 +116,6 @@ Global
|
|||||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x64.Build.0 = Release|x64
|
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x64.Build.0 = Release|x64
|
||||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x86.ActiveCfg = Release|x64
|
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x86.ActiveCfg = Release|x64
|
||||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x86.Build.0 = Release|x64
|
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x86.Build.0 = Release|x64
|
||||||
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -1,72 +1,11 @@
|
|||||||
tagline: "Lightless Sync v2.0.1"
|
tagline: "Lightless Sync v2.0.0"
|
||||||
subline: "LIGHTLESS IS EVOLVING!!"
|
subline: "LIGHTLESS IS EVOLVING!!"
|
||||||
changelog:
|
changelog:
|
||||||
- name: "v2.0.2"
|
|
||||||
tagline: "Last update of 2025!... ... ... If Nothing breaks"
|
|
||||||
date: "December 28 2025"
|
|
||||||
# be sure to set this every new version
|
|
||||||
isCurrent: true
|
|
||||||
versions:
|
|
||||||
- number: "Chat"
|
|
||||||
icon: ""
|
|
||||||
items:
|
|
||||||
- "Added a 7TV emote picker to chat. You’ll now see a new button next to Send that opens an emote selector."
|
|
||||||
- "Pin User, Remove User, and Ban User (including Syncshell) have been added when you right click a user in chat."
|
|
||||||
- "Chatters now show status icons/labels in the Syncshell (e.g., Owner, Moderator, and Pinned when applicable)."
|
|
||||||
- "The Rules page no longer blocks input for other open Lightless UI windows."
|
|
||||||
- number: "LightFinder"
|
|
||||||
icon: ""
|
|
||||||
items:
|
|
||||||
- "If the ImGui Lightfinder icons aren’t working correctly, you can switch back to the Nameplate signature hook. Important warning - USE AT YOUR OWN RISK: The native nameplate hook can crash the game if multiple plugins hook the nameplate function at the same time. We will not provide support about Nameplate crashes, nor will the Dalamud team, **DO NOT BOTHER THEM.**"
|
|
||||||
- "The LightFinder label in the menu has a counter next to it showing the number of broadcasting users."
|
|
||||||
- "There is less interference of hidden UI elements for the imGui renderer of LightFinder."
|
|
||||||
- number: "Miscellaneous fixes"
|
|
||||||
icon: ""
|
|
||||||
items:
|
|
||||||
- "Overhauled transient resources in an attempt to mitigate mount and minion problems."
|
|
||||||
- "Some file cache entries will now be cached to reduce load on your game."
|
|
||||||
- "Downloading and decompressing have been redone to fix the locking issues."
|
|
||||||
- "Disabling the context menu will now hide the context menu on right clicks again. (Thanks @infiniti)"
|
|
||||||
- "Temporary collections that were not cleared before will now be cleared when the plugin starts."
|
|
||||||
- "Pair requests will now appear in chat if notifications are not enabled or on chat mode."
|
|
||||||
- "Fixed an instance were an object may be null in the Download UI."
|
|
||||||
- "API 14 - Migrate to IPlayerState service"
|
|
||||||
- name: "v2.0.1"
|
|
||||||
tagline: "Some Fixes"
|
|
||||||
date: "December 23 2025"
|
|
||||||
versions:
|
|
||||||
- number: "Chat"
|
|
||||||
icon: ""
|
|
||||||
items:
|
|
||||||
- "You can turn off the syncshell chat as Owner by going to the Syncshell Admin panel -> Owner -> Enable/Disable Chat."
|
|
||||||
- "Fixed an issue where you can't chat due to regions being in a different language."
|
|
||||||
- number: "LightFinder"
|
|
||||||
icon: ""
|
|
||||||
items:
|
|
||||||
- "The icon/Lightfinder Text will be hidden when Game UI is hidden and behind game elements/UI"
|
|
||||||
- "Able to select an icon for the selected list or a custom glyph if you know the code."
|
|
||||||
- "Smoothing and reducing jitter on the icon/Lightfinder Text."
|
|
||||||
- "Fixed so higher scaled UI options (100/150/200% UI scale) wouldn't break the element."
|
|
||||||
- "Detects if GPose is active, wouldn't render the elements"
|
|
||||||
- number: "Miscellaneous fixes"
|
|
||||||
icon: ""
|
|
||||||
items:
|
|
||||||
- "Fixed the null error given on GetCID when transferring between zones/housing."
|
|
||||||
- "Added push/pop on certain ImGUI elements to remove them after being used. "
|
|
||||||
- "Having all tabs open in the Main UI wouldn't lag out the game anymore."
|
|
||||||
- "Cycle pause has been adjusted to the old function. There is a separate button to pause normally, now called 'Toggle (Un)Pause State'."
|
|
||||||
- "Changes have been made to the character redraw to address the issues with the building character data constantly being redrawn and the redrawn behavior with Honorific titles."
|
|
||||||
- "GPose characters should appear again in the actor screen"
|
|
||||||
- "Lightspeed download console messages are no longer shown as warnings."
|
|
||||||
- number: "Server Updates"
|
|
||||||
icon: ""
|
|
||||||
items:
|
|
||||||
- "Changes have been made to the disabling of your profile. It should save again."
|
|
||||||
- "Ability added to toggle chats from syncshell to be disabled."
|
|
||||||
- "Files are continuously being deleted due to high volumes in storage, potentially causing MCDOs to have missing files. We have increased the limit of the storage in our configurations to see if that helps."
|
|
||||||
- name: "v2.0.0"
|
- name: "v2.0.0"
|
||||||
tagline: "Thank you for 4 months!"
|
tagline: "Thank you for 4 months!"
|
||||||
date: "December 2025"
|
date: "December 2025"
|
||||||
|
# be sure to set this every new version
|
||||||
|
isCurrent: true
|
||||||
versions:
|
versions:
|
||||||
- number: "Lightless Chat"
|
- number: "Lightless Chat"
|
||||||
icon: ""
|
icon: ""
|
||||||
|
|||||||
@@ -103,7 +103,6 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
record WatcherChange(WatcherChangeTypes ChangeType, string? OldPath = null);
|
record WatcherChange(WatcherChangeTypes ChangeType, string? OldPath = null);
|
||||||
private readonly record struct CacheEvictionCandidate(string FullPath, long Size, DateTime LastAccessTime);
|
|
||||||
private readonly Dictionary<string, WatcherChange> _watcherChanges = new(StringComparer.OrdinalIgnoreCase);
|
private readonly Dictionary<string, WatcherChange> _watcherChanges = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private readonly Dictionary<string, WatcherChange> _lightlessChanges = new(StringComparer.OrdinalIgnoreCase);
|
private readonly Dictionary<string, WatcherChange> _lightlessChanges = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
@@ -442,40 +441,116 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
Logger.LogWarning(ex, "Could not determine drive size for storage folder {folder}", _configService.Current.CacheFolder);
|
Logger.LogWarning(ex, "Could not determine drive size for storage folder {folder}", _configService.Current.CacheFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
var cacheFolder = _configService.Current.CacheFolder;
|
var files = Directory.EnumerateFiles(_configService.Current.CacheFolder)
|
||||||
var candidates = new List<CacheEvictionCandidate>();
|
.Select(f => new FileInfo(f))
|
||||||
|
.OrderBy(f => f.LastAccessTime)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
long totalSize = 0;
|
long totalSize = 0;
|
||||||
totalSize += AddFolderCandidates(cacheFolder, candidates, token, isWine);
|
|
||||||
totalSize += AddFolderCandidates(Path.Combine(cacheFolder, "downscaled"), candidates, token, isWine);
|
foreach (var f in files)
|
||||||
totalSize += AddFolderCandidates(Path.Combine(cacheFolder, "decimated"), candidates, token, isWine);
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
long size = 0;
|
||||||
|
|
||||||
|
if (!isWine)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
size = _fileCompactor.GetFileSizeOnDisk(f);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogTrace(ex, "GetFileSizeOnDisk failed for {file}, using fallback length", f.FullName);
|
||||||
|
size = f.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
size = f.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalSize += size;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogTrace(ex, "Error getting size for {file}", f.FullName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
FileCacheSize = totalSize;
|
FileCacheSize = totalSize;
|
||||||
|
|
||||||
|
if (Directory.Exists(_configService.Current.CacheFolder + "/downscaled"))
|
||||||
|
{
|
||||||
|
var filesDownscaled = Directory.EnumerateFiles(_configService.Current.CacheFolder + "/downscaled").Select(f => new FileInfo(f)).OrderBy(f => f.LastAccessTime).ToList();
|
||||||
|
|
||||||
|
long totalSizeDownscaled = 0;
|
||||||
|
|
||||||
|
foreach (var f in filesDownscaled)
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
long size = 0;
|
||||||
|
|
||||||
|
if (!isWine)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
size = _fileCompactor.GetFileSizeOnDisk(f);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogTrace(ex, "GetFileSizeOnDisk failed for {file}, using fallback length", f.FullName);
|
||||||
|
size = f.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
size = f.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalSizeDownscaled += size;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogTrace(ex, "Error getting size for {file}", f.FullName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileCacheSize = (totalSize + totalSizeDownscaled);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FileCacheSize = totalSize;
|
||||||
|
}
|
||||||
|
|
||||||
var maxCacheInBytes = (long)(_configService.Current.MaxLocalCacheInGiB * 1024d * 1024d * 1024d);
|
var maxCacheInBytes = (long)(_configService.Current.MaxLocalCacheInGiB * 1024d * 1024d * 1024d);
|
||||||
if (FileCacheSize < maxCacheInBytes)
|
if (FileCacheSize < maxCacheInBytes)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var maxCacheBuffer = maxCacheInBytes * 0.05d;
|
var maxCacheBuffer = maxCacheInBytes * 0.05d;
|
||||||
|
|
||||||
candidates.Sort(static (a, b) => a.LastAccessTime.CompareTo(b.LastAccessTime));
|
while (FileCacheSize > maxCacheInBytes - (long)maxCacheBuffer && files.Count > 0)
|
||||||
|
|
||||||
var evictionTarget = maxCacheInBytes - (long)maxCacheBuffer;
|
|
||||||
var index = 0;
|
|
||||||
while (FileCacheSize > evictionTarget && index < candidates.Count)
|
|
||||||
{
|
{
|
||||||
var oldestFile = candidates[index];
|
var oldestFile = files[0];
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
EvictCacheCandidate(oldestFile, cacheFolder);
|
long fileSize = oldestFile.Length;
|
||||||
FileCacheSize -= oldestFile.Size;
|
File.Delete(oldestFile.FullName);
|
||||||
|
FileCacheSize -= fileSize;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogTrace(ex, "Failed to delete old file {file}", oldestFile.FullPath);
|
Logger.LogTrace(ex, "Failed to delete old file {file}", oldestFile.FullName);
|
||||||
}
|
}
|
||||||
|
|
||||||
index++;
|
files.RemoveAt(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,114 +559,6 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
HaltScanLocks.Clear();
|
HaltScanLocks.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private long AddFolderCandidates(string directory, List<CacheEvictionCandidate> candidates, CancellationToken token, bool isWine)
|
|
||||||
{
|
|
||||||
if (!Directory.Exists(directory))
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
long totalSize = 0;
|
|
||||||
foreach (var path in Directory.EnumerateFiles(directory))
|
|
||||||
{
|
|
||||||
token.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var file = new FileInfo(path);
|
|
||||||
var size = GetFileSizeOnDisk(file, isWine);
|
|
||||||
totalSize += size;
|
|
||||||
candidates.Add(new CacheEvictionCandidate(file.FullName, size, file.LastAccessTime));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogTrace(ex, "Error getting size for {file}", path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
private long GetFileSizeOnDisk(FileInfo file, bool isWine)
|
|
||||||
{
|
|
||||||
if (isWine)
|
|
||||||
{
|
|
||||||
return file.Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return _fileCompactor.GetFileSizeOnDisk(file);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogTrace(ex, "GetFileSizeOnDisk failed for {file}, using fallback length", file.FullName);
|
|
||||||
return file.Length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EvictCacheCandidate(CacheEvictionCandidate candidate, string cacheFolder)
|
|
||||||
{
|
|
||||||
if (TryGetCacheHashAndPrefixedPath(candidate.FullPath, cacheFolder, out var hash, out var prefixedPath))
|
|
||||||
{
|
|
||||||
_fileDbManager.RemoveHashedFile(hash, prefixedPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (File.Exists(candidate.FullPath))
|
|
||||||
{
|
|
||||||
File.Delete(candidate.FullPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogTrace(ex, "Failed to delete old file {file}", candidate.FullPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryGetCacheHashAndPrefixedPath(string filePath, string cacheFolder, out string hash, out string prefixedPath)
|
|
||||||
{
|
|
||||||
hash = string.Empty;
|
|
||||||
prefixedPath = string.Empty;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(cacheFolder))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fileName = Path.GetFileNameWithoutExtension(filePath);
|
|
||||||
if (string.IsNullOrEmpty(fileName) || !IsSha1Hash(fileName))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var relative = Path.GetRelativePath(cacheFolder, filePath)
|
|
||||||
.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
|
|
||||||
var sanitizedRelative = relative.TrimStart(Path.DirectorySeparatorChar);
|
|
||||||
prefixedPath = Path.Combine(FileCacheManager.CachePrefix, sanitizedRelative);
|
|
||||||
hash = fileName;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsSha1Hash(string value)
|
|
||||||
{
|
|
||||||
if (value.Length != 40)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var ch in value)
|
|
||||||
{
|
|
||||||
if (!Uri.IsHexDigit(ch))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ResumeScan(string source)
|
public void ResumeScan(string source)
|
||||||
{
|
{
|
||||||
if (!HaltScanLocks.ContainsKey(source)) HaltScanLocks[source] = 0;
|
if (!HaltScanLocks.ContainsKey(source)) HaltScanLocks[source] = 0;
|
||||||
|
|||||||
@@ -1,241 +0,0 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO.Pipes;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace LightlessSync.FileCache;
|
|
||||||
|
|
||||||
internal sealed class ExternalCompactionExecutor : ICompactionExecutor, IDisposable
|
|
||||||
{
|
|
||||||
private readonly ILogger<ExternalCompactionExecutor> _logger;
|
|
||||||
private readonly ICompactorContext _context;
|
|
||||||
private readonly TimeSpan _timeout = TimeSpan.FromMinutes(5);
|
|
||||||
private readonly string _pipeName;
|
|
||||||
private Process? _workerProcess;
|
|
||||||
private bool _disposed;
|
|
||||||
private readonly object _sync = new();
|
|
||||||
|
|
||||||
public ExternalCompactionExecutor(ILogger<ExternalCompactionExecutor> logger, ICompactorContext context)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_context = context;
|
|
||||||
_pipeName = $"LightlessCompactor-{Environment.ProcessId}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryCompact(string filePath)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!EnsureWorkerRunning())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var request = new CompactorRequest
|
|
||||||
{
|
|
||||||
Type = "compact",
|
|
||||||
Path = filePath
|
|
||||||
};
|
|
||||||
|
|
||||||
return SendRequest(request, out var response) && response?.Success == true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "External compactor failed for {file}", filePath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
SendRequest(new CompactorRequest { Type = "shutdown" }, out _);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_sync)
|
|
||||||
{
|
|
||||||
if (_workerProcess is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
TryKill(_workerProcess);
|
|
||||||
_workerProcess.Dispose();
|
|
||||||
_workerProcess = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool EnsureWorkerRunning()
|
|
||||||
{
|
|
||||||
lock (_sync)
|
|
||||||
{
|
|
||||||
if (_workerProcess is { HasExited: false })
|
|
||||||
return true;
|
|
||||||
|
|
||||||
_workerProcess?.Dispose();
|
|
||||||
_workerProcess = null;
|
|
||||||
|
|
||||||
var workerPath = ResolveWorkerPath();
|
|
||||||
if (string.IsNullOrEmpty(workerPath))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var args = BuildArguments();
|
|
||||||
var startInfo = new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = workerPath,
|
|
||||||
Arguments = args,
|
|
||||||
CreateNoWindow = true,
|
|
||||||
UseShellExecute = false,
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
RedirectStandardError = true
|
|
||||||
};
|
|
||||||
|
|
||||||
var process = new Process { StartInfo = startInfo };
|
|
||||||
if (!process.Start())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
TrySetLowPriority(process);
|
|
||||||
_ = DrainAsync(process.StandardOutput, "stdout");
|
|
||||||
_ = DrainAsync(process.StandardError, "stderr");
|
|
||||||
|
|
||||||
_workerProcess = process;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool SendRequest(CompactorRequest request, out CompactorResponse? response)
|
|
||||||
{
|
|
||||||
response = null;
|
|
||||||
using var pipe = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
pipe.Connect((int)_timeout.TotalMilliseconds);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogDebug(ex, "Compactor pipe connection failed.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var writer = new StreamWriter(pipe) { AutoFlush = true };
|
|
||||||
using var reader = new StreamReader(pipe);
|
|
||||||
|
|
||||||
var payload = JsonSerializer.Serialize(request);
|
|
||||||
writer.WriteLine(payload);
|
|
||||||
|
|
||||||
var readTask = reader.ReadLineAsync();
|
|
||||||
if (!readTask.Wait(_timeout))
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Compactor pipe timed out waiting for response.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var line = readTask.Result;
|
|
||||||
if (string.IsNullOrWhiteSpace(line))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
response = JsonSerializer.Deserialize<CompactorResponse>(line);
|
|
||||||
return response is not null;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogDebug(ex, "Failed to parse compactor response.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string? ResolveWorkerPath()
|
|
||||||
{
|
|
||||||
var baseDir = AppContext.BaseDirectory;
|
|
||||||
var exeName = OperatingSystem.IsWindows() || _context.IsWine
|
|
||||||
? "LightlessCompactorWorker.exe"
|
|
||||||
: "LightlessCompactorWorker";
|
|
||||||
var path = Path.Combine(baseDir, exeName);
|
|
||||||
return File.Exists(path) ? path : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string BuildArguments()
|
|
||||||
{
|
|
||||||
var args = new List<string> { "--pipe", Quote(_pipeName), "--parent", Environment.ProcessId.ToString() };
|
|
||||||
if (_context.IsWine)
|
|
||||||
args.Add("--wine");
|
|
||||||
return string.Join(' ', args);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Quote(string value)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(value))
|
|
||||||
return "\"\"";
|
|
||||||
|
|
||||||
if (!value.Contains('"', StringComparison.Ordinal))
|
|
||||||
return "\"" + value + "\"";
|
|
||||||
|
|
||||||
return "\"" + value.Replace("\"", "\\\"", StringComparison.Ordinal) + "\"";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void TrySetLowPriority(Process process)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (OperatingSystem.IsWindows())
|
|
||||||
process.PriorityClass = ProcessPriorityClass.BelowNormal;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DrainAsync(StreamReader reader, string label)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string? line;
|
|
||||||
while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null)
|
|
||||||
{
|
|
||||||
if (_logger.IsEnabled(LogLevel.Trace))
|
|
||||||
_logger.LogTrace("Compactor {label}: {line}", label, line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void TryKill(Process process)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
process.Kill(entireProcessTree: true);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class CompactorRequest
|
|
||||||
{
|
|
||||||
public string Type { get; init; } = "compact";
|
|
||||||
public string? Path { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class CompactorResponse
|
|
||||||
{
|
|
||||||
public bool Success { get; init; }
|
|
||||||
public string? Error { get; init; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,15 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace LightlessSync.FileCache;
|
namespace LightlessSync.FileCache;
|
||||||
|
|
||||||
public class FileCacheEntity
|
public class FileCacheEntity
|
||||||
{
|
{
|
||||||
[JsonConstructor]
|
public FileCacheEntity(string hash, string path, string lastModifiedDateTicks, long? size = null, long? compressedSize = null)
|
||||||
public FileCacheEntity(
|
|
||||||
string hash,
|
|
||||||
string prefixedFilePath,
|
|
||||||
string lastModifiedDateTicks,
|
|
||||||
long? size = null,
|
|
||||||
long? compressedSize = null)
|
|
||||||
{
|
{
|
||||||
Size = size;
|
Size = size;
|
||||||
CompressedSize = compressedSize;
|
CompressedSize = compressedSize;
|
||||||
Hash = hash;
|
Hash = hash;
|
||||||
PrefixedFilePath = prefixedFilePath;
|
PrefixedFilePath = path;
|
||||||
LastModifiedDateTicks = lastModifiedDateTicks;
|
LastModifiedDateTicks = lastModifiedDateTicks;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,5 +23,7 @@ public class FileCacheEntity
|
|||||||
public long? Size { get; set; }
|
public long? Size { get; set; }
|
||||||
|
|
||||||
public void SetResolvedFilePath(string filePath)
|
public void SetResolvedFilePath(string filePath)
|
||||||
=> ResolvedFilepath = filePath.ToLowerInvariant().Replace("\\\\", "\\", StringComparison.Ordinal);
|
{
|
||||||
|
ResolvedFilepath = filePath.ToLowerInvariant().Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,8 +7,6 @@ using Microsoft.Extensions.Hosting;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace LightlessSync.FileCache;
|
namespace LightlessSync.FileCache;
|
||||||
@@ -27,21 +25,12 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, FileCacheEntity>> _fileCaches = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, FileCacheEntity>> _fileCaches = new(StringComparer.Ordinal);
|
||||||
private readonly ConcurrentDictionary<string, FileCacheEntity> _fileCachesByPrefixedPath = new(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, FileCacheEntity> _fileCachesByPrefixedPath = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private readonly SemaphoreSlim _getCachesByPathsSemaphore = new(1, 1);
|
private readonly SemaphoreSlim _getCachesByPathsSemaphore = new(1, 1);
|
||||||
private readonly SemaphoreSlim _evictSemaphore = new(1, 1);
|
|
||||||
private readonly Lock _fileWriteLock = new();
|
private readonly Lock _fileWriteLock = new();
|
||||||
private readonly IpcManager _ipcManager;
|
private readonly IpcManager _ipcManager;
|
||||||
private readonly ILogger<FileCacheManager> _logger;
|
private readonly ILogger<FileCacheManager> _logger;
|
||||||
private bool _csvHeaderEnsured;
|
private bool _csvHeaderEnsured;
|
||||||
public string CacheFolder => _configService.Current.CacheFolder;
|
public string CacheFolder => _configService.Current.CacheFolder;
|
||||||
|
|
||||||
private const string _compressedCacheExtension = ".llz4";
|
|
||||||
private readonly ConcurrentDictionary<string, SemaphoreSlim> _compressLocks = new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
private readonly ConcurrentDictionary<string, SizeInfo> _sizeCache =
|
|
||||||
new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Auto)]
|
|
||||||
public readonly record struct SizeInfo(long Original, long Compressed);
|
|
||||||
|
|
||||||
public FileCacheManager(ILogger<FileCacheManager> logger, IpcManager ipcManager, LightlessConfigService configService, LightlessMediator lightlessMediator)
|
public FileCacheManager(ILogger<FileCacheManager> logger, IpcManager ipcManager, LightlessConfigService configService, LightlessMediator lightlessMediator)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -56,18 +45,6 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
private static string NormalizeSeparators(string path) => path.Replace("/", "\\", StringComparison.Ordinal)
|
private static string NormalizeSeparators(string path) => path.Replace("/", "\\", StringComparison.Ordinal)
|
||||||
.Replace("\\\\", "\\", StringComparison.Ordinal);
|
.Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||||
|
|
||||||
private SemaphoreSlim GetCompressLock(string hash)
|
|
||||||
=> _compressLocks.GetOrAdd(hash, _ => new SemaphoreSlim(1, 1));
|
|
||||||
|
|
||||||
public void SetSizeInfo(string hash, long original, long compressed)
|
|
||||||
=> _sizeCache[hash] = new SizeInfo(original, compressed);
|
|
||||||
|
|
||||||
public bool TryGetSizeInfo(string hash, out SizeInfo info)
|
|
||||||
=> _sizeCache.TryGetValue(hash, out info);
|
|
||||||
|
|
||||||
private string GetCompressedCachePath(string hash)
|
|
||||||
=> Path.Combine(CacheFolder, hash + _compressedCacheExtension);
|
|
||||||
|
|
||||||
private static string NormalizePrefixedPathKey(string prefixedPath)
|
private static string NormalizePrefixedPathKey(string prefixedPath)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(prefixedPath))
|
if (string.IsNullOrEmpty(prefixedPath))
|
||||||
@@ -115,35 +92,6 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryGetHashFromFileName(FileInfo fileInfo, out string hash)
|
|
||||||
{
|
|
||||||
hash = Path.GetFileNameWithoutExtension(fileInfo.Name);
|
|
||||||
if (string.IsNullOrWhiteSpace(hash))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hash.Length is not (40 or 64))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < hash.Length; i++)
|
|
||||||
{
|
|
||||||
var c = hash[i];
|
|
||||||
var isHex = (c >= '0' && c <= '9')
|
|
||||||
|| (c >= 'a' && c <= 'f')
|
|
||||||
|| (c >= 'A' && c <= 'F');
|
|
||||||
if (!isHex)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hash = hash.ToUpperInvariant();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildVersionHeader() => $"{FileCacheVersionHeaderPrefix}{FileCacheVersion}";
|
private static string BuildVersionHeader() => $"{FileCacheVersionHeaderPrefix}{FileCacheVersion}";
|
||||||
|
|
||||||
private static bool TryParseVersionHeader(string? line, out int version)
|
private static bool TryParseVersionHeader(string? line, out int version)
|
||||||
@@ -163,124 +111,6 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
return int.TryParse(versionSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out version);
|
return int.TryParse(versionSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out version);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateSizeInfo(string hash, long? original = null, long? compressed = null)
|
|
||||||
{
|
|
||||||
_sizeCache.AddOrUpdate(
|
|
||||||
hash,
|
|
||||||
_ => new SizeInfo(original ?? 0, compressed ?? 0),
|
|
||||||
(_, old) => new SizeInfo(original ?? old.Original, compressed ?? old.Compressed));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateEntitiesSizes(string hash, long original, long compressed)
|
|
||||||
{
|
|
||||||
if (_fileCaches.TryGetValue(hash, out var dict))
|
|
||||||
{
|
|
||||||
foreach (var e in dict.Values)
|
|
||||||
{
|
|
||||||
e.Size = original;
|
|
||||||
e.CompressedSize = compressed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void ApplySizesToEntries(IEnumerable<FileCacheEntity?> entries, long original, long compressed)
|
|
||||||
{
|
|
||||||
foreach (var e in entries)
|
|
||||||
{
|
|
||||||
if (e == null) continue;
|
|
||||||
e.Size = original;
|
|
||||||
e.CompressedSize = compressed > 0 ? compressed : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<long> GetCompressedSizeAsync(string hash, CancellationToken token)
|
|
||||||
{
|
|
||||||
if (_sizeCache.TryGetValue(hash, out var info) && info.Compressed > 0)
|
|
||||||
return info.Compressed;
|
|
||||||
|
|
||||||
if (_fileCaches.TryGetValue(hash, out var dict))
|
|
||||||
{
|
|
||||||
var any = dict.Values.FirstOrDefault();
|
|
||||||
if (any != null && any.CompressedSize > 0)
|
|
||||||
{
|
|
||||||
UpdateSizeInfo(hash, original: any.Size > 0 ? any.Size : null, compressed: any.CompressedSize);
|
|
||||||
return (long)any.CompressedSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(CacheFolder))
|
|
||||||
{
|
|
||||||
var path = GetCompressedCachePath(hash);
|
|
||||||
if (File.Exists(path))
|
|
||||||
{
|
|
||||||
var len = new FileInfo(path).Length;
|
|
||||||
UpdateSizeInfo(hash, compressed: len);
|
|
||||||
return len;
|
|
||||||
}
|
|
||||||
|
|
||||||
var bytes = await EnsureCompressedCacheBytesAsync(hash, token).ConfigureAwait(false);
|
|
||||||
return bytes.LongLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fallback = await GetCompressedFileData(hash, token).ConfigureAwait(false);
|
|
||||||
return fallback.Item2.LongLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<byte[]> EnsureCompressedCacheBytesAsync(string hash, CancellationToken token)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(CacheFolder))
|
|
||||||
throw new InvalidOperationException("CacheFolder is not set; cannot persist compressed cache.");
|
|
||||||
|
|
||||||
Directory.CreateDirectory(CacheFolder);
|
|
||||||
|
|
||||||
var compressedPath = GetCompressedCachePath(hash);
|
|
||||||
|
|
||||||
if (File.Exists(compressedPath))
|
|
||||||
return await File.ReadAllBytesAsync(compressedPath, token).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var sem = GetCompressLock(hash);
|
|
||||||
await sem.WaitAsync(token).ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (File.Exists(compressedPath))
|
|
||||||
return await File.ReadAllBytesAsync(compressedPath, token).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var entity = GetFileCacheByHash(hash);
|
|
||||||
if (entity == null || string.IsNullOrWhiteSpace(entity.ResolvedFilepath))
|
|
||||||
throw new InvalidOperationException($"No local file cache found for hash {hash}.");
|
|
||||||
|
|
||||||
var sourcePath = entity.ResolvedFilepath;
|
|
||||||
var originalSize = new FileInfo(sourcePath).Length;
|
|
||||||
|
|
||||||
var raw = await File.ReadAllBytesAsync(sourcePath, token).ConfigureAwait(false);
|
|
||||||
var compressed = LZ4Wrapper.WrapHC(raw, 0, raw.Length);
|
|
||||||
|
|
||||||
var tmpPath = compressedPath + ".tmp";
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await File.WriteAllBytesAsync(tmpPath, compressed, token).ConfigureAwait(false);
|
|
||||||
File.Move(tmpPath, compressedPath, overwrite: true);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
try { if (File.Exists(tmpPath)) File.Delete(tmpPath); } catch { /* ignore */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
var compressedSize = new FileInfo(compressedPath).Length;
|
|
||||||
SetSizeInfo(hash, originalSize, compressedSize);
|
|
||||||
UpdateEntitiesSizes(hash, originalSize, compressedSize);
|
|
||||||
|
|
||||||
var maxBytes = GiBToBytes(_configService.Current.MaxLocalCacheInGiB);
|
|
||||||
await EnforceCacheLimitAsync(maxBytes, token).ConfigureAwait(false);
|
|
||||||
|
|
||||||
return compressed;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
sem.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string NormalizeToPrefixedPath(string path)
|
private string NormalizeToPrefixedPath(string path)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(path)) return string.Empty;
|
if (string.IsNullOrEmpty(path)) return string.Empty;
|
||||||
@@ -317,34 +147,9 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
_logger.LogTrace("Creating cache entry for {path}", path);
|
_logger.LogTrace("Creating cache entry for {path}", path);
|
||||||
var cacheFolder = _configService.Current.CacheFolder;
|
var cacheFolder = _configService.Current.CacheFolder;
|
||||||
if (string.IsNullOrEmpty(cacheFolder)) return null;
|
if (string.IsNullOrEmpty(cacheFolder)) return null;
|
||||||
if (TryGetHashFromFileName(fi, out var hash))
|
|
||||||
{
|
|
||||||
return CreateCacheEntryWithKnownHash(fi.FullName, hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
return CreateFileEntity(cacheFolder, CachePrefix, fi);
|
return CreateFileEntity(cacheFolder, CachePrefix, fi);
|
||||||
}
|
}
|
||||||
|
|
||||||
public FileCacheEntity? CreateCacheEntryWithKnownHash(string path, string hash)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(hash))
|
|
||||||
{
|
|
||||||
return CreateCacheEntry(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
FileInfo fi = new(path);
|
|
||||||
if (!fi.Exists) return null;
|
|
||||||
_logger.LogTrace("Creating cache entry for {path} using provided hash", path);
|
|
||||||
var cacheFolder = _configService.Current.CacheFolder;
|
|
||||||
if (string.IsNullOrEmpty(cacheFolder)) return null;
|
|
||||||
if (!TryBuildPrefixedPath(fi.FullName, cacheFolder, CachePrefix, out var prefixedPath, out _))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return CreateFileCacheEntity(fi, prefixedPath, hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
public FileCacheEntity? CreateFileEntry(string path)
|
public FileCacheEntity? CreateFileEntry(string path)
|
||||||
{
|
{
|
||||||
FileInfo fi = new(path);
|
FileInfo fi = new(path);
|
||||||
@@ -513,18 +318,9 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
|
|
||||||
public async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken)
|
public async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(CacheFolder))
|
|
||||||
{
|
|
||||||
var bytes = await EnsureCompressedCacheBytesAsync(fileHash, uploadToken).ConfigureAwait(false);
|
|
||||||
UpdateSizeInfo(fileHash, compressed: bytes.LongLength);
|
|
||||||
return (fileHash, bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
var fileCache = GetFileCacheByHash(fileHash)!.ResolvedFilepath;
|
var fileCache = GetFileCacheByHash(fileHash)!.ResolvedFilepath;
|
||||||
var raw = await File.ReadAllBytesAsync(fileCache, uploadToken).ConfigureAwait(false);
|
return (fileHash, LZ4Wrapper.WrapHC(await File.ReadAllBytesAsync(fileCache, uploadToken).ConfigureAwait(false), 0,
|
||||||
var compressed = LZ4Wrapper.WrapHC(raw, 0, raw.Length);
|
(int)new FileInfo(fileCache).Length));
|
||||||
UpdateSizeInfo(fileHash, original: raw.LongLength, compressed: compressed.LongLength);
|
|
||||||
return (fileHash, compressed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public FileCacheEntity? GetFileCacheByHash(string hash)
|
public FileCacheEntity? GetFileCacheByHash(string hash)
|
||||||
@@ -627,10 +423,9 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveHashedFile(string hash, string prefixedFilePath, bool removeDerivedFiles = true)
|
public void RemoveHashedFile(string hash, string prefixedFilePath)
|
||||||
{
|
{
|
||||||
var normalizedPath = NormalizePrefixedPathKey(prefixedFilePath);
|
var normalizedPath = NormalizePrefixedPathKey(prefixedFilePath);
|
||||||
var removedHash = false;
|
|
||||||
|
|
||||||
if (_fileCaches.TryGetValue(hash, out var caches))
|
if (_fileCaches.TryGetValue(hash, out var caches))
|
||||||
{
|
{
|
||||||
@@ -643,16 +438,11 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
|
|
||||||
if (caches.IsEmpty)
|
if (caches.IsEmpty)
|
||||||
{
|
{
|
||||||
removedHash = _fileCaches.TryRemove(hash, out _);
|
_fileCaches.TryRemove(hash, out _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_fileCachesByPrefixedPath.TryRemove(normalizedPath, out _);
|
_fileCachesByPrefixedPath.TryRemove(normalizedPath, out _);
|
||||||
|
|
||||||
if (removeDerivedFiles && removedHash)
|
|
||||||
{
|
|
||||||
RemoveDerivedCacheFiles(hash);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateHashedFile(FileCacheEntity fileCache, bool computeProperties = true)
|
public void UpdateHashedFile(FileCacheEntity fileCache, bool computeProperties = true)
|
||||||
@@ -668,8 +458,7 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
fileCache.Hash = Crypto.ComputeFileHash(fileCache.ResolvedFilepath, Crypto.HashAlgo.Sha1);
|
fileCache.Hash = Crypto.ComputeFileHash(fileCache.ResolvedFilepath, Crypto.HashAlgo.Sha1);
|
||||||
fileCache.LastModifiedDateTicks = fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture);
|
fileCache.LastModifiedDateTicks = fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
var removeDerivedFiles = !string.Equals(oldHash, fileCache.Hash, StringComparison.OrdinalIgnoreCase);
|
RemoveHashedFile(oldHash, prefixedPath);
|
||||||
RemoveHashedFile(oldHash, prefixedPath, removeDerivedFiles);
|
|
||||||
AddHashedFile(fileCache);
|
AddHashedFile(fileCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -819,7 +608,7 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath, removeDerivedFiles: false);
|
RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath);
|
||||||
var extensionPath = fileCache.ResolvedFilepath.ToUpper(CultureInfo.InvariantCulture) + "." + ext;
|
var extensionPath = fileCache.ResolvedFilepath.ToUpper(CultureInfo.InvariantCulture) + "." + ext;
|
||||||
File.Move(fileCache.ResolvedFilepath, extensionPath, overwrite: true);
|
File.Move(fileCache.ResolvedFilepath, extensionPath, overwrite: true);
|
||||||
var newHashedEntity = new FileCacheEntity(fileCache.Hash, fileCache.PrefixedFilePath + "." + ext, DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture));
|
var newHashedEntity = new FileCacheEntity(fileCache.Hash, fileCache.PrefixedFilePath + "." + ext, DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture));
|
||||||
@@ -836,33 +625,6 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveDerivedCacheFiles(string hash)
|
|
||||||
{
|
|
||||||
var cacheFolder = _configService.Current.CacheFolder;
|
|
||||||
if (string.IsNullOrWhiteSpace(cacheFolder))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
TryDeleteDerivedCacheFile(Path.Combine(cacheFolder, "downscaled", $"{hash}.tex"));
|
|
||||||
TryDeleteDerivedCacheFile(Path.Combine(cacheFolder, "decimated", $"{hash}.mdl"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TryDeleteDerivedCacheFile(string path)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (File.Exists(path))
|
|
||||||
{
|
|
||||||
File.Delete(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogTrace(ex, "Failed to delete derived cache file {path}", path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddHashedFile(FileCacheEntity fileCache)
|
private void AddHashedFile(FileCacheEntity fileCache)
|
||||||
{
|
{
|
||||||
var normalizedPath = NormalizePrefixedPathKey(fileCache.PrefixedFilePath);
|
var normalizedPath = NormalizePrefixedPathKey(fileCache.PrefixedFilePath);
|
||||||
@@ -976,83 +738,6 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
}, token).ConfigureAwait(false);
|
}, token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task EnforceCacheLimitAsync(long maxBytes, CancellationToken token)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(CacheFolder) || maxBytes <= 0) return;
|
|
||||||
|
|
||||||
await _evictSemaphore.WaitAsync(token).ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(CacheFolder);
|
|
||||||
|
|
||||||
foreach (var tmp in Directory.EnumerateFiles(CacheFolder, "*" + _compressedCacheExtension + ".tmp"))
|
|
||||||
{
|
|
||||||
try { File.Delete(tmp); } catch { /* ignore */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
var files = Directory.EnumerateFiles(CacheFolder, "*" + _compressedCacheExtension, SearchOption.TopDirectoryOnly)
|
|
||||||
.Select(p => new FileInfo(p))
|
|
||||||
.Where(fi => fi.Exists)
|
|
||||||
.OrderBy(fi => fi.LastWriteTimeUtc)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
long total = files.Sum(f => f.Length);
|
|
||||||
if (total <= maxBytes) return;
|
|
||||||
|
|
||||||
foreach (var fi in files)
|
|
||||||
{
|
|
||||||
token.ThrowIfCancellationRequested();
|
|
||||||
if (total <= maxBytes) break;
|
|
||||||
|
|
||||||
var hash = Path.GetFileNameWithoutExtension(fi.Name);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var len = fi.Length;
|
|
||||||
fi.Delete();
|
|
||||||
total -= len;
|
|
||||||
_sizeCache.TryRemove(hash, out _);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Failed to evict cache file {file}", fi.FullName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_evictSemaphore.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long GiBToBytes(double gib)
|
|
||||||
{
|
|
||||||
if (double.IsNaN(gib) || double.IsInfinity(gib) || gib <= 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
var bytes = gib * 1024d * 1024d * 1024d;
|
|
||||||
|
|
||||||
if (bytes >= long.MaxValue) return long.MaxValue;
|
|
||||||
|
|
||||||
return (long)Math.Round(bytes, MidpointRounding.AwayFromZero);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CleanupOrphanCompressedCache()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(CacheFolder) || !Directory.Exists(CacheFolder))
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var path in Directory.EnumerateFiles(CacheFolder, "*" + _compressedCacheExtension))
|
|
||||||
{
|
|
||||||
var hash = Path.GetFileNameWithoutExtension(path);
|
|
||||||
if (!_fileCaches.ContainsKey(hash))
|
|
||||||
{
|
|
||||||
try { File.Delete(path); }
|
|
||||||
catch (Exception ex) { _logger.LogWarning(ex, "Failed deleting orphan {file}", path); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task StartAsync(CancellationToken cancellationToken)
|
public async Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Starting FileCacheManager");
|
_logger.LogInformation("Starting FileCacheManager");
|
||||||
@@ -1206,14 +891,6 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
compressed = resultCompressed;
|
compressed = resultCompressed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (size > 0 || compressed > 0)
|
|
||||||
{
|
|
||||||
UpdateSizeInfo(hash,
|
|
||||||
original: size > 0 ? size : null,
|
|
||||||
compressed: compressed > 0 ? compressed : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time, size, compressed)));
|
AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time, size, compressed)));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -1236,8 +913,6 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
{
|
{
|
||||||
await WriteOutFullCsvAsync(cancellationToken).ConfigureAwait(false);
|
await WriteOutFullCsvAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
CleanupOrphanCompressedCache();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Started FileCacheManager");
|
_logger.LogInformation("Started FileCacheManager");
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using LightlessSync.Services.Compactor;
|
using LightlessSync.LightlessConfiguration;
|
||||||
|
using LightlessSync.Services;
|
||||||
|
using LightlessSync.Services.Compactor;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Win32.SafeHandles;
|
using Microsoft.Win32.SafeHandles;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
@@ -18,8 +20,8 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
|
|
||||||
private readonly ConcurrentDictionary<string, byte> _pendingCompactions;
|
private readonly ConcurrentDictionary<string, byte> _pendingCompactions;
|
||||||
private readonly ILogger<FileCompactor> _logger;
|
private readonly ILogger<FileCompactor> _logger;
|
||||||
private readonly ICompactorContext _context;
|
private readonly LightlessConfigService _lightlessConfigService;
|
||||||
private readonly ICompactionExecutor _compactionExecutor;
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
|
|
||||||
private readonly Channel<string> _compactionQueue;
|
private readonly Channel<string> _compactionQueue;
|
||||||
private readonly CancellationTokenSource _compactionCts = new();
|
private readonly CancellationTokenSource _compactionCts = new();
|
||||||
@@ -57,12 +59,12 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
XPRESS16K = 3
|
XPRESS16K = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
public FileCompactor(ILogger<FileCompactor> logger, ICompactorContext context, ICompactionExecutor compactionExecutor)
|
public FileCompactor(ILogger<FileCompactor> logger, LightlessConfigService lightlessConfigService, DalamudUtilService dalamudUtilService)
|
||||||
{
|
{
|
||||||
_pendingCompactions = new(StringComparer.OrdinalIgnoreCase);
|
_pendingCompactions = new(StringComparer.OrdinalIgnoreCase);
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger;
|
||||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
_lightlessConfigService = lightlessConfigService;
|
||||||
_compactionExecutor = compactionExecutor ?? throw new ArgumentNullException(nameof(compactionExecutor));
|
_dalamudUtilService = dalamudUtilService;
|
||||||
_isWindows = OperatingSystem.IsWindows();
|
_isWindows = OperatingSystem.IsWindows();
|
||||||
|
|
||||||
_compactionQueue = Channel.CreateUnbounded<string>(new UnboundedChannelOptions
|
_compactionQueue = Channel.CreateUnbounded<string>(new UnboundedChannelOptions
|
||||||
@@ -92,7 +94,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
|
|
||||||
//Uses an batching service for the filefrag command on Linux
|
//Uses an batching service for the filefrag command on Linux
|
||||||
_fragBatch = new BatchFilefragService(
|
_fragBatch = new BatchFilefragService(
|
||||||
useShell: _context.IsWine,
|
useShell: _dalamudUtilService.IsWine,
|
||||||
log: _logger,
|
log: _logger,
|
||||||
batchSize: 64,
|
batchSize: 64,
|
||||||
flushMs: 25,
|
flushMs: 25,
|
||||||
@@ -116,7 +118,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var folder = _context.CacheFolder;
|
var folder = _lightlessConfigService.Current.CacheFolder;
|
||||||
if (string.IsNullOrWhiteSpace(folder) || !Directory.Exists(folder))
|
if (string.IsNullOrWhiteSpace(folder) || !Directory.Exists(folder))
|
||||||
{
|
{
|
||||||
if (_logger.IsEnabled(LogLevel.Warning))
|
if (_logger.IsEnabled(LogLevel.Warning))
|
||||||
@@ -125,7 +127,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var files = Directory.EnumerateFiles(folder, "*", SearchOption.AllDirectories).ToArray();
|
var files = Directory.EnumerateFiles(folder).ToArray();
|
||||||
var total = files.Length;
|
var total = files.Length;
|
||||||
Progress = $"0/{total}";
|
Progress = $"0/{total}";
|
||||||
if (total == 0) return;
|
if (total == 0) return;
|
||||||
@@ -153,7 +155,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
{
|
{
|
||||||
if (compress)
|
if (compress)
|
||||||
{
|
{
|
||||||
if (_context.UseCompactor)
|
if (_lightlessConfigService.Current.UseCompactor)
|
||||||
CompactFile(file, workerId);
|
CompactFile(file, workerId);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -219,52 +221,19 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
|
|
||||||
await File.WriteAllBytesAsync(filePath, bytes, token).ConfigureAwait(false);
|
await File.WriteAllBytesAsync(filePath, bytes, token).ConfigureAwait(false);
|
||||||
|
|
||||||
if (_context.UseCompactor)
|
if (_lightlessConfigService.Current.UseCompactor)
|
||||||
EnqueueCompaction(filePath);
|
EnqueueCompaction(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Notify the compactor that a file was written directly (streamed) so it can enqueue compaction.
|
|
||||||
/// </summary>
|
|
||||||
public void NotifyFileWritten(string filePath)
|
|
||||||
{
|
|
||||||
EnqueueCompaction(filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryCompactFile(string filePath)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(filePath))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!_context.UseCompactor || !File.Exists(filePath))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
CompactFile(filePath, workerId: -1);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (IOException ioEx)
|
|
||||||
{
|
|
||||||
_logger.LogDebug(ioEx, "File being read/written, skipping file: {file}", filePath);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Error compacting file: {file}", filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the File size for an BTRFS or NTFS file system for the given FileInfo
|
/// Gets the File size for an BTRFS or NTFS file system for the given FileInfo
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">Amount of blocks used in the disk</param>
|
/// <param name="path">Amount of blocks used in the disk</param>
|
||||||
public long GetFileSizeOnDisk(FileInfo fileInfo)
|
public long GetFileSizeOnDisk(FileInfo fileInfo)
|
||||||
{
|
{
|
||||||
var fsType = GetFilesystemType(fileInfo.FullName, _context.IsWine);
|
var fsType = GetFilesystemType(fileInfo.FullName, _dalamudUtilService.IsWine);
|
||||||
|
|
||||||
if (fsType == FilesystemType.NTFS && !_context.IsWine)
|
if (fsType == FilesystemType.NTFS && !_dalamudUtilService.IsWine)
|
||||||
{
|
{
|
||||||
(bool flowControl, long value) = GetFileSizeNTFS(fileInfo);
|
(bool flowControl, long value) = GetFileSizeNTFS(fileInfo);
|
||||||
if (!flowControl)
|
if (!flowControl)
|
||||||
@@ -321,7 +290,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var blockSize = GetBlockSizeForPath(fileInfo.FullName, _logger, _context.IsWine);
|
var blockSize = GetBlockSizeForPath(fileInfo.FullName, _logger, _dalamudUtilService.IsWine);
|
||||||
if (blockSize <= 0)
|
if (blockSize <= 0)
|
||||||
throw new InvalidOperationException($"Invalid block size {blockSize} for {fileInfo.FullName}");
|
throw new InvalidOperationException($"Invalid block size {blockSize} for {fileInfo.FullName}");
|
||||||
|
|
||||||
@@ -361,7 +330,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var fsType = GetFilesystemType(filePath, _context.IsWine);
|
var fsType = GetFilesystemType(filePath, _dalamudUtilService.IsWine);
|
||||||
var oldSize = fi.Length;
|
var oldSize = fi.Length;
|
||||||
int blockSize = (int)(GetFileSizeOnDisk(fi) / 512);
|
int blockSize = (int)(GetFileSizeOnDisk(fi) / 512);
|
||||||
|
|
||||||
@@ -377,7 +346,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fsType == FilesystemType.NTFS && !_context.IsWine)
|
if (fsType == FilesystemType.NTFS && !_dalamudUtilService.IsWine)
|
||||||
{
|
{
|
||||||
if (!IsWOFCompactedFile(filePath))
|
if (!IsWOFCompactedFile(filePath))
|
||||||
{
|
{
|
||||||
@@ -433,9 +402,9 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
private void DecompressFile(string filePath, int workerId)
|
private void DecompressFile(string filePath, int workerId)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("[W{worker}] Decompress request: {file}", workerId, filePath);
|
_logger.LogDebug("[W{worker}] Decompress request: {file}", workerId, filePath);
|
||||||
var fsType = GetFilesystemType(filePath, _context.IsWine);
|
var fsType = GetFilesystemType(filePath, _dalamudUtilService.IsWine);
|
||||||
|
|
||||||
if (fsType == FilesystemType.NTFS && !_context.IsWine)
|
if (fsType == FilesystemType.NTFS && !_dalamudUtilService.IsWine)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -479,7 +448,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
bool isWine = _context.IsWine;
|
bool isWine = _dalamudUtilService?.IsWine ?? false;
|
||||||
string linuxPath = isWine ? ToLinuxPathIfWine(path, isWine) : path;
|
string linuxPath = isWine ? ToLinuxPathIfWine(path, isWine) : path;
|
||||||
|
|
||||||
var opts = GetMountOptionsForPath(linuxPath);
|
var opts = GetMountOptionsForPath(linuxPath);
|
||||||
@@ -992,7 +961,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
if (finished != bothTasks)
|
if (finished != bothTasks)
|
||||||
return KillProcess(proc, outTask, errTask, token);
|
return KillProcess(proc, outTask, errTask, token);
|
||||||
|
|
||||||
bool isWine = _context.IsWine;
|
bool isWine = _dalamudUtilService?.IsWine ?? false;
|
||||||
if (!isWine)
|
if (!isWine)
|
||||||
{
|
{
|
||||||
try { proc.WaitForExit(); } catch { /* ignore quirks */ }
|
try { proc.WaitForExit(); } catch { /* ignore quirks */ }
|
||||||
@@ -1036,7 +1005,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
if (string.IsNullOrWhiteSpace(filePath))
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!_context.UseCompactor)
|
if (!_lightlessConfigService.Current.UseCompactor)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!File.Exists(filePath))
|
if (!File.Exists(filePath))
|
||||||
@@ -1048,7 +1017,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
bool enqueued = false;
|
bool enqueued = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
bool isWine = _context.IsWine;
|
bool isWine = _dalamudUtilService?.IsWine ?? false;
|
||||||
var fsType = GetFilesystemType(filePath, isWine);
|
var fsType = GetFilesystemType(filePath, isWine);
|
||||||
|
|
||||||
// If under Wine, we should skip NTFS because its not Windows but might return NTFS.
|
// If under Wine, we should skip NTFS because its not Windows but might return NTFS.
|
||||||
@@ -1101,11 +1070,8 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_context.UseCompactor && File.Exists(filePath))
|
if (_lightlessConfigService.Current.UseCompactor && File.Exists(filePath))
|
||||||
{
|
CompactFile(filePath, workerId);
|
||||||
if (!_compactionExecutor.TryCompact(filePath))
|
|
||||||
CompactFile(filePath, workerId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using LightlessSync.LightlessConfiguration;
|
|
||||||
using LightlessSync.Services;
|
|
||||||
|
|
||||||
namespace LightlessSync.FileCache;
|
|
||||||
|
|
||||||
internal sealed class PluginCompactorContext : ICompactorContext
|
|
||||||
{
|
|
||||||
private readonly LightlessConfigService _configService;
|
|
||||||
private readonly DalamudUtilService _dalamudUtilService;
|
|
||||||
|
|
||||||
public PluginCompactorContext(LightlessConfigService configService, DalamudUtilService dalamudUtilService)
|
|
||||||
{
|
|
||||||
_configService = configService;
|
|
||||||
_dalamudUtilService = dalamudUtilService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool UseCompactor => _configService.Current.UseCompactor;
|
|
||||||
public string CacheFolder => _configService.Current.CacheFolder;
|
|
||||||
public bool IsWine => _dalamudUtilService.IsWine;
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,9 @@ using LightlessSync.Services.Mediator;
|
|||||||
using LightlessSync.Utils;
|
using LightlessSync.Utils;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Linq;
|
||||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||||
|
|
||||||
namespace LightlessSync.FileCache;
|
namespace LightlessSync.FileCache;
|
||||||
@@ -26,8 +29,6 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
private readonly string[] _handledFileTypes = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk", "kdb"];
|
private readonly string[] _handledFileTypes = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk", "kdb"];
|
||||||
private readonly string[] _handledRecordingFileTypes = ["tex", "mdl", "mtrl"];
|
private readonly string[] _handledRecordingFileTypes = ["tex", "mdl", "mtrl"];
|
||||||
private readonly HashSet<GameObjectHandler> _playerRelatedPointers = [];
|
private readonly HashSet<GameObjectHandler> _playerRelatedPointers = [];
|
||||||
private readonly object _playerRelatedLock = new();
|
|
||||||
private readonly ConcurrentDictionary<nint, GameObjectHandler> _playerRelatedByAddress = new();
|
|
||||||
private readonly Dictionary<nint, GameObjectHandler> _ownedHandlers = new();
|
private readonly Dictionary<nint, GameObjectHandler> _ownedHandlers = new();
|
||||||
private ConcurrentDictionary<nint, ObjectKind> _cachedFrameAddresses = new();
|
private ConcurrentDictionary<nint, ObjectKind> _cachedFrameAddresses = new();
|
||||||
private ConcurrentDictionary<ObjectKind, HashSet<string>>? _semiTransientResources = null;
|
private ConcurrentDictionary<ObjectKind, HashSet<string>>? _semiTransientResources = null;
|
||||||
@@ -41,6 +42,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
_dalamudUtil = dalamudUtil;
|
_dalamudUtil = dalamudUtil;
|
||||||
_actorObjectService = actorObjectService;
|
_actorObjectService = actorObjectService;
|
||||||
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||||
|
|
||||||
Mediator.Subscribe<PenumbraResourceLoadMessage>(this, Manager_PenumbraResourceLoadEvent);
|
Mediator.Subscribe<PenumbraResourceLoadMessage>(this, Manager_PenumbraResourceLoadEvent);
|
||||||
Mediator.Subscribe<ActorTrackedMessage>(this, msg => HandleActorTracked(msg.Descriptor));
|
Mediator.Subscribe<ActorTrackedMessage>(this, msg => HandleActorTracked(msg.Descriptor));
|
||||||
Mediator.Subscribe<ActorUntrackedMessage>(this, msg => HandleActorUntracked(msg.Descriptor));
|
Mediator.Subscribe<ActorUntrackedMessage>(this, msg => HandleActorUntracked(msg.Descriptor));
|
||||||
@@ -49,21 +51,15 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
Mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, (msg) =>
|
Mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
if (!msg.OwnedObject) return;
|
if (!msg.OwnedObject) return;
|
||||||
lock (_playerRelatedLock)
|
_playerRelatedPointers.Add(msg.GameObjectHandler);
|
||||||
{
|
|
||||||
_playerRelatedPointers.Add(msg.GameObjectHandler);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
Mediator.Subscribe<GameObjectHandlerDestroyedMessage>(this, (msg) =>
|
Mediator.Subscribe<GameObjectHandlerDestroyedMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
if (!msg.OwnedObject) return;
|
if (!msg.OwnedObject) return;
|
||||||
lock (_playerRelatedLock)
|
_playerRelatedPointers.Remove(msg.GameObjectHandler);
|
||||||
{
|
|
||||||
_playerRelatedPointers.Remove(msg.GameObjectHandler);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
foreach (var descriptor in _actorObjectService.ObjectDescriptors)
|
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
|
||||||
{
|
{
|
||||||
HandleActorTracked(descriptor);
|
HandleActorTracked(descriptor);
|
||||||
}
|
}
|
||||||
@@ -82,7 +78,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string PlayerPersistentDataKey => _dalamudUtil.GetPlayerName() + "_" + _dalamudUtil.GetHomeWorldId();
|
private string PlayerPersistentDataKey => _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult() + "_" + _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult();
|
||||||
private ConcurrentDictionary<ObjectKind, HashSet<string>> SemiTransientResources
|
private ConcurrentDictionary<ObjectKind, HashSet<string>> SemiTransientResources
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -91,12 +87,9 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
_semiTransientResources = new();
|
_semiTransientResources = new();
|
||||||
PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData);
|
PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData);
|
||||||
_semiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache.Concat(jobSpecificData ?? [])
|
_semiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache.Concat(jobSpecificData ?? []).ToHashSet(StringComparer.Ordinal);
|
||||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
||||||
PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petSpecificData);
|
PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petSpecificData);
|
||||||
_semiTransientResources[ObjectKind.Pet] = new HashSet<string>(
|
_semiTransientResources[ObjectKind.Pet] = [.. petSpecificData ?? []];
|
||||||
petSpecificData ?? [],
|
|
||||||
StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return _semiTransientResources;
|
return _semiTransientResources;
|
||||||
@@ -134,14 +127,14 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
SemiTransientResources.TryGetValue(objectKind, out var result);
|
SemiTransientResources.TryGetValue(objectKind, out var result);
|
||||||
|
|
||||||
return result ?? new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
return result ?? new HashSet<string>(StringComparer.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PersistTransientResources(ObjectKind objectKind)
|
public void PersistTransientResources(ObjectKind objectKind)
|
||||||
{
|
{
|
||||||
if (!SemiTransientResources.TryGetValue(objectKind, out HashSet<string>? semiTransientResources))
|
if (!SemiTransientResources.TryGetValue(objectKind, out HashSet<string>? semiTransientResources))
|
||||||
{
|
{
|
||||||
SemiTransientResources[objectKind] = semiTransientResources = new(StringComparer.OrdinalIgnoreCase);
|
SemiTransientResources[objectKind] = semiTransientResources = new(StringComparer.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TransientResources.TryGetValue(objectKind, out var resources))
|
if (!TransientResources.TryGetValue(objectKind, out var resources))
|
||||||
@@ -159,7 +152,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
List<string> newlyAddedGamePaths;
|
List<string> newlyAddedGamePaths;
|
||||||
lock (semiTransientResources)
|
lock (semiTransientResources)
|
||||||
{
|
{
|
||||||
newlyAddedGamePaths = transientResources.Except(semiTransientResources, StringComparer.OrdinalIgnoreCase).ToList();
|
newlyAddedGamePaths = transientResources.Except(semiTransientResources, StringComparer.Ordinal).ToList();
|
||||||
foreach (var gamePath in transientResources)
|
foreach (var gamePath in transientResources)
|
||||||
{
|
{
|
||||||
semiTransientResources.Add(gamePath);
|
semiTransientResources.Add(gamePath);
|
||||||
@@ -204,13 +197,12 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public void RemoveTransientResource(ObjectKind objectKind, string path)
|
public void RemoveTransientResource(ObjectKind objectKind, string path)
|
||||||
{
|
{
|
||||||
var normalizedPath = NormalizeGamePath(path);
|
|
||||||
if (SemiTransientResources.TryGetValue(objectKind, out var resources))
|
if (SemiTransientResources.TryGetValue(objectKind, out var resources))
|
||||||
{
|
{
|
||||||
resources.Remove(normalizedPath);
|
resources.RemoveWhere(f => string.Equals(path, f, StringComparison.Ordinal));
|
||||||
if (objectKind == ObjectKind.Player)
|
if (objectKind == ObjectKind.Player)
|
||||||
{
|
{
|
||||||
PlayerConfig.RemovePath(normalizedPath, objectKind);
|
PlayerConfig.RemovePath(path, objectKind);
|
||||||
Logger.LogTrace("Saving transient.json from {method}", nameof(RemoveTransientResource));
|
Logger.LogTrace("Saving transient.json from {method}", nameof(RemoveTransientResource));
|
||||||
_configurationService.Save();
|
_configurationService.Save();
|
||||||
}
|
}
|
||||||
@@ -219,17 +211,16 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
internal bool AddTransientResource(ObjectKind objectKind, string item)
|
internal bool AddTransientResource(ObjectKind objectKind, string item)
|
||||||
{
|
{
|
||||||
var normalizedItem = NormalizeGamePath(item);
|
if (SemiTransientResources.TryGetValue(objectKind, out var semiTransient) && semiTransient != null && semiTransient.Contains(item))
|
||||||
if (SemiTransientResources.TryGetValue(objectKind, out var semiTransient) && semiTransient != null && semiTransient.Contains(normalizedItem))
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (!TransientResources.TryGetValue(objectKind, out HashSet<string>? transientResource))
|
if (!TransientResources.TryGetValue(objectKind, out HashSet<string>? transientResource))
|
||||||
{
|
{
|
||||||
transientResource = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
transientResource = new HashSet<string>(StringComparer.Ordinal);
|
||||||
TransientResources[objectKind] = transientResource;
|
TransientResources[objectKind] = transientResource;
|
||||||
}
|
}
|
||||||
|
|
||||||
return transientResource.Add(normalizedItem);
|
return transientResource.Add(item.ToLowerInvariant());
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void ClearTransientPaths(ObjectKind objectKind, List<string> list)
|
internal void ClearTransientPaths(ObjectKind objectKind, List<string> list)
|
||||||
@@ -294,73 +285,47 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private void DalamudUtil_FrameworkUpdate()
|
private void DalamudUtil_FrameworkUpdate()
|
||||||
{
|
{
|
||||||
_ = Task.Run(() => RefreshPlayerRelatedAddressMap());
|
|
||||||
|
|
||||||
lock (_cacheAdditionLock)
|
lock (_cacheAdditionLock)
|
||||||
{
|
{
|
||||||
_cachedHandledPaths.Clear();
|
_cachedHandledPaths.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_lastClassJobId != _dalamudUtil.ClassJobId)
|
var activeDescriptors = new Dictionary<nint, ObjectKind>();
|
||||||
|
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
|
||||||
{
|
{
|
||||||
UpdateClassJobCache();
|
if (TryResolveObjectKind(descriptor, out var resolvedKind))
|
||||||
}
|
|
||||||
|
|
||||||
CleanupAbsentObjects();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RefreshPlayerRelatedAddressMap()
|
|
||||||
{
|
|
||||||
var tempMap = new ConcurrentDictionary<nint, GameObjectHandler>();
|
|
||||||
var updatedFrameAddresses = new ConcurrentDictionary<nint, ObjectKind>();
|
|
||||||
|
|
||||||
lock (_playerRelatedLock)
|
|
||||||
{
|
|
||||||
foreach (var handler in _playerRelatedPointers)
|
|
||||||
{
|
{
|
||||||
var address = (nint)handler.Address;
|
activeDescriptors[descriptor.Address] = resolvedKind;
|
||||||
if (address != nint.Zero)
|
|
||||||
{
|
|
||||||
tempMap[address] = handler;
|
|
||||||
updatedFrameAddresses[address] = handler.ObjectKind;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_playerRelatedByAddress.Clear();
|
foreach (var address in _cachedFrameAddresses.Keys.ToList())
|
||||||
foreach (var kvp in tempMap)
|
|
||||||
{
|
{
|
||||||
_playerRelatedByAddress[kvp.Key] = kvp.Value;
|
if (!activeDescriptors.ContainsKey(address))
|
||||||
|
{
|
||||||
|
_cachedFrameAddresses.TryRemove(address, out _);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_cachedFrameAddresses.Clear();
|
foreach (var descriptor in activeDescriptors)
|
||||||
foreach (var kvp in updatedFrameAddresses)
|
|
||||||
{
|
{
|
||||||
_cachedFrameAddresses[kvp.Key] = kvp.Value;
|
_cachedFrameAddresses[descriptor.Key] = descriptor.Value;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateClassJobCache()
|
|
||||||
{
|
|
||||||
_lastClassJobId = _dalamudUtil.ClassJobId;
|
|
||||||
if (SemiTransientResources.TryGetValue(ObjectKind.Pet, out HashSet<string>? value))
|
|
||||||
{
|
|
||||||
value?.Clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData);
|
if (_lastClassJobId != _dalamudUtil.ClassJobId)
|
||||||
SemiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache
|
{
|
||||||
.Concat(jobSpecificData ?? [])
|
_lastClassJobId = _dalamudUtil.ClassJobId;
|
||||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
if (SemiTransientResources.TryGetValue(ObjectKind.Pet, out HashSet<string>? value))
|
||||||
|
{
|
||||||
|
value?.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petSpecificData);
|
PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData);
|
||||||
SemiTransientResources[ObjectKind.Pet] = new HashSet<string>(
|
SemiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache.Concat(jobSpecificData ?? []).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
petSpecificData ?? [],
|
PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petSpecificData);
|
||||||
StringComparer.OrdinalIgnoreCase);
|
SemiTransientResources[ObjectKind.Pet] = [.. petSpecificData ?? []];
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CleanupAbsentObjects()
|
|
||||||
{
|
|
||||||
foreach (var kind in Enum.GetValues(typeof(ObjectKind)).Cast<ObjectKind>())
|
foreach (var kind in Enum.GetValues(typeof(ObjectKind)).Cast<ObjectKind>())
|
||||||
{
|
{
|
||||||
if (!_cachedFrameAddresses.Any(k => k.Value == kind) && TransientResources.Remove(kind, out _))
|
if (!_cachedFrameAddresses.Any(k => k.Value == kind) && TransientResources.Remove(kind, out _))
|
||||||
@@ -375,12 +340,9 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
_ = Task.Run(() =>
|
_ = Task.Run(() =>
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Penumbra Mod Settings changed, verifying SemiTransientResources");
|
Logger.LogDebug("Penumbra Mod Settings changed, verifying SemiTransientResources");
|
||||||
lock (_playerRelatedLock)
|
foreach (var item in _playerRelatedPointers)
|
||||||
{
|
{
|
||||||
foreach (var item in _playerRelatedPointers)
|
Mediator.Publish(new TransientResourceChangedMessage(item.Address));
|
||||||
{
|
|
||||||
Mediator.Publish(new TransientResourceChangedMessage(item.Address));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -390,20 +352,41 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
_semiTransientResources = null;
|
_semiTransientResources = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryResolveObjectKind(ActorObjectService.ActorDescriptor descriptor, out ObjectKind resolvedKind)
|
||||||
|
{
|
||||||
|
if (descriptor.OwnedKind is ObjectKind ownedKind)
|
||||||
|
{
|
||||||
|
resolvedKind = ownedKind;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (descriptor.ObjectKind == DalamudObjectKind.Player)
|
||||||
|
{
|
||||||
|
resolvedKind = ObjectKind.Player;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedKind = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor)
|
private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor)
|
||||||
{
|
{
|
||||||
if (descriptor.IsInGpose)
|
if (descriptor.IsInGpose)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (descriptor.OwnedKind is not ObjectKind ownedKind)
|
if (!TryResolveObjectKind(descriptor, out var resolvedKind))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (Logger.IsEnabled(LogLevel.Debug))
|
if (Logger.IsEnabled(LogLevel.Debug))
|
||||||
{
|
{
|
||||||
Logger.LogDebug("ActorObject tracked: {kind} addr={address:X} name={name}", ownedKind, descriptor.Address, descriptor.Name);
|
Logger.LogDebug("ActorObject tracked: {kind} addr={address:X} name={name}", resolvedKind, descriptor.Address, descriptor.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
_cachedFrameAddresses[descriptor.Address] = ownedKind;
|
_cachedFrameAddresses[descriptor.Address] = resolvedKind;
|
||||||
|
|
||||||
|
if (descriptor.OwnedKind is not ObjectKind ownedKind)
|
||||||
|
return;
|
||||||
|
|
||||||
lock (_ownedHandlerLock)
|
lock (_ownedHandlerLock)
|
||||||
{
|
{
|
||||||
@@ -482,42 +465,6 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string NormalizeGamePath(string path)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(path))
|
|
||||||
return string.Empty;
|
|
||||||
|
|
||||||
return path.Replace("\\", "/", StringComparison.Ordinal).ToLowerInvariant();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string NormalizeFilePath(string path)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(path))
|
|
||||||
return string.Empty;
|
|
||||||
|
|
||||||
if (path.StartsWith("|", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
var lastPipe = path.LastIndexOf('|');
|
|
||||||
if (lastPipe >= 0 && lastPipe + 1 < path.Length)
|
|
||||||
{
|
|
||||||
path = path[(lastPipe + 1)..];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NormalizeGamePath(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool HasHandledFileType(string gamePath, string[] handledTypes)
|
|
||||||
{
|
|
||||||
for (var i = 0; i < handledTypes.Length; i++)
|
|
||||||
{
|
|
||||||
if (gamePath.EndsWith(handledTypes[i], StringComparison.Ordinal))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Manager_PenumbraResourceLoadEvent(PenumbraResourceLoadMessage msg)
|
private void Manager_PenumbraResourceLoadEvent(PenumbraResourceLoadMessage msg)
|
||||||
{
|
{
|
||||||
var gamePath = msg.GamePath.ToLowerInvariant();
|
var gamePath = msg.GamePath.ToLowerInvariant();
|
||||||
@@ -576,12 +523,11 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
TransientResources[objectKind] = transientResources;
|
TransientResources[objectKind] = transientResources;
|
||||||
}
|
}
|
||||||
|
|
||||||
_playerRelatedByAddress.TryGetValue(gameObjectAddress, out var owner);
|
var owner = _playerRelatedPointers.FirstOrDefault(f => f.Address == gameObjectAddress);
|
||||||
bool alreadyTransient = false;
|
bool alreadyTransient = false;
|
||||||
|
|
||||||
bool transientContains = transientResources.Contains(replacedGamePath);
|
bool transientContains = transientResources.Contains(replacedGamePath);
|
||||||
bool semiTransientContains = SemiTransientResources.SelectMany(k => k.Value)
|
bool semiTransientContains = SemiTransientResources.SelectMany(k => k.Value).Any(f => string.Equals(f, gamePath, StringComparison.OrdinalIgnoreCase));
|
||||||
.Any(f => string.Equals(f, gamePath, StringComparison.OrdinalIgnoreCase));
|
|
||||||
if (transientContains || semiTransientContains)
|
if (transientContains || semiTransientContains)
|
||||||
{
|
{
|
||||||
if (!IsTransientRecording)
|
if (!IsTransientRecording)
|
||||||
@@ -676,7 +622,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
if (!item.AddTransient || item.AlreadyTransient) continue;
|
if (!item.AddTransient || item.AlreadyTransient) continue;
|
||||||
if (!TransientResources.TryGetValue(item.Owner.ObjectKind, out var transient))
|
if (!TransientResources.TryGetValue(item.Owner.ObjectKind, out var transient))
|
||||||
{
|
{
|
||||||
TransientResources[item.Owner.ObjectKind] = transient = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
TransientResources[item.Owner.ObjectKind] = transient = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.LogTrace("Adding recorded: {gamePath} => {filePath}", item.GamePath, item.FilePath);
|
Logger.LogTrace("Adding recorded: {gamePath} => {filePath}", item.GamePath, item.FilePath);
|
||||||
@@ -703,4 +649,4 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
public bool AddTransient { get; set; }
|
public bool AddTransient { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -12,35 +11,24 @@ public unsafe class BlockedCharacterHandler
|
|||||||
private readonly Dictionary<CharaData, bool> _blockedCharacterCache = new();
|
private readonly Dictionary<CharaData, bool> _blockedCharacterCache = new();
|
||||||
|
|
||||||
private readonly ILogger<BlockedCharacterHandler> _logger;
|
private readonly ILogger<BlockedCharacterHandler> _logger;
|
||||||
private readonly IObjectTable _objectTable;
|
|
||||||
|
|
||||||
public BlockedCharacterHandler(ILogger<BlockedCharacterHandler> logger, IGameInteropProvider gameInteropProvider, IObjectTable objectTable)
|
public BlockedCharacterHandler(ILogger<BlockedCharacterHandler> logger, IGameInteropProvider gameInteropProvider)
|
||||||
{
|
{
|
||||||
gameInteropProvider.InitializeFromAttributes(this);
|
gameInteropProvider.InitializeFromAttributes(this);
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_objectTable = objectTable;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private CharaData? TryGetIdsFromPlayerPointer(nint ptr, ushort objectIndex)
|
private static CharaData GetIdsFromPlayerPointer(nint ptr)
|
||||||
{
|
{
|
||||||
if (ptr == nint.Zero || objectIndex >= 200)
|
if (ptr == nint.Zero) return new(0, 0);
|
||||||
return null;
|
var castChar = ((BattleChara*)ptr);
|
||||||
|
|
||||||
var obj = _objectTable[objectIndex];
|
|
||||||
if (obj is not IPlayerCharacter player || player.Address != ptr)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var castChar = (BattleChara*)player.Address;
|
|
||||||
return new(castChar->Character.AccountId, castChar->Character.ContentId);
|
return new(castChar->Character.AccountId, castChar->Character.ContentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsCharacterBlocked(nint ptr, ushort objectIndex, out bool firstTime)
|
public bool IsCharacterBlocked(nint ptr, out bool firstTime)
|
||||||
{
|
{
|
||||||
firstTime = false;
|
firstTime = false;
|
||||||
var combined = TryGetIdsFromPlayerPointer(ptr, objectIndex);
|
var combined = GetIdsFromPlayerPointer(ptr);
|
||||||
if (combined == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (_blockedCharacterCache.TryGetValue(combined, out var isBlocked))
|
if (_blockedCharacterCache.TryGetValue(combined, out var isBlocked))
|
||||||
return isBlocked;
|
return isBlocked;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
namespace Lifestream.Enums;
|
|
||||||
|
|
||||||
public enum ResidentialAetheryteKind
|
|
||||||
{
|
|
||||||
None = -1,
|
|
||||||
Uldah = 9,
|
|
||||||
Gridania = 2,
|
|
||||||
Limsa = 8,
|
|
||||||
Foundation = 70,
|
|
||||||
Kugane = 111,
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
namespace Lifestream.Enums;
|
|
||||||
|
|
||||||
public enum TerritoryTypeIdHousing
|
|
||||||
{
|
|
||||||
None = -1,
|
|
||||||
|
|
||||||
// Mist (Limsa Lominsa)
|
|
||||||
Mist = 339,
|
|
||||||
MistSmall = 282,
|
|
||||||
MistMedium = 283,
|
|
||||||
MistLarge = 284,
|
|
||||||
MistFCRoom = 384,
|
|
||||||
MistFCWorkshop = 423,
|
|
||||||
MistApartment = 608,
|
|
||||||
|
|
||||||
// Lavender Beds (Gridania)
|
|
||||||
Lavender = 340,
|
|
||||||
LavenderSmall = 342,
|
|
||||||
LavenderMedium = 343,
|
|
||||||
LavenderLarge = 344,
|
|
||||||
LavenderFCRoom = 385,
|
|
||||||
LavenderFCWorkshop = 425,
|
|
||||||
LavenderApartment = 609,
|
|
||||||
|
|
||||||
// Goblet (Ul'dah)
|
|
||||||
Goblet = 341,
|
|
||||||
GobletSmall = 345,
|
|
||||||
GobletMedium = 346,
|
|
||||||
GobletLarge = 347,
|
|
||||||
GobletFCRoom = 386,
|
|
||||||
GobletFCWorkshop = 424,
|
|
||||||
GobletApartment = 610,
|
|
||||||
|
|
||||||
// Shirogane (Kugane)
|
|
||||||
Shirogane = 641,
|
|
||||||
ShiroganeSmall = 649,
|
|
||||||
ShiroganeMedium = 650,
|
|
||||||
ShiroganeLarge = 651,
|
|
||||||
ShiroganeFCRoom = 652,
|
|
||||||
ShiroganeFCWorkshop = 653,
|
|
||||||
ShiroganeApartment = 655,
|
|
||||||
|
|
||||||
// Empyreum (Ishgard)
|
|
||||||
Empyream = 979,
|
|
||||||
EmpyreamSmall = 980,
|
|
||||||
EmpyreamMedium = 981,
|
|
||||||
EmpyreamLarge = 982,
|
|
||||||
EmpyreamFCRoom = 983,
|
|
||||||
EmpyreamFCWorkshop = 984,
|
|
||||||
EmpyreamApartment = 999,
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
global using AddressBookEntryTuple = (string Name, int World, int City, int Ward, int PropertyType, int Plot, int Apartment, bool ApartmentSubdivision, bool AliasEnabled, string Alias);
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
using Dalamud.Plugin;
|
|
||||||
using Dalamud.Plugin.Ipc;
|
|
||||||
using Lifestream.Enums;
|
|
||||||
using LightlessSync.Interop.Ipc.Framework;
|
|
||||||
using LightlessSync.Services.Mediator;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
|
|
||||||
namespace LightlessSync.Interop.Ipc;
|
|
||||||
|
|
||||||
public sealed class IpcCallerLifestream : IpcServiceBase
|
|
||||||
{
|
|
||||||
private static readonly IpcServiceDescriptor LifestreamDescriptor = new("Lifestream", "Lifestream", new Version(0, 0, 0, 0));
|
|
||||||
|
|
||||||
private readonly ICallGateSubscriber<string, object> _executeLifestreamCommand;
|
|
||||||
private readonly ICallGateSubscriber<AddressBookEntryTuple, bool> _isHere;
|
|
||||||
private readonly ICallGateSubscriber<AddressBookEntryTuple, object> _goToHousingAddress;
|
|
||||||
private readonly ICallGateSubscriber<bool> _isBusy;
|
|
||||||
private readonly ICallGateSubscriber<object> _abort;
|
|
||||||
private readonly ICallGateSubscriber<string, bool> _changeWorld;
|
|
||||||
private readonly ICallGateSubscriber<uint, bool> _changeWorldById;
|
|
||||||
private readonly ICallGateSubscriber<string, bool> _aetheryteTeleport;
|
|
||||||
private readonly ICallGateSubscriber<uint, bool> _aetheryteTeleportById;
|
|
||||||
private readonly ICallGateSubscriber<bool> _canChangeInstance;
|
|
||||||
private readonly ICallGateSubscriber<int> _getCurrentInstance;
|
|
||||||
private readonly ICallGateSubscriber<int> _getNumberOfInstances;
|
|
||||||
private readonly ICallGateSubscriber<int, object> _changeInstance;
|
|
||||||
private readonly ICallGateSubscriber<(ResidentialAetheryteKind, int, int)> _getCurrentPlotInfo;
|
|
||||||
|
|
||||||
public IpcCallerLifestream(IDalamudPluginInterface pi, LightlessMediator lightlessMediator, ILogger<IpcCallerLifestream> logger)
|
|
||||||
: base(logger, lightlessMediator, pi, LifestreamDescriptor)
|
|
||||||
{
|
|
||||||
_executeLifestreamCommand = pi.GetIpcSubscriber<string, object>("Lifestream.ExecuteCommand");
|
|
||||||
_isHere = pi.GetIpcSubscriber<AddressBookEntryTuple, bool>("Lifestream.IsHere");
|
|
||||||
_goToHousingAddress = pi.GetIpcSubscriber<AddressBookEntryTuple, object>("Lifestream.GoToHousingAddress");
|
|
||||||
_isBusy = pi.GetIpcSubscriber<bool>("Lifestream.IsBusy");
|
|
||||||
_abort = pi.GetIpcSubscriber<object>("Lifestream.Abort");
|
|
||||||
_changeWorld = pi.GetIpcSubscriber<string, bool>("Lifestream.ChangeWorld");
|
|
||||||
_changeWorldById = pi.GetIpcSubscriber<uint, bool>("Lifestream.ChangeWorldById");
|
|
||||||
_aetheryteTeleport = pi.GetIpcSubscriber<string, bool>("Lifestream.AetheryteTeleport");
|
|
||||||
_aetheryteTeleportById = pi.GetIpcSubscriber<uint, bool>("Lifestream.AetheryteTeleportById");
|
|
||||||
_canChangeInstance = pi.GetIpcSubscriber<bool>("Lifestream.CanChangeInstance");
|
|
||||||
_getCurrentInstance = pi.GetIpcSubscriber<int>("Lifestream.GetCurrentInstance");
|
|
||||||
_getNumberOfInstances = pi.GetIpcSubscriber<int>("Lifestream.GetNumberOfInstances");
|
|
||||||
_changeInstance = pi.GetIpcSubscriber<int, object>("Lifestream.ChangeInstance");
|
|
||||||
_getCurrentPlotInfo = pi.GetIpcSubscriber<(ResidentialAetheryteKind, int, int)>("Lifestream.GetCurrentPlotInfo");
|
|
||||||
CheckAPI();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ExecuteLifestreamCommand(string command)
|
|
||||||
{
|
|
||||||
if (!APIAvailable) return;
|
|
||||||
_executeLifestreamCommand.InvokeAction(command);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsHere(AddressBookEntryTuple entry)
|
|
||||||
{
|
|
||||||
if (!APIAvailable) return false;
|
|
||||||
return _isHere.InvokeFunc(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void GoToHousingAddress(AddressBookEntryTuple entry)
|
|
||||||
{
|
|
||||||
if (!APIAvailable) return;
|
|
||||||
_goToHousingAddress.InvokeAction(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsBusy()
|
|
||||||
{
|
|
||||||
if (!APIAvailable) return false;
|
|
||||||
return _isBusy.InvokeFunc();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Abort()
|
|
||||||
{
|
|
||||||
if (!APIAvailable) return;
|
|
||||||
_abort.InvokeAction();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ChangeWorld(string worldName)
|
|
||||||
{
|
|
||||||
if (!APIAvailable) return false;
|
|
||||||
return _changeWorld.InvokeFunc(worldName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool AetheryteTeleport(string aetheryteName)
|
|
||||||
{
|
|
||||||
if (!APIAvailable) return false;
|
|
||||||
return _aetheryteTeleport.InvokeFunc(aetheryteName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ChangeWorldById(uint worldId)
|
|
||||||
{
|
|
||||||
if (!APIAvailable) return false;
|
|
||||||
return _changeWorldById.InvokeFunc(worldId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool AetheryteTeleportById(uint aetheryteId)
|
|
||||||
{
|
|
||||||
if (!APIAvailable) return false;
|
|
||||||
return _aetheryteTeleportById.InvokeFunc(aetheryteId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CanChangeInstance()
|
|
||||||
{
|
|
||||||
if (!APIAvailable) return false;
|
|
||||||
return _canChangeInstance.InvokeFunc();
|
|
||||||
}
|
|
||||||
public int GetCurrentInstance()
|
|
||||||
{
|
|
||||||
if (!APIAvailable) return -1;
|
|
||||||
return _getCurrentInstance.InvokeFunc();
|
|
||||||
}
|
|
||||||
public int GetNumberOfInstances()
|
|
||||||
{
|
|
||||||
if (!APIAvailable) return -1;
|
|
||||||
return _getNumberOfInstances.InvokeFunc();
|
|
||||||
}
|
|
||||||
public void ChangeInstance(int instanceNumber)
|
|
||||||
{
|
|
||||||
if (!APIAvailable) return;
|
|
||||||
_changeInstance.InvokeAction(instanceNumber);
|
|
||||||
}
|
|
||||||
public (ResidentialAetheryteKind, int, int)? GetCurrentPlotInfo()
|
|
||||||
{
|
|
||||||
if (!APIAvailable) return (ResidentialAetheryteKind.None, -1, -1);
|
|
||||||
return _getCurrentPlotInfo.InvokeFunc();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@ using LightlessSync.Interop.Ipc.Penumbra;
|
|||||||
using LightlessSync.LightlessConfiguration.Models;
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
|
using LightlessSync.Services.ActorTracking;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
@@ -35,7 +36,8 @@ public sealed class IpcCallerPenumbra : IpcServiceBase
|
|||||||
IDalamudPluginInterface pluginInterface,
|
IDalamudPluginInterface pluginInterface,
|
||||||
DalamudUtilService dalamudUtil,
|
DalamudUtilService dalamudUtil,
|
||||||
LightlessMediator mediator,
|
LightlessMediator mediator,
|
||||||
RedrawManager redrawManager) : base(logger, mediator, pluginInterface, PenumbraDescriptor)
|
RedrawManager redrawManager,
|
||||||
|
ActorObjectService actorObjectService) : base(logger, mediator, pluginInterface, PenumbraDescriptor)
|
||||||
{
|
{
|
||||||
_penumbraEnabled = new GetEnabledState(pluginInterface);
|
_penumbraEnabled = new GetEnabledState(pluginInterface);
|
||||||
_penumbraGetModDirectory = new GetModDirectory(pluginInterface);
|
_penumbraGetModDirectory = new GetModDirectory(pluginInterface);
|
||||||
@@ -44,7 +46,7 @@ public sealed class IpcCallerPenumbra : IpcServiceBase
|
|||||||
_penumbraModSettingChanged = ModSettingChanged.Subscriber(pluginInterface, HandlePenumbraModSettingChanged);
|
_penumbraModSettingChanged = ModSettingChanged.Subscriber(pluginInterface, HandlePenumbraModSettingChanged);
|
||||||
|
|
||||||
_collections = RegisterInterop(new PenumbraCollections(logger, pluginInterface, dalamudUtil, mediator));
|
_collections = RegisterInterop(new PenumbraCollections(logger, pluginInterface, dalamudUtil, mediator));
|
||||||
_resources = RegisterInterop(new PenumbraResource(logger, pluginInterface, dalamudUtil, mediator));
|
_resources = RegisterInterop(new PenumbraResource(logger, pluginInterface, dalamudUtil, mediator, actorObjectService));
|
||||||
_redraw = RegisterInterop(new PenumbraRedraw(logger, pluginInterface, dalamudUtil, mediator, redrawManager));
|
_redraw = RegisterInterop(new PenumbraRedraw(logger, pluginInterface, dalamudUtil, mediator, redrawManager));
|
||||||
_textures = RegisterInterop(new PenumbraTexture(logger, pluginInterface, dalamudUtil, mediator, _redraw));
|
_textures = RegisterInterop(new PenumbraTexture(logger, pluginInterface, dalamudUtil, mediator, _redraw));
|
||||||
|
|
||||||
@@ -93,20 +95,11 @@ public sealed class IpcCallerPenumbra : IpcServiceBase
|
|||||||
public Task<(string[] forward, string[][] reverse)> ResolvePathsAsync(string[] forward, string[] reverse)
|
public Task<(string[] forward, string[][] reverse)> ResolvePathsAsync(string[] forward, string[] reverse)
|
||||||
=> _resources.ResolvePathsAsync(forward, reverse);
|
=> _resources.ResolvePathsAsync(forward, reverse);
|
||||||
|
|
||||||
public string ResolveGameObjectPath(string gamePath, int objectIndex)
|
|
||||||
=> _resources.ResolveGameObjectPath(gamePath, objectIndex);
|
|
||||||
|
|
||||||
public string[] ReverseResolveGameObjectPath(string moddedPath, int objectIndex)
|
|
||||||
=> _resources.ReverseResolveGameObjectPath(moddedPath, objectIndex);
|
|
||||||
|
|
||||||
public Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token)
|
public Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token)
|
||||||
=> _redraw.RedrawAsync(logger, handler, applicationId, token);
|
=> _redraw.RedrawAsync(logger, handler, applicationId, token);
|
||||||
|
|
||||||
public void RequestImmediateRedraw(int objectIndex, RedrawType redrawType)
|
public Task ConvertTextureFiles(ILogger logger, IReadOnlyList<TextureConversionJob> jobs, IProgress<TextureConversionProgress>? progress, CancellationToken token)
|
||||||
=> _redraw.RequestImmediateRedraw(objectIndex, redrawType);
|
=> _textures.ConvertTextureFilesAsync(logger, jobs, progress, token);
|
||||||
|
|
||||||
public Task ConvertTextureFiles(ILogger logger, IReadOnlyList<TextureConversionJob> jobs, IProgress<TextureConversionProgress>? progress, CancellationToken token, bool requestRedraw = true)
|
|
||||||
=> _textures.ConvertTextureFilesAsync(logger, jobs, progress, token, requestRedraw);
|
|
||||||
|
|
||||||
public Task ConvertTextureFileDirectAsync(TextureConversionJob job, CancellationToken token)
|
public Task ConvertTextureFileDirectAsync(TextureConversionJob job, CancellationToken token)
|
||||||
=> _textures.ConvertTextureFileDirectAsync(job, token);
|
=> _textures.ConvertTextureFileDirectAsync(job, token);
|
||||||
@@ -178,6 +171,11 @@ public sealed class IpcCallerPenumbra : IpcServiceBase
|
|||||||
});
|
});
|
||||||
|
|
||||||
Mediator.Subscribe<DalamudLoginMessage>(this, _ => _shownPenumbraUnavailable = false);
|
Mediator.Subscribe<DalamudLoginMessage>(this, _ => _shownPenumbraUnavailable = false);
|
||||||
|
|
||||||
|
Mediator.Subscribe<ActorTrackedMessage>(this, msg => _resources.TrackActor(msg.Descriptor.Address));
|
||||||
|
Mediator.Subscribe<ActorUntrackedMessage>(this, msg => _resources.UntrackActor(msg.Descriptor.Address));
|
||||||
|
Mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, msg => _resources.TrackActor(msg.GameObjectHandler.Address));
|
||||||
|
Mediator.Subscribe<GameObjectHandlerDestroyedMessage>(this, msg => _resources.UntrackActor(msg.GameObjectHandler.Address));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandlePenumbraInitialized()
|
private void HandlePenumbraInitialized()
|
||||||
|
|||||||
@@ -5,12 +5,9 @@ namespace LightlessSync.Interop.Ipc;
|
|||||||
|
|
||||||
public sealed partial class IpcManager : DisposableMediatorSubscriberBase
|
public sealed partial class IpcManager : DisposableMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
private bool _wasInitialized;
|
|
||||||
|
|
||||||
public IpcManager(ILogger<IpcManager> logger, LightlessMediator mediator,
|
public IpcManager(ILogger<IpcManager> logger, LightlessMediator mediator,
|
||||||
IpcCallerPenumbra penumbraIpc, IpcCallerGlamourer glamourerIpc, IpcCallerCustomize customizeIpc, IpcCallerHeels heelsIpc,
|
IpcCallerPenumbra penumbraIpc, IpcCallerGlamourer glamourerIpc, IpcCallerCustomize customizeIpc, IpcCallerHeels heelsIpc,
|
||||||
IpcCallerHonorific honorificIpc, IpcCallerMoodles moodlesIpc, IpcCallerPetNames ipcCallerPetNames, IpcCallerBrio ipcCallerBrio,
|
IpcCallerHonorific honorificIpc, IpcCallerMoodles moodlesIpc, IpcCallerPetNames ipcCallerPetNames, IpcCallerBrio ipcCallerBrio) : base(logger, mediator)
|
||||||
IpcCallerLifestream ipcCallerLifestream) : base(logger, mediator)
|
|
||||||
{
|
{
|
||||||
CustomizePlus = customizeIpc;
|
CustomizePlus = customizeIpc;
|
||||||
Heels = heelsIpc;
|
Heels = heelsIpc;
|
||||||
@@ -20,10 +17,8 @@ public sealed partial class IpcManager : DisposableMediatorSubscriberBase
|
|||||||
Moodles = moodlesIpc;
|
Moodles = moodlesIpc;
|
||||||
PetNames = ipcCallerPetNames;
|
PetNames = ipcCallerPetNames;
|
||||||
Brio = ipcCallerBrio;
|
Brio = ipcCallerBrio;
|
||||||
Lifestream = ipcCallerLifestream;
|
|
||||||
|
|
||||||
_wasInitialized = Initialized;
|
if (Initialized)
|
||||||
if (_wasInitialized)
|
|
||||||
{
|
{
|
||||||
Mediator.Publish(new PenumbraInitializedMessage());
|
Mediator.Publish(new PenumbraInitializedMessage());
|
||||||
}
|
}
|
||||||
@@ -49,8 +44,8 @@ public sealed partial class IpcManager : DisposableMediatorSubscriberBase
|
|||||||
public IpcCallerPenumbra Penumbra { get; }
|
public IpcCallerPenumbra Penumbra { get; }
|
||||||
public IpcCallerMoodles Moodles { get; }
|
public IpcCallerMoodles Moodles { get; }
|
||||||
public IpcCallerPetNames PetNames { get; }
|
public IpcCallerPetNames PetNames { get; }
|
||||||
|
|
||||||
public IpcCallerBrio Brio { get; }
|
public IpcCallerBrio Brio { get; }
|
||||||
public IpcCallerLifestream Lifestream { get; }
|
|
||||||
|
|
||||||
private void PeriodicApiStateCheck()
|
private void PeriodicApiStateCheck()
|
||||||
{
|
{
|
||||||
@@ -63,14 +58,5 @@ public sealed partial class IpcManager : DisposableMediatorSubscriberBase
|
|||||||
Moodles.CheckAPI();
|
Moodles.CheckAPI();
|
||||||
PetNames.CheckAPI();
|
PetNames.CheckAPI();
|
||||||
Brio.CheckAPI();
|
Brio.CheckAPI();
|
||||||
|
|
||||||
var initialized = Initialized;
|
|
||||||
if (initialized && !_wasInitialized)
|
|
||||||
{
|
|
||||||
Mediator.Publish(new PenumbraInitializedMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
_wasInitialized = initialized;
|
|
||||||
Lifestream.CheckAPI();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using LightlessSync.Interop.Ipc.Framework;
|
using LightlessSync.Interop.Ipc.Framework;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Penumbra.Api.Enums;
|
||||||
using Penumbra.Api.IpcSubscribers;
|
using Penumbra.Api.IpcSubscribers;
|
||||||
|
|
||||||
namespace LightlessSync.Interop.Ipc.Penumbra;
|
namespace LightlessSync.Interop.Ipc.Penumbra;
|
||||||
@@ -14,6 +16,10 @@ public sealed class PenumbraCollections : PenumbraBase
|
|||||||
private readonly DeleteTemporaryCollection _removeTemporaryCollection;
|
private readonly DeleteTemporaryCollection _removeTemporaryCollection;
|
||||||
private readonly AddTemporaryMod _addTemporaryMod;
|
private readonly AddTemporaryMod _addTemporaryMod;
|
||||||
private readonly RemoveTemporaryMod _removeTemporaryMod;
|
private readonly RemoveTemporaryMod _removeTemporaryMod;
|
||||||
|
private readonly GetCollections _getCollections;
|
||||||
|
private readonly ConcurrentDictionary<Guid, string> _activeTemporaryCollections = new();
|
||||||
|
|
||||||
|
private int _cleanupScheduled;
|
||||||
|
|
||||||
public PenumbraCollections(
|
public PenumbraCollections(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
@@ -26,6 +32,7 @@ public sealed class PenumbraCollections : PenumbraBase
|
|||||||
_removeTemporaryCollection = new DeleteTemporaryCollection(pluginInterface);
|
_removeTemporaryCollection = new DeleteTemporaryCollection(pluginInterface);
|
||||||
_addTemporaryMod = new AddTemporaryMod(pluginInterface);
|
_addTemporaryMod = new AddTemporaryMod(pluginInterface);
|
||||||
_removeTemporaryMod = new RemoveTemporaryMod(pluginInterface);
|
_removeTemporaryMod = new RemoveTemporaryMod(pluginInterface);
|
||||||
|
_getCollections = new GetCollections(pluginInterface);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string Name => "Penumbra.Collections";
|
public override string Name => "Penumbra.Collections";
|
||||||
@@ -55,11 +62,16 @@ public sealed class PenumbraCollections : PenumbraBase
|
|||||||
var (collectionId, collectionName) = await DalamudUtil.RunOnFrameworkThread(() =>
|
var (collectionId, collectionName) = await DalamudUtil.RunOnFrameworkThread(() =>
|
||||||
{
|
{
|
||||||
var name = $"Lightless_{uid}";
|
var name = $"Lightless_{uid}";
|
||||||
var createResult = _createNamedTemporaryCollection.Invoke(name, name, out var tempCollectionId);
|
_createNamedTemporaryCollection.Invoke(name, name, out var tempCollectionId);
|
||||||
logger.LogTrace("Creating Temp Collection {CollectionName}, GUID: {CollectionId}, Result: {Result}", name, tempCollectionId, createResult);
|
logger.LogTrace("Creating Temp Collection {CollectionName}, GUID: {CollectionId}", name, tempCollectionId);
|
||||||
return (tempCollectionId, name);
|
return (tempCollectionId, name);
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (collectionId != Guid.Empty)
|
||||||
|
{
|
||||||
|
_activeTemporaryCollections[collectionId] = collectionName;
|
||||||
|
}
|
||||||
|
|
||||||
return collectionId;
|
return collectionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,9 +89,10 @@ public sealed class PenumbraCollections : PenumbraBase
|
|||||||
logger.LogTrace("[{ApplicationId}] RemoveTemporaryCollection: {Result}", applicationId, result);
|
logger.LogTrace("[{ApplicationId}] RemoveTemporaryCollection: {Result}", applicationId, result);
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_activeTemporaryCollections.TryRemove(collectionId, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, Dictionary<string, string> modPaths)
|
public async Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, IReadOnlyDictionary<string, string> modPaths)
|
||||||
{
|
{
|
||||||
if (!IsAvailable || collectionId == Guid.Empty)
|
if (!IsAvailable || collectionId == Guid.Empty)
|
||||||
{
|
{
|
||||||
@@ -96,7 +109,7 @@ public sealed class PenumbraCollections : PenumbraBase
|
|||||||
var removeResult = _removeTemporaryMod.Invoke("LightlessChara_Files", collectionId, 0);
|
var removeResult = _removeTemporaryMod.Invoke("LightlessChara_Files", collectionId, 0);
|
||||||
logger.LogTrace("[{ApplicationId}] Removing temp files mod for {CollectionId}, Success: {Result}", applicationId, collectionId, removeResult);
|
logger.LogTrace("[{ApplicationId}] Removing temp files mod for {CollectionId}, Success: {Result}", applicationId, collectionId, removeResult);
|
||||||
|
|
||||||
var addResult = _addTemporaryMod.Invoke("LightlessChara_Files", collectionId, modPaths, string.Empty, 0);
|
var addResult = _addTemporaryMod.Invoke("LightlessChara_Files", collectionId, new Dictionary<string, string>(modPaths), string.Empty, 0);
|
||||||
logger.LogTrace("[{ApplicationId}] Setting temp files mod for {CollectionId}, Success: {Result}", applicationId, collectionId, addResult);
|
logger.LogTrace("[{ApplicationId}] Setting temp files mod for {CollectionId}, Success: {Result}", applicationId, collectionId, addResult);
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -118,5 +131,67 @@ public sealed class PenumbraCollections : PenumbraBase
|
|||||||
|
|
||||||
protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current)
|
protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current)
|
||||||
{
|
{
|
||||||
|
if (current == IpcConnectionState.Available)
|
||||||
|
{
|
||||||
|
ScheduleCleanup();
|
||||||
|
}
|
||||||
|
else if (previous == IpcConnectionState.Available && current != IpcConnectionState.Available)
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _cleanupScheduled, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ScheduleCleanup()
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _cleanupScheduled, 1) != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = Task.Run(CleanupTemporaryCollectionsAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CleanupTemporaryCollectionsAsync()
|
||||||
|
{
|
||||||
|
if (!IsAvailable)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var collections = await DalamudUtil.RunOnFrameworkThread(() => _getCollections.Invoke()).ConfigureAwait(false);
|
||||||
|
foreach (var (collectionId, name) in collections)
|
||||||
|
{
|
||||||
|
if (!IsLightlessCollectionName(name) || _activeTemporaryCollections.ContainsKey(collectionId))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogDebug("Cleaning up stale temporary collection {CollectionName} ({CollectionId})", name, collectionId);
|
||||||
|
var deleteResult = await DalamudUtil.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
var result = (PenumbraApiEc)_removeTemporaryCollection.Invoke(collectionId);
|
||||||
|
Logger.LogTrace("Cleanup RemoveTemporaryCollection result for {CollectionName} ({CollectionId}): {Result}", name, collectionId, result);
|
||||||
|
return result;
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (deleteResult == PenumbraApiEc.Success)
|
||||||
|
{
|
||||||
|
_activeTemporaryCollections.TryRemove(collectionId, out _);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Skipped removing temporary collection {CollectionName} ({CollectionId}). Result: {Result}", name, collectionId, deleteResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Failed to clean up Penumbra temporary collections");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsLightlessCollectionName(string? name)
|
||||||
|
=> !string.IsNullOrEmpty(name) && name.StartsWith("Lightless_", StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using LightlessSync.Interop.Ipc.Framework;
|
using LightlessSync.Interop.Ipc.Framework;
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
|
using LightlessSync.Services.ActorTracking;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Globalization;
|
|
||||||
using Penumbra.Api.Helpers;
|
using Penumbra.Api.Helpers;
|
||||||
using Penumbra.Api.IpcSubscribers;
|
using Penumbra.Api.IpcSubscribers;
|
||||||
|
|
||||||
@@ -13,25 +13,30 @@ namespace LightlessSync.Interop.Ipc.Penumbra;
|
|||||||
|
|
||||||
public sealed class PenumbraResource : PenumbraBase
|
public sealed class PenumbraResource : PenumbraBase
|
||||||
{
|
{
|
||||||
|
private readonly ActorObjectService _actorObjectService;
|
||||||
private readonly GetGameObjectResourcePaths _gameObjectResourcePaths;
|
private readonly GetGameObjectResourcePaths _gameObjectResourcePaths;
|
||||||
private readonly ResolveGameObjectPath _resolveGameObjectPath;
|
|
||||||
private readonly ReverseResolveGameObjectPath _reverseResolveGameObjectPath;
|
|
||||||
private readonly ResolvePlayerPathsAsync _resolvePlayerPaths;
|
private readonly ResolvePlayerPathsAsync _resolvePlayerPaths;
|
||||||
private readonly GetPlayerMetaManipulations _getPlayerMetaManipulations;
|
private readonly GetPlayerMetaManipulations _getPlayerMetaManipulations;
|
||||||
private readonly EventSubscriber<nint, string, string> _gameObjectResourcePathResolved;
|
private readonly EventSubscriber<nint, string, string> _gameObjectResourcePathResolved;
|
||||||
|
private readonly ConcurrentDictionary<IntPtr, byte> _trackedActors = new();
|
||||||
|
|
||||||
public PenumbraResource(
|
public PenumbraResource(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
IDalamudPluginInterface pluginInterface,
|
IDalamudPluginInterface pluginInterface,
|
||||||
DalamudUtilService dalamudUtil,
|
DalamudUtilService dalamudUtil,
|
||||||
LightlessMediator mediator) : base(logger, pluginInterface, dalamudUtil, mediator)
|
LightlessMediator mediator,
|
||||||
|
ActorObjectService actorObjectService) : base(logger, pluginInterface, dalamudUtil, mediator)
|
||||||
{
|
{
|
||||||
|
_actorObjectService = actorObjectService;
|
||||||
_gameObjectResourcePaths = new GetGameObjectResourcePaths(pluginInterface);
|
_gameObjectResourcePaths = new GetGameObjectResourcePaths(pluginInterface);
|
||||||
_resolveGameObjectPath = new ResolveGameObjectPath(pluginInterface);
|
|
||||||
_reverseResolveGameObjectPath = new ReverseResolveGameObjectPath(pluginInterface);
|
|
||||||
_resolvePlayerPaths = new ResolvePlayerPathsAsync(pluginInterface);
|
_resolvePlayerPaths = new ResolvePlayerPathsAsync(pluginInterface);
|
||||||
_getPlayerMetaManipulations = new GetPlayerMetaManipulations(pluginInterface);
|
_getPlayerMetaManipulations = new GetPlayerMetaManipulations(pluginInterface);
|
||||||
_gameObjectResourcePathResolved = GameObjectResourcePathResolved.Subscriber(pluginInterface, HandleResourceLoaded);
|
_gameObjectResourcePathResolved = GameObjectResourcePathResolved.Subscriber(pluginInterface, HandleResourceLoaded);
|
||||||
|
|
||||||
|
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
|
||||||
|
{
|
||||||
|
TrackActor(descriptor.Address);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string Name => "Penumbra.Resources";
|
public override string Name => "Penumbra.Resources";
|
||||||
@@ -43,33 +48,17 @@ public sealed class PenumbraResource : PenumbraBase
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var requestId = Guid.NewGuid();
|
return await DalamudUtil.RunOnFrameworkThread(() =>
|
||||||
var totalTimer = Stopwatch.StartNew();
|
|
||||||
logger.LogTrace("[{requestId}] Requesting Penumbra.GetGameObjectResourcePaths for {handler}", requestId, handler);
|
|
||||||
|
|
||||||
var result = await DalamudUtil.RunOnFrameworkThread(() =>
|
|
||||||
{
|
{
|
||||||
|
logger.LogTrace("Calling On IPC: Penumbra.GetGameObjectResourcePaths");
|
||||||
var idx = handler.GetGameObject()?.ObjectIndex;
|
var idx = handler.GetGameObject()?.ObjectIndex;
|
||||||
if (idx == null)
|
if (idx == null)
|
||||||
{
|
{
|
||||||
logger.LogTrace("[{requestId}] GetGameObjectResourcePaths aborted (missing object index) for {handler}", requestId, handler);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogTrace("[{requestId}] Invoking Penumbra.GetGameObjectResourcePaths for index {index}", requestId, idx.Value);
|
return _gameObjectResourcePaths.Invoke(idx.Value)[0];
|
||||||
var invokeTimer = Stopwatch.StartNew();
|
|
||||||
var data = _gameObjectResourcePaths.Invoke(idx.Value)[0];
|
|
||||||
invokeTimer.Stop();
|
|
||||||
logger.LogTrace("[{requestId}] Penumbra.GetGameObjectResourcePaths returned {count} entries in {elapsedMs}ms",
|
|
||||||
requestId, data?.Count ?? 0, invokeTimer.ElapsedMilliseconds);
|
|
||||||
return data;
|
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
totalTimer.Stop();
|
|
||||||
logger.LogTrace("[{requestId}] Penumbra.GetGameObjectResourcePaths finished in {elapsedMs}ms (null: {isNull})",
|
|
||||||
requestId, totalTimer.ElapsedMilliseconds, result is null);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetMetaManipulations()
|
public string GetMetaManipulations()
|
||||||
@@ -85,22 +74,63 @@ public sealed class PenumbraResource : PenumbraBase
|
|||||||
return await _resolvePlayerPaths.Invoke(forwardPaths, reversePaths).ConfigureAwait(false);
|
return await _resolvePlayerPaths.Invoke(forwardPaths, reversePaths).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ResolveGameObjectPath(string gamePath, int gameObjectIndex)
|
public void TrackActor(nint address)
|
||||||
=> IsAvailable ? _resolveGameObjectPath.Invoke(gamePath, gameObjectIndex) : gamePath;
|
|
||||||
|
|
||||||
public string[] ReverseResolveGameObjectPath(string moddedPath, int gameObjectIndex)
|
|
||||||
=> IsAvailable ? _reverseResolveGameObjectPath.Invoke(moddedPath, gameObjectIndex) : Array.Empty<string>();
|
|
||||||
|
|
||||||
private void HandleResourceLoaded(nint ptr, string gamePath, string resolvedPath)
|
|
||||||
{
|
{
|
||||||
if (ptr != nint.Zero && string.Compare(gamePath, resolvedPath, ignoreCase: true, CultureInfo.InvariantCulture) != 0)
|
if (address != nint.Zero)
|
||||||
{
|
{
|
||||||
Mediator.Publish(new PenumbraResourceLoadMessage(ptr, gamePath, resolvedPath));
|
_trackedActors[(IntPtr)address] = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void UntrackActor(nint address)
|
||||||
|
{
|
||||||
|
if (address != nint.Zero)
|
||||||
|
{
|
||||||
|
_trackedActors.TryRemove((IntPtr)address, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleResourceLoaded(nint ptr, string resolvedPath, string gamePath)
|
||||||
|
{
|
||||||
|
if (ptr == nint.Zero)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_trackedActors.ContainsKey(ptr))
|
||||||
|
{
|
||||||
|
var descriptor = _actorObjectService.PlayerDescriptors.FirstOrDefault(d => d.Address == ptr);
|
||||||
|
if (descriptor.Address != nint.Zero)
|
||||||
|
{
|
||||||
|
_trackedActors[ptr] = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Compare(resolvedPath, gamePath, StringComparison.OrdinalIgnoreCase) == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Mediator.Publish(new PenumbraResourceLoadMessage(ptr, resolvedPath, gamePath));
|
||||||
|
}
|
||||||
|
|
||||||
protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current)
|
protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current)
|
||||||
{
|
{
|
||||||
|
if (current != IpcConnectionState.Available)
|
||||||
|
{
|
||||||
|
_trackedActors.Clear();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
|
||||||
|
{
|
||||||
|
TrackActor(descriptor.Address);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Dispose()
|
public override void Dispose()
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public sealed class PenumbraTexture : PenumbraBase
|
|||||||
|
|
||||||
public override string Name => "Penumbra.Textures";
|
public override string Name => "Penumbra.Textures";
|
||||||
|
|
||||||
public async Task ConvertTextureFilesAsync(ILogger logger, IReadOnlyList<TextureConversionJob> jobs, IProgress<TextureConversionProgress>? progress, CancellationToken token, bool requestRedraw)
|
public async Task ConvertTextureFilesAsync(ILogger logger, IReadOnlyList<TextureConversionJob> jobs, IProgress<TextureConversionProgress>? progress, CancellationToken token)
|
||||||
{
|
{
|
||||||
if (!IsAvailable || jobs.Count == 0)
|
if (!IsAvailable || jobs.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -57,7 +57,7 @@ public sealed class PenumbraTexture : PenumbraBase
|
|||||||
Mediator.Publish(new ResumeScanMessage(nameof(ConvertTextureFilesAsync)));
|
Mediator.Publish(new ResumeScanMessage(nameof(ConvertTextureFilesAsync)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestRedraw && completedJobs > 0 && !token.IsCancellationRequested)
|
if (completedJobs > 0 && !token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
await DalamudUtil.RunOnFrameworkThread(async () =>
|
await DalamudUtil.RunOnFrameworkThread(async () =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
using System.Globalization;
|
using LightlessSync.WebAPI;
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Nodes;
|
|
||||||
using LightlessSync.LightlessConfiguration.Configurations;
|
|
||||||
using LightlessSync.LightlessConfiguration.Models;
|
|
||||||
using LightlessSync.WebAPI;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace LightlessSync.LightlessConfiguration;
|
namespace LightlessSync.LightlessConfiguration;
|
||||||
|
|
||||||
public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, TransientConfigService transientConfigService,
|
public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, TransientConfigService transientConfigService,
|
||||||
ServerConfigService serverConfigService, TempCollectionConfigService tempCollectionConfigService,
|
ServerConfigService serverConfigService) : IHostedService
|
||||||
LightlessConfigService lightlessConfigService) : IHostedService
|
|
||||||
{
|
{
|
||||||
private readonly ILogger<ConfigurationMigrator> _logger = logger;
|
private readonly ILogger<ConfigurationMigrator> _logger = logger;
|
||||||
|
|
||||||
@@ -25,27 +19,6 @@ public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, Transi
|
|||||||
transientConfigService.Save();
|
transientConfigService.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transientConfigService.Current.Version == 1)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Migrating Transient Config V1 => V2");
|
|
||||||
var totalRemoved = 0;
|
|
||||||
var configCount = 0;
|
|
||||||
var changedCount = 0;
|
|
||||||
foreach (var config in transientConfigService.Current.TransientConfigs.Values)
|
|
||||||
{
|
|
||||||
if (config.NormalizePaths(out var removed))
|
|
||||||
changedCount++;
|
|
||||||
|
|
||||||
totalRemoved += removed;
|
|
||||||
|
|
||||||
configCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Transient config normalization: processed {count} entries, updated {updated}, removed {removed} paths", configCount, changedCount, totalRemoved);
|
|
||||||
transientConfigService.Current.Version = 2;
|
|
||||||
transientConfigService.Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serverConfigService.Current.Version == 1)
|
if (serverConfigService.Current.Version == 1)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Migrating Server Config V1 => V2");
|
_logger.LogInformation("Migrating Server Config V1 => V2");
|
||||||
@@ -57,8 +30,6 @@ public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, Transi
|
|||||||
serverConfigService.Current.Version = 2;
|
serverConfigService.Current.Version = 2;
|
||||||
serverConfigService.Save();
|
serverConfigService.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
MigrateTempCollectionConfig(tempCollectionConfigService, lightlessConfigService);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
@@ -71,273 +42,4 @@ public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, Transi
|
|||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MigrateTempCollectionConfig(TempCollectionConfigService tempCollectionConfigService, LightlessConfigService lightlessConfigService)
|
|
||||||
{
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
TempCollectionConfig tempConfig = tempCollectionConfigService.Current;
|
|
||||||
var tempChanged = false;
|
|
||||||
var tempNeedsSave = false;
|
|
||||||
|
|
||||||
if (TryReadTempCollectionData(lightlessConfigService.ConfigurationPath, out var root, out var ids, out var entries))
|
|
||||||
{
|
|
||||||
tempChanged |= MergeTempCollectionData(tempConfig, ids, entries, now);
|
|
||||||
var removed = root.Remove("OrphanableTempCollections");
|
|
||||||
removed |= root.Remove("OrphanableTempCollectionEntries");
|
|
||||||
if (removed)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string updatedJson = root.ToJsonString(new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
WriteIndented = true
|
|
||||||
});
|
|
||||||
File.WriteAllText(lightlessConfigService.ConfigurationPath, updatedJson);
|
|
||||||
lightlessConfigService.UpdateLastWriteTime();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Failed to rewrite {config} after temp collection migration", lightlessConfigService.ConfigurationPath);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ids.Count > 0 || entries.Count > 0)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Migrated {ids} temp collection ids and {entries} entries to {configName}",
|
|
||||||
ids.Count, entries.Count, tempCollectionConfigService.ConfigurationName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TryReadTempCollectionData(tempCollectionConfigService.ConfigurationPath, out var tempRoot, out var tempIds, out var tempEntries))
|
|
||||||
{
|
|
||||||
tempChanged |= MergeTempCollectionData(tempConfig, tempIds, tempEntries, now);
|
|
||||||
if (tempRoot.Remove("OrphanableTempCollections"))
|
|
||||||
{
|
|
||||||
tempNeedsSave = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tempChanged || tempNeedsSave)
|
|
||||||
{
|
|
||||||
tempCollectionConfigService.Save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryReadTempCollectionData(string configPath, out JsonObject root, out HashSet<Guid> ids, out List<OrphanableTempCollectionEntry> entries)
|
|
||||||
{
|
|
||||||
root = new JsonObject();
|
|
||||||
ids = [];
|
|
||||||
entries = [];
|
|
||||||
|
|
||||||
if (!File.Exists(configPath))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
root = JsonNode.Parse(File.ReadAllText(configPath)) as JsonObject ?? new JsonObject();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Failed to read temp collection data from {config}", configPath);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
root.TryGetPropertyValue("OrphanableTempCollections", out JsonNode? idsNode);
|
|
||||||
root.TryGetPropertyValue("OrphanableTempCollectionEntries", out JsonNode? entriesNode);
|
|
||||||
|
|
||||||
if (idsNode == null && entriesNode == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ids = ParseGuidSet(idsNode);
|
|
||||||
entries = ParseEntries(entriesNode);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static HashSet<Guid> ParseGuidSet(JsonNode? node)
|
|
||||||
{
|
|
||||||
HashSet<Guid> ids = [];
|
|
||||||
if (node is not JsonArray array)
|
|
||||||
{
|
|
||||||
return ids;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (JsonNode? item in array)
|
|
||||||
{
|
|
||||||
Guid id = ParseGuid(item);
|
|
||||||
if (id != Guid.Empty)
|
|
||||||
{
|
|
||||||
ids.Add(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ids;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<OrphanableTempCollectionEntry> ParseEntries(JsonNode? node)
|
|
||||||
{
|
|
||||||
List<OrphanableTempCollectionEntry> entries = [];
|
|
||||||
if (node is not JsonArray array)
|
|
||||||
{
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (JsonNode? item in array)
|
|
||||||
{
|
|
||||||
if (item is not JsonObject obj)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Guid id = ParseGuid(obj["Id"]);
|
|
||||||
if (id == Guid.Empty)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTime registeredAtUtc = DateTime.MinValue;
|
|
||||||
if (TryParseDateTime(obj["RegisteredAtUtc"], out DateTime parsed))
|
|
||||||
{
|
|
||||||
registeredAtUtc = parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
entries.Add(new OrphanableTempCollectionEntry
|
|
||||||
{
|
|
||||||
Id = id,
|
|
||||||
RegisteredAtUtc = registeredAtUtc
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Guid ParseGuid(JsonNode? node)
|
|
||||||
{
|
|
||||||
if (node is JsonValue value)
|
|
||||||
{
|
|
||||||
if (value.TryGetValue<string>(out string? stringValue) && Guid.TryParse(stringValue, out Guid parsed))
|
|
||||||
{
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Guid.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryParseDateTime(JsonNode? node, out DateTime value)
|
|
||||||
{
|
|
||||||
value = DateTime.MinValue;
|
|
||||||
if (node is not JsonValue val)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (val.TryGetValue<DateTime>(out DateTime direct))
|
|
||||||
{
|
|
||||||
value = direct;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (val.TryGetValue<string>(out string? stringValue)
|
|
||||||
&& DateTime.TryParse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTime parsed))
|
|
||||||
{
|
|
||||||
value = parsed;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool MergeTempCollectionData(TempCollectionConfig config, HashSet<Guid> ids, List<OrphanableTempCollectionEntry> entries, DateTime now)
|
|
||||||
{
|
|
||||||
bool changed = false;
|
|
||||||
Dictionary<Guid, OrphanableTempCollectionEntry> entryLookup = new();
|
|
||||||
for (var i = config.OrphanableTempCollectionEntries.Count - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
var entry = config.OrphanableTempCollectionEntries[i];
|
|
||||||
if (entry.Id == Guid.Empty)
|
|
||||||
{
|
|
||||||
config.OrphanableTempCollectionEntries.RemoveAt(i);
|
|
||||||
changed = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entryLookup.TryGetValue(entry.Id, out var existing))
|
|
||||||
{
|
|
||||||
if (entry.RegisteredAtUtc != DateTime.MinValue
|
|
||||||
&& (existing.RegisteredAtUtc == DateTime.MinValue || entry.RegisteredAtUtc < existing.RegisteredAtUtc))
|
|
||||||
{
|
|
||||||
existing.RegisteredAtUtc = entry.RegisteredAtUtc;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
config.OrphanableTempCollectionEntries.RemoveAt(i);
|
|
||||||
changed = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
entryLookup[entry.Id] = entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (OrphanableTempCollectionEntry entry in entries)
|
|
||||||
{
|
|
||||||
if (entry.Id == Guid.Empty)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!entryLookup.TryGetValue(entry.Id, out OrphanableTempCollectionEntry? existing))
|
|
||||||
{
|
|
||||||
var added = new OrphanableTempCollectionEntry
|
|
||||||
{
|
|
||||||
Id = entry.Id,
|
|
||||||
RegisteredAtUtc = entry.RegisteredAtUtc
|
|
||||||
};
|
|
||||||
config.OrphanableTempCollectionEntries.Add(added);
|
|
||||||
entryLookup[entry.Id] = added;
|
|
||||||
changed = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.RegisteredAtUtc != DateTime.MinValue
|
|
||||||
&& (existing.RegisteredAtUtc == DateTime.MinValue || entry.RegisteredAtUtc < existing.RegisteredAtUtc))
|
|
||||||
{
|
|
||||||
existing.RegisteredAtUtc = entry.RegisteredAtUtc;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (Guid id in ids)
|
|
||||||
{
|
|
||||||
if (id == Guid.Empty)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!entryLookup.TryGetValue(id, out OrphanableTempCollectionEntry? existing))
|
|
||||||
{
|
|
||||||
var added = new OrphanableTempCollectionEntry
|
|
||||||
{
|
|
||||||
Id = id,
|
|
||||||
RegisteredAtUtc = now
|
|
||||||
};
|
|
||||||
config.OrphanableTempCollectionEntries.Add(added);
|
|
||||||
entryLookup[id] = added;
|
|
||||||
changed = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existing.RegisteredAtUtc == DateTime.MinValue)
|
|
||||||
{
|
|
||||||
existing.RegisteredAtUtc = now;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,41 +72,37 @@ public class ConfigurationSaveService : IHostedService
|
|||||||
{
|
{
|
||||||
_logger.LogTrace("Saving {configName}", config.ConfigurationName);
|
_logger.LogTrace("Saving {configName}", config.ConfigurationName);
|
||||||
var configDir = config.ConfigurationPath.Replace(config.ConfigurationName, string.Empty);
|
var configDir = config.ConfigurationPath.Replace(config.ConfigurationName, string.Empty);
|
||||||
var isTempCollections = string.Equals(config.ConfigurationName, TempCollectionConfigService.ConfigName, StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
if (!isTempCollections)
|
try
|
||||||
{
|
{
|
||||||
try
|
var configBackupFolder = Path.Join(configDir, BackupFolder);
|
||||||
{
|
if (!Directory.Exists(configBackupFolder))
|
||||||
var configBackupFolder = Path.Join(configDir, BackupFolder);
|
Directory.CreateDirectory(configBackupFolder);
|
||||||
if (!Directory.Exists(configBackupFolder))
|
|
||||||
Directory.CreateDirectory(configBackupFolder);
|
|
||||||
|
|
||||||
var configNameSplit = config.ConfigurationName.Split(".");
|
var configNameSplit = config.ConfigurationName.Split(".");
|
||||||
var existingConfigs = Directory.EnumerateFiles(
|
var existingConfigs = Directory.EnumerateFiles(
|
||||||
configBackupFolder,
|
configBackupFolder,
|
||||||
configNameSplit[0] + "*")
|
configNameSplit[0] + "*")
|
||||||
.Select(c => new FileInfo(c))
|
.Select(c => new FileInfo(c))
|
||||||
.OrderByDescending(c => c.LastWriteTime).ToList();
|
.OrderByDescending(c => c.LastWriteTime).ToList();
|
||||||
if (existingConfigs.Skip(10).Any())
|
if (existingConfigs.Skip(10).Any())
|
||||||
|
{
|
||||||
|
foreach (var oldBak in existingConfigs.Skip(10).ToList())
|
||||||
{
|
{
|
||||||
foreach (var oldBak in existingConfigs.Skip(10).ToList())
|
oldBak.Delete();
|
||||||
{
|
|
||||||
oldBak.Delete();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
string backupPath = Path.Combine(configBackupFolder, configNameSplit[0] + "." + DateTime.Now.ToString("yyyyMMddHHmmss") + "." + configNameSplit[1]);
|
string backupPath = Path.Combine(configBackupFolder, configNameSplit[0] + "." + DateTime.Now.ToString("yyyyMMddHHmmss") + "." + configNameSplit[1]);
|
||||||
_logger.LogTrace("Backing up current config to {backupPath}", backupPath);
|
_logger.LogTrace("Backing up current config to {backupPath}", backupPath);
|
||||||
File.Copy(config.ConfigurationPath, backupPath, overwrite: true);
|
File.Copy(config.ConfigurationPath, backupPath, overwrite: true);
|
||||||
FileInfo fi = new(backupPath);
|
FileInfo fi = new(backupPath);
|
||||||
fi.LastWriteTimeUtc = DateTime.UtcNow;
|
fi.LastWriteTimeUtc = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// ignore if file cannot be backupped
|
// ignore if file cannot be backupped
|
||||||
_logger.LogWarning(ex, "Could not create backup for {config}", config.ConfigurationPath);
|
_logger.LogWarning(ex, "Could not create backup for {config}", config.ConfigurationPath);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var temp = config.ConfigurationPath + ".tmp";
|
var temp = config.ConfigurationPath + ".tmp";
|
||||||
@@ -114,7 +110,7 @@ public class ConfigurationSaveService : IHostedService
|
|||||||
{
|
{
|
||||||
await File.WriteAllTextAsync(temp, JsonSerializer.Serialize(config.Current, typeof(T), new JsonSerializerOptions()
|
await File.WriteAllTextAsync(temp, JsonSerializer.Serialize(config.Current, typeof(T), new JsonSerializerOptions()
|
||||||
{
|
{
|
||||||
WriteIndented = !isTempCollections
|
WriteIndented = true
|
||||||
})).ConfigureAwait(false);
|
})).ConfigureAwait(false);
|
||||||
File.Move(temp, config.ConfigurationPath, true);
|
File.Move(temp, config.ConfigurationPath, true);
|
||||||
config.UpdateLastWriteTime();
|
config.UpdateLastWriteTime();
|
||||||
|
|||||||
@@ -10,11 +10,6 @@ public sealed class ChatConfig : ILightlessConfiguration
|
|||||||
public bool AutoEnableChatOnLogin { get; set; } = false;
|
public bool AutoEnableChatOnLogin { get; set; } = false;
|
||||||
public bool ShowRulesOverlayOnOpen { get; set; } = true;
|
public bool ShowRulesOverlayOnOpen { get; set; } = true;
|
||||||
public bool ShowMessageTimestamps { get; set; } = true;
|
public bool ShowMessageTimestamps { get; set; } = true;
|
||||||
public bool ShowNotesInSyncshellChat { get; set; } = true;
|
|
||||||
public bool EnableAnimatedEmotes { get; set; } = true;
|
|
||||||
public float EmoteScale { get; set; } = 1.5f;
|
|
||||||
public bool EnableMentionNotifications { get; set; } = true;
|
|
||||||
public bool AutoOpenChatOnNewMessage { get; set; } = false;
|
|
||||||
public float ChatWindowOpacity { get; set; } = .97f;
|
public float ChatWindowOpacity { get; set; } = .97f;
|
||||||
public bool FadeWhenUnfocused { get; set; } = false;
|
public bool FadeWhenUnfocused { get; set; } = false;
|
||||||
public float UnfocusedWindowOpacity { get; set; } = 0.6f;
|
public float UnfocusedWindowOpacity { get; set; } = 0.6f;
|
||||||
@@ -26,9 +21,6 @@ public sealed class ChatConfig : ILightlessConfiguration
|
|||||||
public bool ShowWhenUiHidden { get; set; } = true;
|
public bool ShowWhenUiHidden { get; set; } = true;
|
||||||
public bool ShowInCutscenes { get; set; } = true;
|
public bool ShowInCutscenes { get; set; } = true;
|
||||||
public bool ShowInGpose { get; set; } = true;
|
public bool ShowInGpose { get; set; } = true;
|
||||||
public bool PersistSyncshellHistory { get; set; } = false;
|
|
||||||
public List<string> ChannelOrder { get; set; } = new();
|
public List<string> ChannelOrder { get; set; } = new();
|
||||||
public Dictionary<string, bool> HiddenChannels { get; set; } = new(StringComparer.Ordinal);
|
|
||||||
public Dictionary<string, string> SyncshellChannelHistory { get; set; } = new(StringComparer.Ordinal);
|
|
||||||
public Dictionary<string, bool> PreferNotesForChannels { get; set; } = new(StringComparer.Ordinal);
|
public Dictionary<string, bool> PreferNotesForChannels { get; set; } = new(StringComparer.Ordinal);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ using LightlessSync.LightlessConfiguration.Models;
|
|||||||
using LightlessSync.UI;
|
using LightlessSync.UI;
|
||||||
using LightlessSync.UI.Models;
|
using LightlessSync.UI.Models;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using LightlessSync.PlayerData.Factories;
|
|
||||||
|
|
||||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||||
|
|
||||||
@@ -32,8 +31,6 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public DtrEntry.Colors DtrColorsLightfinderUnavailable { get; set; } = new(Foreground: 0x000000u, Glow: 0x000000u);
|
public DtrEntry.Colors DtrColorsLightfinderUnavailable { get; set; } = new(Foreground: 0x000000u, Glow: 0x000000u);
|
||||||
public LightfinderDtrDisplayMode LightfinderDtrDisplayMode { get; set; } = LightfinderDtrDisplayMode.PendingPairRequests;
|
public LightfinderDtrDisplayMode LightfinderDtrDisplayMode { get; set; } = LightfinderDtrDisplayMode.PendingPairRequests;
|
||||||
public bool UseLightlessRedesign { get; set; } = true;
|
public bool UseLightlessRedesign { get; set; } = true;
|
||||||
public bool ShowUiWhenUiHidden { get; set; } = true;
|
|
||||||
public bool ShowUiInGpose { get; set; } = true;
|
|
||||||
public bool EnableRightClickMenus { get; set; } = true;
|
public bool EnableRightClickMenus { get; set; } = true;
|
||||||
public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both;
|
public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both;
|
||||||
public string ExportFolder { get; set; } = string.Empty;
|
public string ExportFolder { get; set; } = string.Empty;
|
||||||
@@ -54,7 +51,6 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public bool PreferNotesOverNamesForVisible { get; set; } = false;
|
public bool PreferNotesOverNamesForVisible { get; set; } = false;
|
||||||
public VisiblePairSortMode VisiblePairSortMode { get; set; } = VisiblePairSortMode.Alphabetical;
|
public VisiblePairSortMode VisiblePairSortMode { get; set; } = VisiblePairSortMode.Alphabetical;
|
||||||
public OnlinePairSortMode OnlinePairSortMode { get; set; } = OnlinePairSortMode.Alphabetical;
|
public OnlinePairSortMode OnlinePairSortMode { get; set; } = OnlinePairSortMode.Alphabetical;
|
||||||
public TextureFormatSortMode TextureFormatSortMode { get; set; } = TextureFormatSortMode.None;
|
|
||||||
public float ProfileDelay { get; set; } = 1.5f;
|
public float ProfileDelay { get; set; } = 1.5f;
|
||||||
public bool ProfilePopoutRight { get; set; } = false;
|
public bool ProfilePopoutRight { get; set; } = false;
|
||||||
public bool ProfilesAllowNsfw { get; set; } = false;
|
public bool ProfilesAllowNsfw { get; set; } = false;
|
||||||
@@ -144,7 +140,6 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public bool useColoredUIDs { get; set; } = true;
|
public bool useColoredUIDs { get; set; } = true;
|
||||||
public bool BroadcastEnabled { get; set; } = false;
|
public bool BroadcastEnabled { get; set; } = false;
|
||||||
public bool LightfinderAutoEnableOnConnect { get; set; } = false;
|
public bool LightfinderAutoEnableOnConnect { get; set; } = false;
|
||||||
public LightfinderLabelRenderer LightfinderLabelRenderer { get; set; } = LightfinderLabelRenderer.Pictomancy;
|
|
||||||
public short LightfinderLabelOffsetX { get; set; } = 0;
|
public short LightfinderLabelOffsetX { get; set; } = 0;
|
||||||
public short LightfinderLabelOffsetY { get; set; } = 0;
|
public short LightfinderLabelOffsetY { get; set; } = 0;
|
||||||
public bool LightfinderLabelUseIcon { get; set; } = false;
|
public bool LightfinderLabelUseIcon { get; set; } = false;
|
||||||
@@ -159,8 +154,4 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public bool SyncshellFinderEnabled { get; set; } = false;
|
public bool SyncshellFinderEnabled { get; set; } = false;
|
||||||
public string? SelectedFinderSyncshell { get; set; } = null;
|
public string? SelectedFinderSyncshell { get; set; } = null;
|
||||||
public string LastSeenVersion { get; set; } = string.Empty;
|
public string LastSeenVersion { get; set; } = string.Empty;
|
||||||
public bool EnableParticleEffects { get; set; } = true;
|
|
||||||
public AnimationValidationMode AnimationValidationMode { get; set; } = AnimationValidationMode.Unsafe;
|
|
||||||
public bool AnimationAllowOneBasedShift { get; set; } = false;
|
|
||||||
public bool AnimationAllowNeighborIndexTolerance { get; set; } = false;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,156 +0,0 @@
|
|||||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
|
||||||
|
|
||||||
public static class ModelDecimationDefaults
|
|
||||||
{
|
|
||||||
public const bool EnableAutoDecimation = false;
|
|
||||||
public const int TriangleThreshold = 15_000;
|
|
||||||
public const double TargetRatio = 0.8;
|
|
||||||
public const bool NormalizeTangents = true;
|
|
||||||
public const bool AvoidBodyIntersection = true;
|
|
||||||
|
|
||||||
/// <summary>Default triangle threshold for batch decimation (0 = no threshold).</summary>
|
|
||||||
public const int BatchTriangleThreshold = 0;
|
|
||||||
|
|
||||||
/// <summary>Default target triangle ratio for batch decimation.</summary>
|
|
||||||
public const double BatchTargetRatio = 0.8;
|
|
||||||
|
|
||||||
/// <summary>Default tangent normalization toggle for batch decimation.</summary>
|
|
||||||
public const bool BatchNormalizeTangents = true;
|
|
||||||
|
|
||||||
/// <summary>Default body collision guard toggle for batch decimation.</summary>
|
|
||||||
public const bool BatchAvoidBodyIntersection = true;
|
|
||||||
|
|
||||||
/// <summary>Default display for the batch decimation warning overlay.</summary>
|
|
||||||
public const bool ShowBatchDecimationWarning = true;
|
|
||||||
|
|
||||||
public const bool KeepOriginalModelFiles = true;
|
|
||||||
public const bool SkipPreferredPairs = true;
|
|
||||||
public const bool AllowBody = false;
|
|
||||||
public const bool AllowFaceHead = false;
|
|
||||||
public const bool AllowTail = false;
|
|
||||||
public const bool AllowClothing = true;
|
|
||||||
public const bool AllowAccessories = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class ModelDecimationAdvancedSettings
|
|
||||||
{
|
|
||||||
/// <summary>Minimum triangles per connected component before skipping decimation.</summary>
|
|
||||||
public const int DefaultMinComponentTriangles = 6;
|
|
||||||
|
|
||||||
/// <summary>Average-edge multiplier used to cap collapses.</summary>
|
|
||||||
public const float DefaultMaxCollapseEdgeLengthFactor = 1.25f;
|
|
||||||
|
|
||||||
/// <summary>Maximum normal deviation (degrees) allowed for a collapse.</summary>
|
|
||||||
public const float DefaultNormalSimilarityThresholdDegrees = 60f;
|
|
||||||
|
|
||||||
/// <summary>Minimum bone-weight overlap required to allow a collapse.</summary>
|
|
||||||
public const float DefaultBoneWeightSimilarityThreshold = 0.85f;
|
|
||||||
|
|
||||||
/// <summary>UV similarity threshold to protect seams.</summary>
|
|
||||||
public const float DefaultUvSimilarityThreshold = 0.02f;
|
|
||||||
|
|
||||||
/// <summary>UV seam cosine threshold for blocking seam collapses.</summary>
|
|
||||||
public const float DefaultUvSeamAngleCos = 0.99f;
|
|
||||||
|
|
||||||
/// <summary>Whether to block UV seam vertices from collapsing.</summary>
|
|
||||||
public const bool DefaultBlockUvSeamVertices = true;
|
|
||||||
|
|
||||||
/// <summary>Whether to allow collapses on boundary edges.</summary>
|
|
||||||
public const bool DefaultAllowBoundaryCollapses = false;
|
|
||||||
|
|
||||||
/// <summary>Body collision distance factor for the primary pass.</summary>
|
|
||||||
public const float DefaultBodyCollisionDistanceFactor = 0.75f;
|
|
||||||
|
|
||||||
/// <summary>Body collision distance factor for the relaxed fallback pass.</summary>
|
|
||||||
public const float DefaultBodyCollisionNoOpDistanceFactor = 0.25f;
|
|
||||||
|
|
||||||
/// <summary>Relax multiplier applied when the mesh is close to the body.</summary>
|
|
||||||
public const float DefaultBodyCollisionAdaptiveRelaxFactor = 1.0f;
|
|
||||||
|
|
||||||
/// <summary>Ratio of near-body vertices required to trigger relaxation.</summary>
|
|
||||||
public const float DefaultBodyCollisionAdaptiveNearRatio = 0.4f;
|
|
||||||
|
|
||||||
/// <summary>UV threshold for relaxed body-collision mode.</summary>
|
|
||||||
public const float DefaultBodyCollisionAdaptiveUvThreshold = 0.08f;
|
|
||||||
|
|
||||||
/// <summary>UV seam cosine threshold for relaxed body-collision mode.</summary>
|
|
||||||
public const float DefaultBodyCollisionNoOpUvSeamAngleCos = 0.98f;
|
|
||||||
|
|
||||||
/// <summary>Expansion factor for protected vertices near the body.</summary>
|
|
||||||
public const float DefaultBodyCollisionProtectionFactor = 1.5f;
|
|
||||||
|
|
||||||
/// <summary>Minimum ratio used when decimating the body proxy.</summary>
|
|
||||||
public const float DefaultBodyProxyTargetRatioMin = 0.85f;
|
|
||||||
|
|
||||||
/// <summary>Inflation applied to body collision distances.</summary>
|
|
||||||
public const float DefaultBodyCollisionProxyInflate = 0.0005f;
|
|
||||||
|
|
||||||
/// <summary>Body collision penetration factor used during collapse checks.</summary>
|
|
||||||
public const float DefaultBodyCollisionPenetrationFactor = 0.75f;
|
|
||||||
|
|
||||||
/// <summary>Minimum body collision distance threshold.</summary>
|
|
||||||
public const float DefaultMinBodyCollisionDistance = 0.0001f;
|
|
||||||
|
|
||||||
/// <summary>Minimum cell size for body collision spatial hashing.</summary>
|
|
||||||
public const float DefaultMinBodyCollisionCellSize = 0.0001f;
|
|
||||||
|
|
||||||
/// <summary>Minimum triangles per connected component before skipping decimation.</summary>
|
|
||||||
public int MinComponentTriangles { get; set; } = DefaultMinComponentTriangles;
|
|
||||||
|
|
||||||
/// <summary>Average-edge multiplier used to cap collapses.</summary>
|
|
||||||
public float MaxCollapseEdgeLengthFactor { get; set; } = DefaultMaxCollapseEdgeLengthFactor;
|
|
||||||
|
|
||||||
/// <summary>Maximum normal deviation (degrees) allowed for a collapse.</summary>
|
|
||||||
public float NormalSimilarityThresholdDegrees { get; set; } = DefaultNormalSimilarityThresholdDegrees;
|
|
||||||
|
|
||||||
/// <summary>Minimum bone-weight overlap required to allow a collapse.</summary>
|
|
||||||
public float BoneWeightSimilarityThreshold { get; set; } = DefaultBoneWeightSimilarityThreshold;
|
|
||||||
|
|
||||||
/// <summary>UV similarity threshold to protect seams.</summary>
|
|
||||||
public float UvSimilarityThreshold { get; set; } = DefaultUvSimilarityThreshold;
|
|
||||||
|
|
||||||
/// <summary>UV seam cosine threshold for blocking seam collapses.</summary>
|
|
||||||
public float UvSeamAngleCos { get; set; } = DefaultUvSeamAngleCos;
|
|
||||||
|
|
||||||
/// <summary>Whether to block UV seam vertices from collapsing.</summary>
|
|
||||||
public bool BlockUvSeamVertices { get; set; } = DefaultBlockUvSeamVertices;
|
|
||||||
|
|
||||||
/// <summary>Whether to allow collapses on boundary edges.</summary>
|
|
||||||
public bool AllowBoundaryCollapses { get; set; } = DefaultAllowBoundaryCollapses;
|
|
||||||
|
|
||||||
/// <summary>Body collision distance factor for the primary pass.</summary>
|
|
||||||
public float BodyCollisionDistanceFactor { get; set; } = DefaultBodyCollisionDistanceFactor;
|
|
||||||
|
|
||||||
/// <summary>Body collision distance factor for the relaxed fallback pass.</summary>
|
|
||||||
public float BodyCollisionNoOpDistanceFactor { get; set; } = DefaultBodyCollisionNoOpDistanceFactor;
|
|
||||||
|
|
||||||
/// <summary>Relax multiplier applied when the mesh is close to the body.</summary>
|
|
||||||
public float BodyCollisionAdaptiveRelaxFactor { get; set; } = DefaultBodyCollisionAdaptiveRelaxFactor;
|
|
||||||
|
|
||||||
/// <summary>Ratio of near-body vertices required to trigger relaxation.</summary>
|
|
||||||
public float BodyCollisionAdaptiveNearRatio { get; set; } = DefaultBodyCollisionAdaptiveNearRatio;
|
|
||||||
|
|
||||||
/// <summary>UV threshold for relaxed body-collision mode.</summary>
|
|
||||||
public float BodyCollisionAdaptiveUvThreshold { get; set; } = DefaultBodyCollisionAdaptiveUvThreshold;
|
|
||||||
|
|
||||||
/// <summary>UV seam cosine threshold for relaxed body-collision mode.</summary>
|
|
||||||
public float BodyCollisionNoOpUvSeamAngleCos { get; set; } = DefaultBodyCollisionNoOpUvSeamAngleCos;
|
|
||||||
|
|
||||||
/// <summary>Expansion factor for protected vertices near the body.</summary>
|
|
||||||
public float BodyCollisionProtectionFactor { get; set; } = DefaultBodyCollisionProtectionFactor;
|
|
||||||
|
|
||||||
/// <summary>Minimum ratio used when decimating the body proxy.</summary>
|
|
||||||
public float BodyProxyTargetRatioMin { get; set; } = DefaultBodyProxyTargetRatioMin;
|
|
||||||
|
|
||||||
/// <summary>Inflation applied to body collision distances.</summary>
|
|
||||||
public float BodyCollisionProxyInflate { get; set; } = DefaultBodyCollisionProxyInflate;
|
|
||||||
|
|
||||||
/// <summary>Body collision penetration factor used during collapse checks.</summary>
|
|
||||||
public float BodyCollisionPenetrationFactor { get; set; } = DefaultBodyCollisionPenetrationFactor;
|
|
||||||
|
|
||||||
/// <summary>Minimum body collision distance threshold.</summary>
|
|
||||||
public float MinBodyCollisionDistance { get; set; } = DefaultMinBodyCollisionDistance;
|
|
||||||
|
|
||||||
/// <summary>Minimum cell size for body collision spatial hashing.</summary>
|
|
||||||
public float MinBodyCollisionCellSize { get; set; } = DefaultMinBodyCollisionCellSize;
|
|
||||||
}
|
|
||||||
@@ -21,26 +21,5 @@ public class PlayerPerformanceConfig : ILightlessConfiguration
|
|||||||
public bool EnableIndexTextureDownscale { get; set; } = false;
|
public bool EnableIndexTextureDownscale { get; set; } = false;
|
||||||
public int TextureDownscaleMaxDimension { get; set; } = 2048;
|
public int TextureDownscaleMaxDimension { get; set; } = 2048;
|
||||||
public bool OnlyDownscaleUncompressedTextures { get; set; } = true;
|
public bool OnlyDownscaleUncompressedTextures { get; set; } = true;
|
||||||
public bool EnableUncompressedTextureCompression { get; set; } = false;
|
|
||||||
public bool SkipUncompressedTextureCompressionMipMaps { get; set; } = false;
|
|
||||||
public bool KeepOriginalTextureFiles { get; set; } = false;
|
public bool KeepOriginalTextureFiles { get; set; } = false;
|
||||||
public bool SkipTextureDownscaleForPreferredPairs { get; set; } = true;
|
|
||||||
public bool EnableModelDecimation { get; set; } = ModelDecimationDefaults.EnableAutoDecimation;
|
|
||||||
public int ModelDecimationTriangleThreshold { get; set; } = ModelDecimationDefaults.TriangleThreshold;
|
|
||||||
public double ModelDecimationTargetRatio { get; set; } = ModelDecimationDefaults.TargetRatio;
|
|
||||||
public bool ModelDecimationNormalizeTangents { get; set; } = ModelDecimationDefaults.NormalizeTangents;
|
|
||||||
public bool ModelDecimationAvoidBodyIntersection { get; set; } = ModelDecimationDefaults.AvoidBodyIntersection;
|
|
||||||
public ModelDecimationAdvancedSettings ModelDecimationAdvanced { get; set; } = new();
|
|
||||||
public int BatchModelDecimationTriangleThreshold { get; set; } = ModelDecimationDefaults.BatchTriangleThreshold;
|
|
||||||
public double BatchModelDecimationTargetRatio { get; set; } = ModelDecimationDefaults.BatchTargetRatio;
|
|
||||||
public bool BatchModelDecimationNormalizeTangents { get; set; } = ModelDecimationDefaults.BatchNormalizeTangents;
|
|
||||||
public bool BatchModelDecimationAvoidBodyIntersection { get; set; } = ModelDecimationDefaults.BatchAvoidBodyIntersection;
|
|
||||||
public bool ShowBatchModelDecimationWarning { get; set; } = ModelDecimationDefaults.ShowBatchDecimationWarning;
|
|
||||||
public bool KeepOriginalModelFiles { get; set; } = ModelDecimationDefaults.KeepOriginalModelFiles;
|
|
||||||
public bool SkipModelDecimationForPreferredPairs { get; set; } = ModelDecimationDefaults.SkipPreferredPairs;
|
|
||||||
public bool ModelDecimationAllowBody { get; set; } = ModelDecimationDefaults.AllowBody;
|
|
||||||
public bool ModelDecimationAllowFaceHead { get; set; } = ModelDecimationDefaults.AllowFaceHead;
|
|
||||||
public bool ModelDecimationAllowTail { get; set; } = ModelDecimationDefaults.AllowTail;
|
|
||||||
public bool ModelDecimationAllowClothing { get; set; } = ModelDecimationDefaults.AllowClothing;
|
|
||||||
public bool ModelDecimationAllowAccessories { get; set; } = ModelDecimationDefaults.AllowAccessories;
|
|
||||||
}
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using LightlessSync.LightlessConfiguration.Models;
|
|
||||||
|
|
||||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public sealed class TempCollectionConfig : ILightlessConfiguration
|
|
||||||
{
|
|
||||||
public int Version { get; set; } = 1;
|
|
||||||
public List<OrphanableTempCollectionEntry> OrphanableTempCollectionEntries { get; set; } = [];
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@ namespace LightlessSync.LightlessConfiguration.Configurations;
|
|||||||
public class TransientConfig : ILightlessConfiguration
|
public class TransientConfig : ILightlessConfiguration
|
||||||
{
|
{
|
||||||
public Dictionary<string, TransientPlayerConfig> TransientConfigs { get; set; } = [];
|
public Dictionary<string, TransientPlayerConfig> TransientConfigs { get; set; } = [];
|
||||||
public int Version { get; set; } = 2;
|
public int Version { get; set; } = 1;
|
||||||
|
|
||||||
public class TransientPlayerConfig
|
public class TransientPlayerConfig
|
||||||
{
|
{
|
||||||
@@ -88,70 +88,5 @@ public class TransientConfig : ILightlessConfiguration
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool NormalizePaths(out int removedEntries)
|
|
||||||
{
|
|
||||||
bool changed = false;
|
|
||||||
removedEntries = 0;
|
|
||||||
|
|
||||||
GlobalPersistentCache = NormalizeList(GlobalPersistentCache, ref changed, ref removedEntries);
|
|
||||||
|
|
||||||
foreach (var jobId in JobSpecificCache.Keys.ToList())
|
|
||||||
{
|
|
||||||
JobSpecificCache[jobId] = NormalizeList(JobSpecificCache[jobId], ref changed, ref removedEntries);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var jobId in JobSpecificPetCache.Keys.ToList())
|
|
||||||
{
|
|
||||||
JobSpecificPetCache[jobId] = NormalizeList(JobSpecificPetCache[jobId], ref changed, ref removedEntries);
|
|
||||||
}
|
|
||||||
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<string> NormalizeList(List<string> entries, ref bool changed, ref int removedEntries)
|
|
||||||
{
|
|
||||||
if (entries.Count == 0)
|
|
||||||
return entries;
|
|
||||||
|
|
||||||
var result = new List<string>(entries.Count);
|
|
||||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
foreach (var entry in entries)
|
|
||||||
{
|
|
||||||
var normalized = NormalizePath(entry);
|
|
||||||
if (string.IsNullOrEmpty(normalized))
|
|
||||||
{
|
|
||||||
changed = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.Equals(entry, normalized, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seen.Add(normalized))
|
|
||||||
{
|
|
||||||
result.Add(normalized);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removedEntries += entries.Count - result.Count;
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string NormalizePath(string path)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(path))
|
|
||||||
return string.Empty;
|
|
||||||
|
|
||||||
return path.Replace("\\", "/", StringComparison.Ordinal).ToLowerInvariant();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ namespace LightlessSync.LightlessConfiguration.Configurations;
|
|||||||
public class XivDataStorageConfig : ILightlessConfiguration
|
public class XivDataStorageConfig : ILightlessConfiguration
|
||||||
{
|
{
|
||||||
public ConcurrentDictionary<string, long> TriangleDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
public ConcurrentDictionary<string, long> TriangleDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
public ConcurrentDictionary<string, long> EffectiveTriangleDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
public ConcurrentDictionary<string, Dictionary<string, List<ushort>>> BonesDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
public ConcurrentDictionary<string, Dictionary<string, List<ushort>>> BonesDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
public int Version { get; set; } = 0;
|
public int Version { get; set; } = 0;
|
||||||
}
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace LightlessSync.LightlessConfiguration.Models;
|
|
||||||
|
|
||||||
public sealed class OrphanableTempCollectionEntry
|
|
||||||
{
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
public DateTime RegisteredAtUtc { get; set; } = DateTime.MinValue;
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using LightlessSync.LightlessConfiguration.Configurations;
|
|
||||||
|
|
||||||
namespace LightlessSync.LightlessConfiguration;
|
|
||||||
|
|
||||||
public sealed class TempCollectionConfigService : ConfigurationServiceBase<TempCollectionConfig>
|
|
||||||
{
|
|
||||||
public const string ConfigName = "tempcollections.json";
|
|
||||||
|
|
||||||
public TempCollectionConfigService(string configDir) : base(configDir) { }
|
|
||||||
|
|
||||||
public override string ConfigurationName => ConfigName;
|
|
||||||
}
|
|
||||||
@@ -74,7 +74,6 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
|||||||
private readonly DalamudUtilService _dalamudUtil;
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
private readonly LightlessConfigService _lightlessConfigService;
|
private readonly LightlessConfigService _lightlessConfigService;
|
||||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
private readonly PairHandlerRegistry _pairHandlerRegistry;
|
|
||||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
private IServiceScope? _runtimeServiceScope;
|
private IServiceScope? _runtimeServiceScope;
|
||||||
private Task? _launchTask = null;
|
private Task? _launchTask = null;
|
||||||
@@ -82,13 +81,11 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
|||||||
public LightlessPlugin(ILogger<LightlessPlugin> logger, LightlessConfigService lightlessConfigService,
|
public LightlessPlugin(ILogger<LightlessPlugin> logger, LightlessConfigService lightlessConfigService,
|
||||||
ServerConfigurationManager serverConfigurationManager,
|
ServerConfigurationManager serverConfigurationManager,
|
||||||
DalamudUtilService dalamudUtil,
|
DalamudUtilService dalamudUtil,
|
||||||
PairHandlerRegistry pairHandlerRegistry,
|
|
||||||
IServiceScopeFactory serviceScopeFactory, LightlessMediator mediator) : base(logger, mediator)
|
IServiceScopeFactory serviceScopeFactory, LightlessMediator mediator) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_lightlessConfigService = lightlessConfigService;
|
_lightlessConfigService = lightlessConfigService;
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
_dalamudUtil = dalamudUtil;
|
_dalamudUtil = dalamudUtil;
|
||||||
_pairHandlerRegistry = pairHandlerRegistry;
|
|
||||||
_serviceScopeFactory = serviceScopeFactory;
|
_serviceScopeFactory = serviceScopeFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,20 +108,12 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
|||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken)
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Halting LightlessPlugin");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_pairHandlerRegistry.ResetAllHandlers();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogWarning(ex, "Failed to reset pair handlers on shutdown");
|
|
||||||
}
|
|
||||||
|
|
||||||
UnsubscribeAll();
|
UnsubscribeAll();
|
||||||
|
|
||||||
DalamudUtilOnLogOut();
|
DalamudUtilOnLogOut();
|
||||||
|
|
||||||
|
Logger.LogDebug("Halting LightlessPlugin");
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors></Authors>
|
<Authors></Authors>
|
||||||
<Company></Company>
|
<Company></Company>
|
||||||
<Version>2.0.3</Version>
|
<Version>2.0.1</Version>
|
||||||
<Description></Description>
|
<Description></Description>
|
||||||
<Copyright></Copyright>
|
<Copyright></Copyright>
|
||||||
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
||||||
@@ -24,15 +24,6 @@
|
|||||||
<Compile Remove="PlayerData\Export\**" />
|
<Compile Remove="PlayerData\Export\**" />
|
||||||
<EmbeddedResource Remove="PlayerData\Export\**" />
|
<EmbeddedResource Remove="PlayerData\Export\**" />
|
||||||
<None Remove="PlayerData\Export\**" />
|
<None Remove="PlayerData\Export\**" />
|
||||||
<EmbeddedResource Update="Resources\Resources.resx">
|
|
||||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
|
||||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
|
||||||
</EmbeddedResource>
|
|
||||||
<Compile Update="Resources\Resources.Designer.cs">
|
|
||||||
<DesignTime>True</DesignTime>
|
|
||||||
<AutoGen>True</AutoGen>
|
|
||||||
<DependentUpon>Resources.resx</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -46,7 +37,6 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.1" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="10.0.1" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="10.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.1" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
|
||||||
<PackageReference Include="Glamourer.Api" Version="2.8.0" />
|
<PackageReference Include="Glamourer.Api" Version="2.8.0" />
|
||||||
<PackageReference Include="NReco.Logging.File" Version="1.3.1" />
|
<PackageReference Include="NReco.Logging.File" Version="1.3.1" />
|
||||||
@@ -77,6 +67,8 @@
|
|||||||
</None>
|
</None>
|
||||||
<EmbeddedResource Include="Changelog\changelog.yaml" />
|
<EmbeddedResource Include="Changelog\changelog.yaml" />
|
||||||
<EmbeddedResource Include="Changelog\credits.yaml" />
|
<EmbeddedResource Include="Changelog\credits.yaml" />
|
||||||
|
<EmbeddedResource Include="Localization\de.json" />
|
||||||
|
<EmbeddedResource Include="Localization\fr.json" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -85,8 +77,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\LightlessAPI\LightlessSyncAPI\LightlessSync.API.csproj" />
|
<ProjectReference Include="..\LightlessAPI\LightlessSyncAPI\LightlessSync.API.csproj" />
|
||||||
<ProjectReference Include="..\LightlessCompactor\LightlessCompactor.csproj" />
|
|
||||||
<ProjectReference Include="..\LightlessCompactorWorker\LightlessCompactorWorker.csproj" ReferenceOutputAssembly="false" />
|
|
||||||
<ProjectReference Include="..\Penumbra.Api\Penumbra.Api.csproj" />
|
<ProjectReference Include="..\Penumbra.Api\Penumbra.Api.csproj" />
|
||||||
<ProjectReference Include="..\Penumbra.GameData\Penumbra.GameData.csproj" />
|
<ProjectReference Include="..\Penumbra.GameData\Penumbra.GameData.csproj" />
|
||||||
<ProjectReference Include="..\Penumbra.String\Penumbra.String.csproj" />
|
<ProjectReference Include="..\Penumbra.String\Penumbra.String.csproj" />
|
||||||
@@ -110,13 +100,5 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Update="DalamudPackager" Version="14.0.1" />
|
<PackageReference Update="DalamudPackager" Version="14.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<CompactorWorkerFiles Include="..\LightlessCompactorWorker\bin\$(Configuration)\net10.0\*.*" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<Target Name="CopyCompactorWorker" AfterTargets="Build">
|
|
||||||
<Copy SourceFiles="@(CompactorWorkerFiles)" DestinationFolder="$(OutputPath)" SkipUnchangedFiles="true" />
|
|
||||||
</Target>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
|
||||||
<s:String x:Key="/Default/CodeEditing/Localization/Localizable/@EntryValue">Yes</s:String>
|
|
||||||
<s:String x:Key="/Default/CodeEditing/Localization/LocalizableInspector/@EntryValue">Pessimistic</s:String></wpf:ResourceDictionary>
|
|
||||||
44
LightlessSync/Localization/Strings.cs
Normal file
44
LightlessSync/Localization/Strings.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using CheapLoc;
|
||||||
|
|
||||||
|
namespace LightlessSync.Localization;
|
||||||
|
|
||||||
|
public static class Strings
|
||||||
|
{
|
||||||
|
public static ToSStrings ToS { get; set; } = new();
|
||||||
|
|
||||||
|
public class ToSStrings
|
||||||
|
{
|
||||||
|
public readonly string AgreeLabel = Loc.Localize("AgreeLabel", "I agree");
|
||||||
|
public readonly string AgreementLabel = Loc.Localize("AgreementLabel", "Agreement of Usage of Service");
|
||||||
|
public readonly string ButtonWillBeAvailableIn = Loc.Localize("ButtonWillBeAvailableIn", "'I agree' button will be available in");
|
||||||
|
public readonly string LanguageLabel = Loc.Localize("LanguageLabel", "Language");
|
||||||
|
|
||||||
|
public readonly string Paragraph1 = Loc.Localize("Paragraph1",
|
||||||
|
"All of the mod files currently active on your character as well as your current character state will be uploaded to the service you registered yourself at automatically. " +
|
||||||
|
"The plugin will exclusively upload the necessary mod files and not the whole mod.");
|
||||||
|
|
||||||
|
public readonly string Paragraph2 = Loc.Localize("Paragraph2",
|
||||||
|
"If you are on a data capped internet connection, higher fees due to data usage depending on the amount of downloaded and uploaded mod files might occur. " +
|
||||||
|
"Mod files will be compressed on up- and download to save on bandwidth usage. Due to varying up- and download speeds, changes in characters might not be visible immediately. " +
|
||||||
|
"Files present on the service that already represent your active mod files will not be uploaded again.");
|
||||||
|
|
||||||
|
public readonly string Paragraph3 = Loc.Localize("Paragraph3",
|
||||||
|
"The mod files you are uploading are confidential and will not be distributed to parties other than the ones who are requesting the exact same mod files. " +
|
||||||
|
"Please think about who you are going to pair since it is unavoidable that they will receive and locally cache the necessary mod files that you have currently in use. " +
|
||||||
|
"Locally cached mod files will have arbitrary file names to discourage attempts at replicating the original mod.");
|
||||||
|
|
||||||
|
public readonly string Paragraph4 = Loc.Localize("Paragraph4",
|
||||||
|
"The plugin creator tried their best to keep you secure. However, there is no guarantee for 100% security. Do not blindly pair your client with everyone.");
|
||||||
|
|
||||||
|
public readonly string Paragraph5 = Loc.Localize("Paragraph5",
|
||||||
|
"Mod files that are saved on the service will remain on the service as long as there are requests for the files from clients. " +
|
||||||
|
"After a period of not being used, the mod files will be automatically deleted. " +
|
||||||
|
"You will also be able to wipe all the files you have personally uploaded on request. " +
|
||||||
|
"The service holds no information about which mod files belong to which mod.");
|
||||||
|
|
||||||
|
public readonly string Paragraph6 = Loc.Localize("Paragraph6",
|
||||||
|
"This service is provided as-is. In case of abuse join the Lightless Sync Discord.");
|
||||||
|
|
||||||
|
public readonly string ReadLabel = Loc.Localize("ReadLabel", "READ THIS CAREFULLY");
|
||||||
|
}
|
||||||
|
}
|
||||||
46
LightlessSync/Localization/de.json
Normal file
46
LightlessSync/Localization/de.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"LanguageLabel": {
|
||||||
|
"message": "Language",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"AgreementLabel": {
|
||||||
|
"message": "Nutzungsbedingungen",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"ReadLabel": {
|
||||||
|
"message": "BITTE LIES DIES SORGFÄLTIG",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"Paragraph1": {
|
||||||
|
"message": "Alle Moddateien, die aktuell auf deinem Charakter aktiv sind und dein Charakterzustand werden automatisch zu dem Service, an dem du dich registriert hast, hochgeladen. Das Plugin wird ausschließlich die nötigen Moddateien hochladen und nicht die gesamte Modifikation.",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"Paragraph2": {
|
||||||
|
"message": "Falls du mit einer getakteten Internetverbindung verbunden bist, können durch den Datentransfer von Hoch- und Runtergeladenen Moddateien höhere Kosten entstehen. Moddateien werden beim Hoch- und Runterladen komprimiert um Bandbreite zu sparen. Durch unterschiedliche Hoch- und Runterladgeschwindigkeiten ist es möglich, dass Änderungen an Charakteren nicht sofort sichtbar sind. Dateien die bereits auf dem Service existieren, werden nicht nochmals hochgeladen.",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"Paragraph3": {
|
||||||
|
"message": "Die Moddateien die du hochlädst sind vertraulich und werden nicht mit anderen Nutzern geteilt, die nicht die exakt selben Dateien anfordern. Bitte überlege dir sorgfältig mit wem du deinen Identifikationscode teilst, da es unvermeidlich ist, dass die andere Person deine Moddateien erhält und lokal zwischenspeichert. Lokal zwischengespeicherte Dateien haben willkürrliche Namen um vor Versuchen abzuschrecken die originalen Moddateien aus diesen wiederherzustellen.",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"Paragraph4": {
|
||||||
|
"message": "Der Ersteller des Plugins hat sein Bestes getan, um deine Sicherheit zu gewährleisten. Es gibt jedoch keine Garantie für 100%ige Sicherheit. Teile deinen Identifikationscode nicht blind mit jedem.",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"Paragraph5": {
|
||||||
|
"message": "Moddateien, die auf dem Service gespeichert sind, verbleiben auf dem Service, solange es Anforderungen für diese Dateien gibt. Nach einer Zeitspanne in der die Dateien nicht verwendet wurden, werden diese automatisch gelöscht. Du hast auch die Möglichkeit manuell alle Dateien auf dem Service zu löschen. Der Service hat keine Informationen welche Moddateien zu welcher Modifikation gehören.",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"Paragraph6": {
|
||||||
|
"message": "Dieser Dienst wird ohne Gewähr angeboten. Im Falle eines Missbrauchs tretet dem Lightless Sync Discord bei.",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"AgreeLabel": {
|
||||||
|
"message": "Ich Stimme zu",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"ButtonWillBeAvailableIn": {
|
||||||
|
"message": "\"Ich stimme zu\" Knopf verfügbar in",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
}
|
||||||
|
}
|
||||||
46
LightlessSync/Localization/fr.json
Normal file
46
LightlessSync/Localization/fr.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"LanguageLabel": {
|
||||||
|
"message": "Language",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"AgreementLabel": {
|
||||||
|
"message": "Conditions d'Utilisation",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"ReadLabel": {
|
||||||
|
"message": "LISEZ CES INFORMATIONS ATTENTIVEMENT",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"Paragraph1": {
|
||||||
|
"message": "Tous les fichiers moddés actuellement en cours d'utilisation ainsi que le statut actuel de votre personnage vont être mix en ligne via le service sur lequel vous vous êtes automatiquement enregistré. Seuls les fichiers nécessaires seront téléversés par le plugin et non pas le mod en entier.",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"Paragraph2": {
|
||||||
|
"message": "Si le débit de votre connexion internet est limité, le téléchargement et téléversement d'un grand nombre de fichiers peut entraîner des coûts supplémentaires. Les fichiers seront compressés au chargement et versement pour réduire l'impact sur votre bande passants. Selon la rapidité de vos téléchargements et téléversements, les changements ne seront peut-être pas visibles instantanément sur les personnages. Les fichiers déja présents sur le service qui correspondent à ceux de vos mods en cours d'utilisation ne seront pas remis en ligne.",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"Paragraph3": {
|
||||||
|
"message": "Les fichiers que vous allez partager sont confidentiels et ne seront envoyés qu'aux utilisateurs qui feront une requête exacte de ceux-çi. Nous vous demandons de (re)considérer qui sera synchronisé avec vous, puisqu'ils recevront et stockeront inévitablement en local les fichiers nécéssaires utilisés à cet instant. Les noms des fichiers stockés localement sont changés de manière arbitraire afin de décourager toute tentative de réplication des originaux.",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"Paragraph4": {
|
||||||
|
"message": "Le créateur de ce plugin a tenté de sécuriser l'application du mieux possible. Cependant, il ne peut pas garantir une protection 100% infaillible. Pour votre sécurité, ne vous synchronisez pas aveuglément et avec n'importe qui.",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"Paragraph5": {
|
||||||
|
"message": "Les fichiers sauvegardés sur le service resteront en ligne tant que des utilisateurs en feront usage. Ils seront effacés automatiquement après une certaine période d'inactivité. Vous pouvez également demander l'effacement de tous les fichiers que vous avez mis en ligne vous-même. Le service en soi ne contient aucune information pouvant identifier quel fichier appartient à quel mod.",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"Paragraph6": {
|
||||||
|
"message": "Ce service et ses composants vous sont fournis en l'état. En cas d'abus rejoindre le serveur Discord Lightless Sync.",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"AgreeLabel": {
|
||||||
|
"message": "J'accept",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
},
|
||||||
|
"ButtonWillBeAvailableIn": {
|
||||||
|
"message": "Bouton \"J'accept\" disposible dans",
|
||||||
|
"description": "ToSStrings..ctor"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace LightlessSync.PlayerData.Factories
|
|
||||||
{
|
|
||||||
public enum AnimationValidationMode
|
|
||||||
{
|
|
||||||
Unsafe = 0,
|
|
||||||
Safe = 1,
|
|
||||||
Safest = 2,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Services.ModelDecimation;
|
|
||||||
using LightlessSync.Services.TextureCompression;
|
using LightlessSync.Services.TextureCompression;
|
||||||
using LightlessSync.WebAPI.Files;
|
using LightlessSync.WebAPI.Files;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -17,9 +16,7 @@ public class FileDownloadManagerFactory
|
|||||||
private readonly FileCompactor _fileCompactor;
|
private readonly FileCompactor _fileCompactor;
|
||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly TextureDownscaleService _textureDownscaleService;
|
private readonly TextureDownscaleService _textureDownscaleService;
|
||||||
private readonly ModelDecimationService _modelDecimationService;
|
|
||||||
private readonly TextureMetadataHelper _textureMetadataHelper;
|
private readonly TextureMetadataHelper _textureMetadataHelper;
|
||||||
private readonly FileDownloadDeduplicator _downloadDeduplicator;
|
|
||||||
|
|
||||||
public FileDownloadManagerFactory(
|
public FileDownloadManagerFactory(
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
@@ -29,9 +26,7 @@ public class FileDownloadManagerFactory
|
|||||||
FileCompactor fileCompactor,
|
FileCompactor fileCompactor,
|
||||||
LightlessConfigService configService,
|
LightlessConfigService configService,
|
||||||
TextureDownscaleService textureDownscaleService,
|
TextureDownscaleService textureDownscaleService,
|
||||||
ModelDecimationService modelDecimationService,
|
TextureMetadataHelper textureMetadataHelper)
|
||||||
TextureMetadataHelper textureMetadataHelper,
|
|
||||||
FileDownloadDeduplicator downloadDeduplicator)
|
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_lightlessMediator = lightlessMediator;
|
_lightlessMediator = lightlessMediator;
|
||||||
@@ -40,9 +35,7 @@ public class FileDownloadManagerFactory
|
|||||||
_fileCompactor = fileCompactor;
|
_fileCompactor = fileCompactor;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_textureDownscaleService = textureDownscaleService;
|
_textureDownscaleService = textureDownscaleService;
|
||||||
_modelDecimationService = modelDecimationService;
|
|
||||||
_textureMetadataHelper = textureMetadataHelper;
|
_textureMetadataHelper = textureMetadataHelper;
|
||||||
_downloadDeduplicator = downloadDeduplicator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public FileDownloadManager Create()
|
public FileDownloadManager Create()
|
||||||
@@ -55,8 +48,6 @@ public class FileDownloadManagerFactory
|
|||||||
_fileCompactor,
|
_fileCompactor,
|
||||||
_configService,
|
_configService,
|
||||||
_textureDownscaleService,
|
_textureDownscaleService,
|
||||||
_modelDecimationService,
|
_textureMetadataHelper);
|
||||||
_textureMetadataHelper,
|
|
||||||
_downloadDeduplicator);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
using Dalamud.Utility;
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
using LightlessSync.API.Data.Enum;
|
||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using LightlessSync.Interop.Ipc;
|
using LightlessSync.Interop.Ipc;
|
||||||
using LightlessSync.LightlessConfiguration;
|
|
||||||
using LightlessSync.LightlessConfiguration.Models;
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
using LightlessSync.PlayerData.Data;
|
using LightlessSync.PlayerData.Data;
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Utils;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
|
||||||
|
|
||||||
namespace LightlessSync.PlayerData.Factories;
|
namespace LightlessSync.PlayerData.Factories;
|
||||||
|
|
||||||
@@ -24,34 +18,13 @@ public class PlayerDataFactory
|
|||||||
private readonly IpcManager _ipcManager;
|
private readonly IpcManager _ipcManager;
|
||||||
private readonly ILogger<PlayerDataFactory> _logger;
|
private readonly ILogger<PlayerDataFactory> _logger;
|
||||||
private readonly PerformanceCollectorService _performanceCollector;
|
private readonly PerformanceCollectorService _performanceCollector;
|
||||||
private readonly LightlessConfigService _configService;
|
|
||||||
private readonly XivDataAnalyzer _modelAnalyzer;
|
private readonly XivDataAnalyzer _modelAnalyzer;
|
||||||
private readonly LightlessMediator _lightlessMediator;
|
private readonly LightlessMediator _lightlessMediator;
|
||||||
private readonly TransientResourceManager _transientResourceManager;
|
private readonly TransientResourceManager _transientResourceManager;
|
||||||
private static readonly SemaphoreSlim _papParseLimiter = new(1, 1);
|
|
||||||
|
|
||||||
// Transient resolved entries threshold
|
public PlayerDataFactory(ILogger<PlayerDataFactory> logger, DalamudUtilService dalamudUtil, IpcManager ipcManager,
|
||||||
private const int _maxTransientResolvedEntries = 1000;
|
TransientResourceManager transientResourceManager, FileCacheManager fileReplacementFactory,
|
||||||
|
PerformanceCollectorService performanceCollector, XivDataAnalyzer modelAnalyzer, LightlessMediator lightlessMediator)
|
||||||
// Character build caches
|
|
||||||
private readonly TaskRegistry<nint> _characterBuildInflight = new();
|
|
||||||
private readonly ConcurrentDictionary<nint, CacheEntry> _characterBuildCache = new();
|
|
||||||
|
|
||||||
// Time out thresholds
|
|
||||||
private static readonly TimeSpan _characterCacheTtl = TimeSpan.FromMilliseconds(750);
|
|
||||||
private static readonly TimeSpan _softReturnIfBusyAfter = TimeSpan.FromMilliseconds(250);
|
|
||||||
private static readonly TimeSpan _hardBuildTimeout = TimeSpan.FromSeconds(30);
|
|
||||||
|
|
||||||
public PlayerDataFactory(
|
|
||||||
ILogger<PlayerDataFactory> logger,
|
|
||||||
DalamudUtilService dalamudUtil,
|
|
||||||
IpcManager ipcManager,
|
|
||||||
TransientResourceManager transientResourceManager,
|
|
||||||
FileCacheManager fileReplacementFactory,
|
|
||||||
PerformanceCollectorService performanceCollector,
|
|
||||||
XivDataAnalyzer modelAnalyzer,
|
|
||||||
LightlessMediator lightlessMediator,
|
|
||||||
LightlessConfigService configService)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_dalamudUtil = dalamudUtil;
|
_dalamudUtil = dalamudUtil;
|
||||||
@@ -61,15 +34,15 @@ public class PlayerDataFactory
|
|||||||
_performanceCollector = performanceCollector;
|
_performanceCollector = performanceCollector;
|
||||||
_modelAnalyzer = modelAnalyzer;
|
_modelAnalyzer = modelAnalyzer;
|
||||||
_lightlessMediator = lightlessMediator;
|
_lightlessMediator = lightlessMediator;
|
||||||
_configService = configService;
|
|
||||||
_logger.LogTrace("Creating {this}", nameof(PlayerDataFactory));
|
_logger.LogTrace("Creating {this}", nameof(PlayerDataFactory));
|
||||||
}
|
}
|
||||||
private sealed record CacheEntry(CharacterDataFragment Fragment, DateTime CreatedUtc);
|
|
||||||
|
|
||||||
public async Task<CharacterDataFragment?> BuildCharacterData(GameObjectHandler playerRelatedObject, CancellationToken token)
|
public async Task<CharacterDataFragment?> BuildCharacterData(GameObjectHandler playerRelatedObject, CancellationToken token)
|
||||||
{
|
{
|
||||||
if (!_ipcManager.Initialized)
|
if (!_ipcManager.Initialized)
|
||||||
|
{
|
||||||
throw new InvalidOperationException("Penumbra or Glamourer is not connected");
|
throw new InvalidOperationException("Penumbra or Glamourer is not connected");
|
||||||
|
}
|
||||||
|
|
||||||
if (playerRelatedObject == null) return null;
|
if (playerRelatedObject == null) return null;
|
||||||
|
|
||||||
@@ -94,17 +67,16 @@ public class PlayerDataFactory
|
|||||||
|
|
||||||
if (pointerIsZero)
|
if (pointerIsZero)
|
||||||
{
|
{
|
||||||
_logger.LogTrace("Pointer was zero for {objectKind}; couldn't build character", playerRelatedObject.ObjectKind);
|
_logger.LogTrace("Pointer was zero for {objectKind}", playerRelatedObject.ObjectKind);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await _performanceCollector.LogPerformance(
|
return await _performanceCollector.LogPerformance(this, $"CreateCharacterData>{playerRelatedObject.ObjectKind}", async () =>
|
||||||
this,
|
{
|
||||||
$"CreateCharacterData>{playerRelatedObject.ObjectKind}",
|
return await CreateCharacterData(playerRelatedObject, token).ConfigureAwait(false);
|
||||||
async () => await CreateCharacterData(playerRelatedObject, token).ConfigureAwait(false)
|
}).ConfigureAwait(true);
|
||||||
).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -119,207 +91,115 @@ public class PlayerDataFactory
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly int _drawObjectOffset =
|
|
||||||
(int)Marshal.OffsetOf<GameObject>(nameof(GameObject.DrawObject));
|
|
||||||
|
|
||||||
private async Task<bool> CheckForNullDrawObject(IntPtr playerPointer)
|
private async Task<bool> CheckForNullDrawObject(IntPtr playerPointer)
|
||||||
=> await _dalamudUtil.RunOnFrameworkThread(() =>
|
|
||||||
{
|
|
||||||
nint basePtr = playerPointer;
|
|
||||||
|
|
||||||
if (!PtrGuard.LooksLikePtr(basePtr))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
nint drawObjAddr = basePtr + _drawObjectOffset;
|
|
||||||
|
|
||||||
if (!PtrGuard.IsReadable(drawObjAddr, (nuint)IntPtr.Size))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (!PtrGuard.TryReadIntPtr(drawObjAddr, out var drawObj))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (drawObj != 0 && !PtrGuard.LooksLikePtr(drawObj))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return drawObj == 0;
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
|
|
||||||
private static bool IsCacheFresh(CacheEntry entry)
|
|
||||||
=> (DateTime.UtcNow - entry.CreatedUtc) <= _characterCacheTtl;
|
|
||||||
|
|
||||||
private Task<CharacterDataFragment> CreateCharacterData(GameObjectHandler playerRelatedObject, CancellationToken ct)
|
|
||||||
=> CreateCharacterDataCoalesced(playerRelatedObject, ct);
|
|
||||||
|
|
||||||
private async Task<CharacterDataFragment> CreateCharacterDataCoalesced(GameObjectHandler obj, CancellationToken ct)
|
|
||||||
{
|
{
|
||||||
var key = obj.Address;
|
return await _dalamudUtil.RunOnFrameworkThread(() => CheckForNullDrawObjectUnsafe(playerPointer)).ConfigureAwait(false);
|
||||||
|
|
||||||
if (_characterBuildCache.TryGetValue(key, out CacheEntry cached) && IsCacheFresh(cached) && !_characterBuildInflight.TryGetExisting(key, out _))
|
|
||||||
return cached.Fragment;
|
|
||||||
|
|
||||||
Task<CharacterDataFragment> buildTask = _characterBuildInflight.GetOrStart(key, () => BuildAndCacheAsync(obj, key));
|
|
||||||
|
|
||||||
if (_characterBuildCache.TryGetValue(key, out cached))
|
|
||||||
{
|
|
||||||
var completed = await Task.WhenAny(buildTask, Task.Delay(_softReturnIfBusyAfter, ct)).ConfigureAwait(false);
|
|
||||||
if (completed != buildTask && (DateTime.UtcNow - cached.CreatedUtc) <= TimeSpan.FromSeconds(5))
|
|
||||||
{
|
|
||||||
return cached.Fragment;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await WithCancellation(buildTask, ct).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<CharacterDataFragment> BuildAndCacheAsync(GameObjectHandler obj, nint key)
|
private unsafe bool CheckForNullDrawObjectUnsafe(IntPtr playerPointer)
|
||||||
{
|
{
|
||||||
using var cts = new CancellationTokenSource(_hardBuildTimeout);
|
if (playerPointer == IntPtr.Zero)
|
||||||
CharacterDataFragment fragment = await CreateCharacterDataInternal(obj, cts.Token).ConfigureAwait(false);
|
return true;
|
||||||
|
|
||||||
_characterBuildCache[key] = new CacheEntry(fragment, DateTime.UtcNow);
|
var character = (Character*)playerPointer;
|
||||||
PruneCharacterCacheIfNeeded();
|
|
||||||
|
|
||||||
return fragment;
|
if (character == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var gameObject = &character->GameObject;
|
||||||
|
if (gameObject == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return gameObject->DrawObject == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PruneCharacterCacheIfNeeded()
|
private async Task<CharacterDataFragment> CreateCharacterData(GameObjectHandler playerRelatedObject, CancellationToken ct)
|
||||||
{
|
|
||||||
if (_characterBuildCache.Count < 2048) return;
|
|
||||||
|
|
||||||
var cutoff = DateTime.UtcNow - TimeSpan.FromSeconds(10);
|
|
||||||
foreach (var kv in _characterBuildCache)
|
|
||||||
{
|
|
||||||
if (kv.Value.CreatedUtc < cutoff)
|
|
||||||
_characterBuildCache.TryRemove(kv.Key, out _);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<T> WithCancellation<T>(Task<T> task, CancellationToken ct)
|
|
||||||
=> await task.WaitAsync(ct).ConfigureAwait(false);
|
|
||||||
|
|
||||||
private async Task<CharacterDataFragment> CreateCharacterDataInternal(GameObjectHandler playerRelatedObject, CancellationToken ct)
|
|
||||||
{
|
{
|
||||||
var objectKind = playerRelatedObject.ObjectKind;
|
var objectKind = playerRelatedObject.ObjectKind;
|
||||||
CharacterDataFragment fragment = objectKind == ObjectKind.Player ? new CharacterDataFragmentPlayer() : new();
|
CharacterDataFragment fragment = objectKind == ObjectKind.Player ? new CharacterDataFragmentPlayer() : new();
|
||||||
|
|
||||||
var logDebug = _logger.IsEnabled(LogLevel.Debug);
|
|
||||||
var sw = Stopwatch.StartNew();
|
|
||||||
|
|
||||||
_logger.LogDebug("Building character data for {obj}", playerRelatedObject);
|
_logger.LogDebug("Building character data for {obj}", playerRelatedObject);
|
||||||
|
var logDebug = _logger.IsEnabled(LogLevel.Debug);
|
||||||
|
|
||||||
await EnsureObjectPresentAsync(playerRelatedObject, ct).ConfigureAwait(false);
|
// wait until chara is not drawing and present so nothing spontaneously explodes
|
||||||
ct.ThrowIfCancellationRequested();
|
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: ct).ConfigureAwait(false);
|
||||||
|
int totalWaitTime = 10000;
|
||||||
var waitRecordingTask = _transientResourceManager.WaitForRecording(ct);
|
while (!await _dalamudUtil.IsObjectPresentAsync(await _dalamudUtil.CreateGameObjectAsync(playerRelatedObject.Address).ConfigureAwait(false)).ConfigureAwait(false) && totalWaitTime > 0)
|
||||||
|
|
||||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: ct)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
// get all remaining paths and resolve them
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
if (await CheckForNullDrawObject(playerRelatedObject.Address).ConfigureAwait(false))
|
|
||||||
throw new InvalidOperationException("DrawObject became null during build (actor despawned)");
|
|
||||||
|
|
||||||
Task<string> getGlamourerData = _ipcManager.Glamourer.GetCharacterCustomizationAsync(playerRelatedObject.Address);
|
|
||||||
Task<string?> getCustomizeData = _ipcManager.CustomizePlus.GetScaleAsync(playerRelatedObject.Address);
|
|
||||||
Task<string?>? getMoodlesData = null;
|
|
||||||
Task<string>? getHeelsOffset = null;
|
|
||||||
Task<string>? getHonorificTitle = null;
|
|
||||||
|
|
||||||
if (objectKind == ObjectKind.Player)
|
|
||||||
{
|
{
|
||||||
getHeelsOffset = _ipcManager.Heels.GetOffsetAsync();
|
_logger.LogTrace("Character is null but it shouldn't be, waiting");
|
||||||
getHonorificTitle = _ipcManager.Honorific.GetTitle();
|
await Task.Delay(50, ct).ConfigureAwait(false);
|
||||||
getMoodlesData = _ipcManager.Moodles.GetStatusAsync(playerRelatedObject.Address);
|
totalWaitTime -= 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
Guid penumbraRequestId = Guid.Empty;
|
|
||||||
Stopwatch? penumbraSw = null;
|
|
||||||
if (logDebug)
|
|
||||||
{
|
|
||||||
penumbraRequestId = Guid.NewGuid();
|
|
||||||
penumbraSw = Stopwatch.StartNew();
|
|
||||||
_logger.LogDebug("Penumbra GetCharacterData start {id} for {obj}", penumbraRequestId, playerRelatedObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
var resolvedPaths = await _ipcManager.Penumbra.GetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (logDebug)
|
|
||||||
{
|
|
||||||
penumbraSw!.Stop();
|
|
||||||
_logger.LogDebug("Penumbra GetCharacterData done {id} in {elapsedMs}ms (count={count})",
|
|
||||||
penumbraRequestId,
|
|
||||||
penumbraSw.ElapsedMilliseconds,
|
|
||||||
resolvedPaths?.Count ?? -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resolvedPaths == null)
|
|
||||||
throw new InvalidOperationException("Penumbra returned null data; couldn't proceed with character");
|
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var staticBuildTask = Task.Run(() => BuildStaticReplacements(resolvedPaths), ct);
|
DateTime start = DateTime.UtcNow;
|
||||||
|
|
||||||
fragment.FileReplacements = await staticBuildTask.ConfigureAwait(false);
|
// penumbra call, it's currently broken
|
||||||
|
Dictionary<string, HashSet<string>>? resolvedPaths;
|
||||||
|
|
||||||
|
resolvedPaths = (await _ipcManager.Penumbra.GetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false));
|
||||||
|
if (resolvedPaths == null) throw new InvalidOperationException("Penumbra returned null data");
|
||||||
|
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
fragment.FileReplacements =
|
||||||
|
new HashSet<FileReplacement>(resolvedPaths.Select(c => new FileReplacement([.. c.Value], c.Key)), FileReplacementComparer.Instance)
|
||||||
|
.Where(p => p.HasFileReplacement).ToHashSet();
|
||||||
|
fragment.FileReplacements.RemoveWhere(c => c.GamePaths.Any(g => !CacheMonitor.AllowedFileExtensions.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase))));
|
||||||
|
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
if (logDebug)
|
if (logDebug)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("== Static Replacements ==");
|
_logger.LogDebug("== Static Replacements ==");
|
||||||
foreach (var replacement in fragment.FileReplacements
|
foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase))
|
||||||
.Where(i => i.HasFileReplacement)
|
|
||||||
.OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
_logger.LogDebug("=> {repl}", replacement);
|
_logger.LogDebug("=> {repl}", replacement);
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
var staticReplacements = new HashSet<FileReplacement>(fragment.FileReplacements, FileReplacementComparer.Instance);
|
|
||||||
|
|
||||||
var transientTask = ResolveTransientReplacementsAsync(
|
|
||||||
playerRelatedObject,
|
|
||||||
objectKind,
|
|
||||||
staticReplacements,
|
|
||||||
waitRecordingTask,
|
|
||||||
ct);
|
|
||||||
|
|
||||||
fragment.GlamourerString = await getGlamourerData.ConfigureAwait(false);
|
|
||||||
_logger.LogDebug("Glamourer is now: {data}", fragment.GlamourerString);
|
|
||||||
|
|
||||||
var customizeScale = await getCustomizeData.ConfigureAwait(false);
|
|
||||||
fragment.CustomizePlusScale = customizeScale ?? string.Empty;
|
|
||||||
_logger.LogDebug("Customize is now: {data}", fragment.CustomizePlusScale);
|
|
||||||
|
|
||||||
if (objectKind == ObjectKind.Player)
|
|
||||||
{
|
{
|
||||||
CharacterDataFragmentPlayer? playerFragment = fragment as CharacterDataFragmentPlayer ?? throw new InvalidOperationException("Failed to cast CharacterDataFragment to Player variant");
|
foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement))
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
playerFragment.ManipulationString = _ipcManager.Penumbra.GetMetaManipulations();
|
await _transientResourceManager.WaitForRecording(ct).ConfigureAwait(false);
|
||||||
playerFragment.HonorificData = await getHonorificTitle!.ConfigureAwait(false);
|
|
||||||
_logger.LogDebug("Honorific is now: {data}", playerFragment!.HonorificData);
|
|
||||||
|
|
||||||
playerFragment.PetNamesData = _ipcManager.PetNames.GetLocalNames();
|
// if it's pet then it's summoner, if it's summoner we actually want to keep all filereplacements alive at all times
|
||||||
_logger.LogDebug("Pet Nicknames is now: {petnames}", playerFragment!.PetNamesData);
|
// or we get into redraw city for every change and nothing works properly
|
||||||
|
if (objectKind == ObjectKind.Pet)
|
||||||
|
{
|
||||||
|
foreach (var item in fragment.FileReplacements.Where(i => i.HasFileReplacement).SelectMany(p => p.GamePaths))
|
||||||
|
{
|
||||||
|
if (_transientResourceManager.AddTransientResource(objectKind, item))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Marking static {item} for Pet as transient", item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
playerFragment.HeelsData = await getHeelsOffset!.ConfigureAwait(false);
|
_logger.LogTrace("Clearing {count} Static Replacements for Pet", fragment.FileReplacements.Count);
|
||||||
_logger.LogDebug("Heels is now: {heels}", playerFragment!.HeelsData);
|
fragment.FileReplacements.Clear();
|
||||||
|
|
||||||
playerFragment.MoodlesData = (await getMoodlesData!.ConfigureAwait(false)) ?? string.Empty;
|
|
||||||
_logger.LogDebug("Moodles is now: {moodles}", playerFragment!.MoodlesData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var (resolvedTransientPaths, clearedForPet) = await transientTask.ConfigureAwait(false);
|
_logger.LogDebug("Handling transient update for {obj}", playerRelatedObject);
|
||||||
if (clearedForPet != null)
|
|
||||||
fragment.FileReplacements.Clear();
|
// remove all potentially gathered paths from the transient resource manager that are resolved through static resolving
|
||||||
|
_transientResourceManager.ClearTransientPaths(objectKind, fragment.FileReplacements.SelectMany(c => c.GamePaths).ToList());
|
||||||
|
|
||||||
|
// get all remaining paths and resolve them
|
||||||
|
var transientPaths = ManageSemiTransientData(objectKind);
|
||||||
|
var resolvedTransientPaths = await GetFileReplacementsFromPaths(transientPaths, new HashSet<string>(StringComparer.Ordinal)).ConfigureAwait(false);
|
||||||
|
|
||||||
if (logDebug)
|
if (logDebug)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("== Transient Replacements ==");
|
_logger.LogDebug("== Transient Replacements ==");
|
||||||
foreach (var replacement in resolvedTransientPaths
|
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal))
|
||||||
.Select(c => new FileReplacement([.. c.Value], c.Key))
|
|
||||||
.OrderBy(f => f.ResolvedPath, StringComparer.Ordinal))
|
|
||||||
{
|
{
|
||||||
_logger.LogDebug("=> {repl}", replacement);
|
_logger.LogDebug("=> {repl}", replacement);
|
||||||
fragment.FileReplacements.Add(replacement);
|
fragment.FileReplacements.Add(replacement);
|
||||||
@@ -328,64 +208,85 @@ public class PlayerDataFactory
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)))
|
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)))
|
||||||
|
{
|
||||||
fragment.FileReplacements.Add(replacement);
|
fragment.FileReplacements.Add(replacement);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clean up all semi transient resources that don't have any file replacement (aka null resolve)
|
||||||
_transientResourceManager.CleanUpSemiTransientResources(objectKind, [.. fragment.FileReplacements]);
|
_transientResourceManager.CleanUpSemiTransientResources(objectKind, [.. fragment.FileReplacements]);
|
||||||
|
|
||||||
fragment.FileReplacements = new HashSet<FileReplacement>(
|
ct.ThrowIfCancellationRequested();
|
||||||
fragment.FileReplacements
|
|
||||||
.Where(v => v.HasFileReplacement)
|
// make sure we only return data that actually has file replacements
|
||||||
.OrderBy(v => v.ResolvedPath, StringComparer.Ordinal),
|
fragment.FileReplacements = new HashSet<FileReplacement>(fragment.FileReplacements.Where(v => v.HasFileReplacement).OrderBy(v => v.ResolvedPath, StringComparer.Ordinal), FileReplacementComparer.Instance);
|
||||||
FileReplacementComparer.Instance);
|
|
||||||
|
// gather up data from ipc
|
||||||
|
Task<string> getHeelsOffset = _ipcManager.Heels.GetOffsetAsync();
|
||||||
|
Task<string> getGlamourerData = _ipcManager.Glamourer.GetCharacterCustomizationAsync(playerRelatedObject.Address);
|
||||||
|
Task<string?> getCustomizeData = _ipcManager.CustomizePlus.GetScaleAsync(playerRelatedObject.Address);
|
||||||
|
Task<string> getHonorificTitle = _ipcManager.Honorific.GetTitle();
|
||||||
|
fragment.GlamourerString = await getGlamourerData.ConfigureAwait(false);
|
||||||
|
_logger.LogDebug("Glamourer is now: {data}", fragment.GlamourerString);
|
||||||
|
var customizeScale = await getCustomizeData.ConfigureAwait(false);
|
||||||
|
fragment.CustomizePlusScale = customizeScale ?? string.Empty;
|
||||||
|
_logger.LogDebug("Customize is now: {data}", fragment.CustomizePlusScale);
|
||||||
|
|
||||||
|
if (objectKind == ObjectKind.Player)
|
||||||
|
{
|
||||||
|
var playerFragment = (fragment as CharacterDataFragmentPlayer)!;
|
||||||
|
playerFragment.ManipulationString = _ipcManager.Penumbra.GetMetaManipulations();
|
||||||
|
|
||||||
|
playerFragment!.HonorificData = await getHonorificTitle.ConfigureAwait(false);
|
||||||
|
_logger.LogDebug("Honorific is now: {data}", playerFragment!.HonorificData);
|
||||||
|
|
||||||
|
playerFragment!.HeelsData = await getHeelsOffset.ConfigureAwait(false);
|
||||||
|
_logger.LogDebug("Heels is now: {heels}", playerFragment!.HeelsData);
|
||||||
|
|
||||||
|
playerFragment!.MoodlesData = await _ipcManager.Moodles.GetStatusAsync(playerRelatedObject.Address).ConfigureAwait(false) ?? string.Empty;
|
||||||
|
_logger.LogDebug("Moodles is now: {moodles}", playerFragment!.MoodlesData);
|
||||||
|
|
||||||
|
playerFragment!.PetNamesData = _ipcManager.PetNames.GetLocalNames();
|
||||||
|
_logger.LogDebug("Pet Nicknames is now: {petnames}", playerFragment!.PetNamesData);
|
||||||
|
}
|
||||||
|
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var toCompute = fragment.FileReplacements.Where(f => !f.IsFileSwap).ToArray();
|
var toCompute = fragment.FileReplacements.Where(f => !f.IsFileSwap).ToArray();
|
||||||
_logger.LogDebug("Getting Hashes for {amount} Files", toCompute.Length);
|
_logger.LogDebug("Getting Hashes for {amount} Files", toCompute.Length);
|
||||||
|
var computedPaths = _fileCacheManager.GetFileCachesByPaths(toCompute.Select(c => c.ResolvedPath).ToArray());
|
||||||
await Task.Run(() =>
|
foreach (var file in toCompute)
|
||||||
{
|
{
|
||||||
var computedPaths = _fileCacheManager.GetFileCachesByPaths([.. toCompute.Select(c => c.ResolvedPath)]);
|
ct.ThrowIfCancellationRequested();
|
||||||
foreach (var file in toCompute)
|
file.Hash = computedPaths[file.ResolvedPath]?.Hash ?? string.Empty;
|
||||||
{
|
}
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
file.Hash = computedPaths[file.ResolvedPath]?.Hash ?? string.Empty;
|
|
||||||
}
|
|
||||||
}, ct).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var removed = fragment.FileReplacements.RemoveWhere(f => !f.IsFileSwap && string.IsNullOrEmpty(f.Hash));
|
var removed = fragment.FileReplacements.RemoveWhere(f => !f.IsFileSwap && string.IsNullOrEmpty(f.Hash));
|
||||||
if (removed > 0)
|
if (removed > 0)
|
||||||
|
{
|
||||||
_logger.LogDebug("Removed {amount} of invalid files", removed);
|
_logger.LogDebug("Removed {amount} of invalid files", removed);
|
||||||
|
}
|
||||||
|
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
Dictionary<string, List<ushort>>? boneIndices = null;
|
Dictionary<string, List<ushort>>? boneIndices = null;
|
||||||
var hasPapFiles = false;
|
var hasPapFiles = false;
|
||||||
|
|
||||||
if (objectKind == ObjectKind.Player)
|
if (objectKind == ObjectKind.Player)
|
||||||
{
|
{
|
||||||
hasPapFiles = fragment.FileReplacements.Any(f =>
|
hasPapFiles = fragment.FileReplacements.Any(f =>
|
||||||
!f.IsFileSwap && f.GamePaths.Any(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)));
|
!f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
if (hasPapFiles)
|
if (hasPapFiles)
|
||||||
{
|
{
|
||||||
boneIndices = await _dalamudUtil
|
boneIndices = await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject)).ConfigureAwait(false);
|
||||||
.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject))
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (objectKind == ObjectKind.Player)
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
#if DEBUG
|
|
||||||
if (hasPapFiles && boneIndices != null)
|
|
||||||
_modelAnalyzer.DumpLocalSkeletonIndices(playerRelatedObject);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (hasPapFiles)
|
if (hasPapFiles)
|
||||||
{
|
{
|
||||||
await VerifyPlayerAnimationBones(boneIndices, (CharacterDataFragmentPlayer)fragment, ct)
|
await VerifyPlayerAnimationBones(boneIndices, (fragment as CharacterDataFragmentPlayer)!, ct).ConfigureAwait(false);
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException e)
|
catch (OperationCanceledException e)
|
||||||
@@ -399,320 +300,109 @@ public class PlayerDataFactory
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Building character data for {obj} took {time}ms",
|
_logger.LogInformation("Building character data for {obj} took {time}ms", objectKind, TimeSpan.FromTicks(DateTime.UtcNow.Ticks - start.Ticks).TotalMilliseconds);
|
||||||
objectKind, sw.Elapsed.TotalMilliseconds);
|
|
||||||
|
|
||||||
return fragment;
|
return fragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task EnsureObjectPresentAsync(GameObjectHandler handler, CancellationToken ct)
|
private async Task VerifyPlayerAnimationBones(Dictionary<string, List<ushort>>? boneIndices, CharacterDataFragmentPlayer fragment, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var remaining = 10000;
|
if (boneIndices == null) return;
|
||||||
while (remaining > 0)
|
|
||||||
{
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
var obj = await _dalamudUtil.CreateGameObjectAsync(handler.Address).ConfigureAwait(false);
|
|
||||||
if (await _dalamudUtil.IsObjectPresentAsync(obj).ConfigureAwait(false))
|
|
||||||
return;
|
|
||||||
|
|
||||||
_logger.LogTrace("Character is null but it shouldn't be, waiting");
|
|
||||||
await Task.Delay(50, ct).ConfigureAwait(false);
|
|
||||||
remaining -= 50;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static HashSet<FileReplacement> BuildStaticReplacements(Dictionary<string, HashSet<string>> resolvedPaths)
|
|
||||||
{
|
|
||||||
var set = new HashSet<FileReplacement>(FileReplacementComparer.Instance);
|
|
||||||
|
|
||||||
foreach (var kvp in resolvedPaths)
|
|
||||||
{
|
|
||||||
var fr = new FileReplacement([.. kvp.Value], kvp.Key);
|
|
||||||
if (!fr.HasFileReplacement) continue;
|
|
||||||
|
|
||||||
var allAllowed = fr.GamePaths.All(g =>
|
|
||||||
CacheMonitor.AllowedFileExtensions.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase)));
|
|
||||||
|
|
||||||
if (!allAllowed) continue;
|
|
||||||
|
|
||||||
set.Add(fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
return set;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<(IReadOnlyDictionary<string, string[]> ResolvedPaths, HashSet<FileReplacement>? ClearedReplacements)>
|
|
||||||
ResolveTransientReplacementsAsync(
|
|
||||||
GameObjectHandler obj,
|
|
||||||
ObjectKind objectKind,
|
|
||||||
HashSet<FileReplacement> staticReplacements,
|
|
||||||
Task waitRecordingTask,
|
|
||||||
CancellationToken ct)
|
|
||||||
{
|
|
||||||
await waitRecordingTask.ConfigureAwait(false);
|
|
||||||
|
|
||||||
HashSet<FileReplacement>? clearedReplacements = null;
|
|
||||||
|
|
||||||
if (objectKind == ObjectKind.Pet)
|
|
||||||
{
|
|
||||||
foreach (var item in staticReplacements.Where(i => i.HasFileReplacement).SelectMany(p => p.GamePaths))
|
|
||||||
{
|
|
||||||
if (_transientResourceManager.AddTransientResource(objectKind, item))
|
|
||||||
_logger.LogDebug("Marking static {item} for Pet as transient", item);
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogTrace("Clearing {count} Static Replacements for Pet", staticReplacements.Count);
|
|
||||||
clearedReplacements = staticReplacements;
|
|
||||||
}
|
|
||||||
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
_transientResourceManager.ClearTransientPaths(objectKind, [.. staticReplacements.SelectMany(c => c.GamePaths)]);
|
|
||||||
|
|
||||||
var transientPaths = ManageSemiTransientData(objectKind);
|
|
||||||
if (transientPaths.Count == 0)
|
|
||||||
return (new Dictionary<string, string[]>(StringComparer.Ordinal), clearedReplacements);
|
|
||||||
|
|
||||||
var resolved = await GetFileReplacementsFromPaths(transientPaths, new HashSet<string>(StringComparer.Ordinal))
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (_maxTransientResolvedEntries > 0 && resolved.Count > _maxTransientResolvedEntries)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Transient entries ({resolved}) are above the threshold {max}; Please consider disable some mods (VFX have heavy load) to reduce transient load",
|
|
||||||
resolved.Count,
|
|
||||||
_maxTransientResolvedEntries);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (resolved, clearedReplacements);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private async Task VerifyPlayerAnimationBones(
|
|
||||||
Dictionary<string, List<ushort>>? playerBoneIndices,
|
|
||||||
CharacterDataFragmentPlayer fragment,
|
|
||||||
CancellationToken ct)
|
|
||||||
{
|
|
||||||
var mode = _configService.Current.AnimationValidationMode;
|
|
||||||
var allowBasedShift = _configService.Current.AnimationAllowOneBasedShift;
|
|
||||||
var allownNightIndex = _configService.Current.AnimationAllowNeighborIndexTolerance;
|
|
||||||
|
|
||||||
if (mode == AnimationValidationMode.Unsafe)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (playerBoneIndices == null || playerBoneIndices.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var localBoneSets = new Dictionary<string, HashSet<ushort>>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
foreach (var (rawLocalKey, indices) in playerBoneIndices)
|
|
||||||
{
|
|
||||||
if (indices is not { Count: > 0 })
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var key = XivDataAnalyzer.CanonicalizeSkeletonKey(rawLocalKey);
|
|
||||||
if (string.IsNullOrEmpty(key))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!localBoneSets.TryGetValue(key, out var set))
|
|
||||||
localBoneSets[key] = set = [];
|
|
||||||
|
|
||||||
foreach (var idx in indices)
|
|
||||||
set.Add(idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (localBoneSets.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
if (_logger.IsEnabled(LogLevel.Debug))
|
||||||
{
|
{
|
||||||
_logger.LogDebug("SEND local buckets: {b}",
|
foreach (var kvp in boneIndices)
|
||||||
string.Join(", ", localBoneSets.Keys.Order(StringComparer.Ordinal)));
|
|
||||||
|
|
||||||
foreach (var kvp in localBoneSets.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
var min = kvp.Value.Count > 0 ? kvp.Value.Min() : 0;
|
_logger.LogDebug("Found {skellyname} ({idx} bone indices) on player: {bones}", kvp.Key, kvp.Value.Any() ? kvp.Value.Max() : 0, string.Join(',', kvp.Value));
|
||||||
var max = kvp.Value.Count > 0 ? kvp.Value.Max() : 0;
|
|
||||||
_logger.LogDebug("Local bucket {bucket}: count={count} min={min} max={max}",
|
|
||||||
kvp.Key, kvp.Value.Count, min, max);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var papGroups = fragment.FileReplacements
|
var maxPlayerBoneIndex = boneIndices.SelectMany(kvp => kvp.Value).DefaultIfEmpty().Max();
|
||||||
.Where(f => !f.IsFileSwap
|
if (maxPlayerBoneIndex <= 0) return;
|
||||||
&& !string.IsNullOrEmpty(f.Hash)
|
|
||||||
&& f.GamePaths is { Count: > 0 }
|
|
||||||
&& f.GamePaths.Any(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)))
|
|
||||||
.GroupBy(f => f.Hash!, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
int noValidationFailed = 0;
|
int noValidationFailed = 0;
|
||||||
|
foreach (var file in fragment.FileReplacements.Where(f => !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase)).ToList())
|
||||||
foreach (var g in papGroups)
|
|
||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var hash = g.Key;
|
var skeletonIndices = await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetBoneIndicesFromPap(file.Hash)).ConfigureAwait(false);
|
||||||
|
bool validationFailed = false;
|
||||||
var resolvedPath = g.Select(f => f.ResolvedPath).Distinct(StringComparer.OrdinalIgnoreCase);
|
if (skeletonIndices != null)
|
||||||
var papPathSummary = string.Join(", ", resolvedPath);
|
|
||||||
if (papPathSummary.IsNullOrEmpty())
|
|
||||||
papPathSummary = "<unknown pap path>";
|
|
||||||
|
|
||||||
Dictionary<string, List<ushort>>? papIndices = null;
|
|
||||||
|
|
||||||
await _papParseLimiter.WaitAsync(ct).ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var cacheEntity = _fileCacheManager.GetFileCacheByHash(hash);
|
// 105 is the maximum vanilla skellington spoopy bone index
|
||||||
var papPath = cacheEntity?.ResolvedFilepath;
|
if (skeletonIndices.All(k => k.Value.Max() <= 105))
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(papPath) && File.Exists(papPath))
|
|
||||||
{
|
{
|
||||||
var havokBytes = await Task.Run(() => XivDataAnalyzer.ReadHavokBytesFromPap(papPath), ct)
|
_logger.LogTrace("All indices of {path} are <= 105, ignoring", file.ResolvedPath);
|
||||||
.ConfigureAwait(false);
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (havokBytes is { Length: > 8 })
|
_logger.LogDebug("Verifying bone indices for {path}, found {x} skeletons", file.ResolvedPath, skeletonIndices.Count);
|
||||||
|
|
||||||
|
foreach (var boneCount in skeletonIndices)
|
||||||
|
{
|
||||||
|
var maxAnimationIndex = boneCount.Value.DefaultIfEmpty().Max();
|
||||||
|
if (maxAnimationIndex > maxPlayerBoneIndex)
|
||||||
{
|
{
|
||||||
papIndices = await _dalamudUtil.RunOnFrameworkThread(
|
_logger.LogWarning("Found more bone indices on the animation {path} skeleton {skl} (max indice {idx}) than on any player related skeleton (max indice {idx2})",
|
||||||
() => _modelAnalyzer.ParseHavokBytesOnFrameworkThread(havokBytes, hash, persistToConfig: false))
|
file.ResolvedPath, boneCount.Key, maxAnimationIndex, maxPlayerBoneIndex);
|
||||||
.ConfigureAwait(false);
|
validationFailed = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
|
if (validationFailed)
|
||||||
{
|
{
|
||||||
_papParseLimiter.Release();
|
noValidationFailed++;
|
||||||
}
|
_logger.LogDebug("Removing {file} from sent file replacements and transient data", file.ResolvedPath);
|
||||||
|
fragment.FileReplacements.Remove(file);
|
||||||
if (papIndices == null || papIndices.Count == 0)
|
foreach (var gamePath in file.GamePaths)
|
||||||
continue;
|
|
||||||
|
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var papBuckets = papIndices
|
_transientResourceManager.RemoveTransientResource(ObjectKind.Player, gamePath);
|
||||||
.Where(kvp => kvp.Value is { Count: > 0 })
|
|
||||||
.Select(kvp => new
|
|
||||||
{
|
|
||||||
Raw = kvp.Key,
|
|
||||||
Key = XivDataAnalyzer.CanonicalizeSkeletonKey(kvp.Key),
|
|
||||||
Indices = kvp.Value
|
|
||||||
})
|
|
||||||
.Where(x => x.Indices is { Count: > 0 })
|
|
||||||
.GroupBy(x => string.IsNullOrEmpty(x.Key) ? x.Raw : x.Key!, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.Select(grp =>
|
|
||||||
{
|
|
||||||
var all = grp.SelectMany(v => v.Indices).ToList();
|
|
||||||
var min = all.Count > 0 ? all.Min() : 0;
|
|
||||||
var max = all.Count > 0 ? all.Max() : 0;
|
|
||||||
var raws = string.Join(',', grp.Select(v => v.Raw).Distinct(StringComparer.OrdinalIgnoreCase));
|
|
||||||
return $"{grp.Key}(min={min},max={max},raw=[{raws}])";
|
|
||||||
})
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
_logger.LogDebug("SEND pap buckets for hash={hash}: {b}",
|
|
||||||
hash,
|
|
||||||
string.Join(" | ", papBuckets));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogDebug(ex, "Error logging PAP bucket details for hash={hash}", hash);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isCompatible = false;
|
|
||||||
string reason = string.Empty;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
isCompatible = XivDataAnalyzer.IsPapCompatible(localBoneSets, papIndices, mode, allowBasedShift, allownNightIndex, out reason);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Error checking PAP compatibility for hash={hash}, path={path}. Treating as incompatible.", hash, papPathSummary);
|
|
||||||
reason = $"Exception during compatibility check: {ex.Message}";
|
|
||||||
isCompatible = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCompatible)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
noValidationFailed++;
|
|
||||||
|
|
||||||
_logger.LogWarning(
|
|
||||||
"Animation PAP is not compatible with local skeletons; dropping mappings for {papPath}. Reason: {reason}",
|
|
||||||
papPathSummary,
|
|
||||||
reason);
|
|
||||||
|
|
||||||
var removedGamePaths = fragment.FileReplacements
|
|
||||||
.Where(fr => !fr.IsFileSwap
|
|
||||||
&& string.Equals(fr.Hash, hash, StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& fr.GamePaths.Any(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)))
|
|
||||||
.SelectMany(fr => fr.GamePaths.Where(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)))
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
fragment.FileReplacements.RemoveWhere(fr =>
|
|
||||||
!fr.IsFileSwap
|
|
||||||
&& string.Equals(fr.Hash, hash, StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& fr.GamePaths.Any(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)));
|
|
||||||
|
|
||||||
foreach (var gp in removedGamePaths)
|
|
||||||
_transientResourceManager.RemoveTransientResource(ObjectKind.Player, gp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (noValidationFailed > 0)
|
if (noValidationFailed > 0)
|
||||||
{
|
{
|
||||||
_lightlessMediator.Publish(new NotificationMessage(
|
_lightlessMediator.Publish(new NotificationMessage("Invalid Skeleton Setup",
|
||||||
"Invalid Skeleton Setup",
|
$"Your client is attempting to send {noValidationFailed} animation files with invalid bone data. Those animation files have been removed from your sent data. " +
|
||||||
$"Your client is attempting to send {noValidationFailed} animation files that don't match your current skeleton validation mode ({mode}). " +
|
$"Verify that you are using the correct skeleton for those animation files (Check /xllog for more information).",
|
||||||
"Please adjust your skeleton/mods or change the validation mode if this is unexpected. " +
|
NotificationType.Warning, TimeSpan.FromSeconds(10)));
|
||||||
"Those animation files have been removed from your sent (player) data. (Check /xllog for details).",
|
|
||||||
NotificationType.Warning,
|
|
||||||
TimeSpan.FromSeconds(10)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<IReadOnlyDictionary<string, string[]>> GetFileReplacementsFromPaths(HashSet<string> forwardResolve, HashSet<string> reverseResolve)
|
||||||
private async Task<IReadOnlyDictionary<string, string[]>> GetFileReplacementsFromPaths(
|
|
||||||
HashSet<string> forwardResolve,
|
|
||||||
HashSet<string> reverseResolve)
|
|
||||||
{
|
{
|
||||||
var forwardPaths = forwardResolve.ToArray();
|
var forwardPaths = forwardResolve.ToArray();
|
||||||
var reversePaths = reverseResolve.ToArray();
|
var reversePaths = reverseResolve.ToArray();
|
||||||
if (forwardPaths.Length == 0 && reversePaths.Length == 0)
|
Dictionary<string, List<string>> resolvedPaths = new(StringComparer.Ordinal);
|
||||||
{
|
|
||||||
return new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase).AsReadOnly();
|
|
||||||
}
|
|
||||||
|
|
||||||
var forwardPathsLower = forwardPaths.Length == 0 ? [] : forwardPaths.Select(p => p.ToLowerInvariant()).ToArray();
|
|
||||||
var reversePathsLower = reversePaths.Length == 0 ? [] : reversePaths.Select(p => p.ToLowerInvariant()).ToArray();
|
|
||||||
|
|
||||||
Dictionary<string, List<string>> resolvedPaths = new(forwardPaths.Length + reversePaths.Length, StringComparer.Ordinal);
|
|
||||||
var (forward, reverse) = await _ipcManager.Penumbra.ResolvePathsAsync(forwardPaths, reversePaths).ConfigureAwait(false);
|
var (forward, reverse) = await _ipcManager.Penumbra.ResolvePathsAsync(forwardPaths, reversePaths).ConfigureAwait(false);
|
||||||
|
|
||||||
for (int i = 0; i < forwardPaths.Length; i++)
|
for (int i = 0; i < forwardPaths.Length; i++)
|
||||||
{
|
{
|
||||||
var filePath = forward[i].ToLowerInvariant();
|
var filePath = forward[i].ToLowerInvariant();
|
||||||
if (resolvedPaths.TryGetValue(filePath, out var list))
|
if (resolvedPaths.TryGetValue(filePath, out var list))
|
||||||
|
{
|
||||||
list.Add(forwardPaths[i].ToLowerInvariant());
|
list.Add(forwardPaths[i].ToLowerInvariant());
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
resolvedPaths[filePath] = [forwardPaths[i].ToLowerInvariant()];
|
resolvedPaths[filePath] = [forwardPaths[i].ToLowerInvariant()];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < reversePaths.Length; i++)
|
for (int i = 0; i < reversePaths.Length; i++)
|
||||||
{
|
{
|
||||||
var filePath = reversePathsLower[i];
|
var filePath = reversePaths[i].ToLowerInvariant();
|
||||||
var reverseResolvedLower = new string[reverse[i].Length];
|
|
||||||
for (var j = 0; j < reverseResolvedLower.Length; j++)
|
|
||||||
{
|
|
||||||
reverseResolvedLower[j] = reverse[i][j].ToLowerInvariant();
|
|
||||||
}
|
|
||||||
if (resolvedPaths.TryGetValue(filePath, out var list))
|
if (resolvedPaths.TryGetValue(filePath, out var list))
|
||||||
|
{
|
||||||
list.AddRange(reverse[i].Select(c => c.ToLowerInvariant()));
|
list.AddRange(reverse[i].Select(c => c.ToLowerInvariant()));
|
||||||
|
}
|
||||||
else
|
else
|
||||||
resolvedPaths[filePath] = [.. reverse[i].Select(c => c.ToLowerInvariant()).ToList()];
|
{
|
||||||
|
resolvedPaths[filePath] = new List<string>(reverse[i].Select(c => c.ToLowerInvariant()).ToList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolvedPaths.ToDictionary(k => k.Key, k => k.Value.ToArray(), StringComparer.OrdinalIgnoreCase).AsReadOnly();
|
return resolvedPaths.ToDictionary(k => k.Key, k => k.Value.ToArray(), StringComparer.OrdinalIgnoreCase).AsReadOnly();
|
||||||
@@ -723,29 +413,11 @@ public class PlayerDataFactory
|
|||||||
_transientResourceManager.PersistTransientResources(objectKind);
|
_transientResourceManager.PersistTransientResources(objectKind);
|
||||||
|
|
||||||
HashSet<string> pathsToResolve = new(StringComparer.Ordinal);
|
HashSet<string> pathsToResolve = new(StringComparer.Ordinal);
|
||||||
|
foreach (var path in _transientResourceManager.GetSemiTransientResources(objectKind).Where(path => !string.IsNullOrEmpty(path)))
|
||||||
int scanned = 0, skippedEmpty = 0, skippedVfx = 0;
|
|
||||||
|
|
||||||
foreach (var path in _transientResourceManager.GetSemiTransientResources(objectKind))
|
|
||||||
{
|
{
|
||||||
scanned++;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(path))
|
|
||||||
{
|
|
||||||
skippedEmpty++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
pathsToResolve.Add(path);
|
pathsToResolve.Add(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
|
||||||
{
|
|
||||||
_logger.LogDebug(
|
|
||||||
"ManageSemiTransientData({kind}): scanned={scanned}, added={added}, skippedEmpty={skippedEmpty}, skippedVfx={skippedVfx}",
|
|
||||||
objectKind, scanned, pathsToResolve.Count, skippedEmpty, skippedVfx);
|
|
||||||
}
|
|
||||||
|
|
||||||
return pathsToResolve;
|
return pathsToResolve;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,12 +2,11 @@
|
|||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Utils;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using static FFXIVClientStructs.FFXIV.Client.Game.Character.DrawDataContainer;
|
using static FFXIVClientStructs.FFXIV.Client.Game.Character.DrawDataContainer;
|
||||||
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
|
||||||
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
|
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
|
||||||
|
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||||
|
|
||||||
namespace LightlessSync.PlayerData.Handlers;
|
namespace LightlessSync.PlayerData.Handlers;
|
||||||
|
|
||||||
@@ -17,8 +16,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
private readonly Func<IntPtr> _getAddress;
|
private readonly Func<IntPtr> _getAddress;
|
||||||
private readonly bool _isOwnedObject;
|
private readonly bool _isOwnedObject;
|
||||||
private readonly PerformanceCollectorService _performanceCollector;
|
private readonly PerformanceCollectorService _performanceCollector;
|
||||||
private readonly object _frameworkUpdateGate = new();
|
|
||||||
private bool _frameworkUpdateSubscribed;
|
|
||||||
private byte _classJob = 0;
|
private byte _classJob = 0;
|
||||||
private Task? _delayedZoningTask;
|
private Task? _delayedZoningTask;
|
||||||
private bool _haltProcessing = false;
|
private bool _haltProcessing = false;
|
||||||
@@ -50,10 +47,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_isOwnedObject)
|
Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => FrameworkUpdate());
|
||||||
{
|
|
||||||
EnableFrameworkUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
Mediator.Subscribe<ZoneSwitchEndMessage>(this, (_) => ZoneSwitchEnd());
|
Mediator.Subscribe<ZoneSwitchEndMessage>(this, (_) => ZoneSwitchEnd());
|
||||||
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (_) => ZoneSwitchStart());
|
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (_) => ZoneSwitchStart());
|
||||||
@@ -114,16 +108,16 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
public async Task ActOnFrameworkAfterEnsureNoDrawAsync(Action<Dalamud.Game.ClientState.Objects.Types.ICharacter> act, CancellationToken token)
|
public async Task ActOnFrameworkAfterEnsureNoDrawAsync(Action<Dalamud.Game.ClientState.Objects.Types.ICharacter> act, CancellationToken token)
|
||||||
{
|
{
|
||||||
while (await _dalamudUtil.RunOnFrameworkThread(() =>
|
while (await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
{
|
{
|
||||||
EnsureLatestObjectState();
|
if (_haltProcessing) CheckAndUpdateObject();
|
||||||
if (CurrentDrawCondition != DrawCondition.None) return true;
|
if (CurrentDrawCondition != DrawCondition.None) return true;
|
||||||
var gameObj = _dalamudUtil.CreateGameObject(Address);
|
var gameObj = _dalamudUtil.CreateGameObject(Address);
|
||||||
if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara)
|
if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara)
|
||||||
{
|
{
|
||||||
act.Invoke(chara);
|
act.Invoke(chara);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}).ConfigureAwait(false))
|
}).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
await Task.Delay(250, token).ConfigureAwait(false);
|
await Task.Delay(250, token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -154,11 +148,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
_haltProcessing = false;
|
_haltProcessing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Refresh()
|
|
||||||
{
|
|
||||||
_dalamudUtil.RunOnFrameworkThread(CheckAndUpdateObject).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> IsBeingDrawnRunOnFrameworkAsync()
|
public async Task<bool> IsBeingDrawnRunOnFrameworkAsync()
|
||||||
{
|
{
|
||||||
return await _dalamudUtil.RunOnFrameworkThread(IsBeingDrawn).ConfigureAwait(false);
|
return await _dalamudUtil.RunOnFrameworkThread(IsBeingDrawn).ConfigureAwait(false);
|
||||||
@@ -170,116 +159,81 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
return $"{owned}/{ObjectKind}:{Name} ({Address:X},{DrawObjectAddress:X})";
|
return $"{owned}/{ObjectKind}:{Name} ({Address:X},{DrawObjectAddress:X})";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CheckAndUpdateObject() => CheckAndUpdateObject(allowPublish: true);
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
|
||||||
private unsafe void CheckAndUpdateObject(bool allowPublish)
|
Mediator.Publish(new GameObjectHandlerDestroyedMessage(this, _isOwnedObject));
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void CheckAndUpdateObject()
|
||||||
{
|
{
|
||||||
var prevAddr = Address;
|
var prevAddr = Address;
|
||||||
var prevDrawObj = DrawObjectAddress;
|
var prevDrawObj = DrawObjectAddress;
|
||||||
string? nameString = null;
|
|
||||||
|
|
||||||
var nextAddr = _getAddress();
|
|
||||||
|
|
||||||
if (nextAddr != IntPtr.Zero && !PtrGuard.LooksLikePtr(nextAddr))
|
|
||||||
{
|
|
||||||
Logger.LogWarning("[{this}] _getAddress returned non-pointer: 0x{addr:X}", this, (ulong)nextAddr);
|
|
||||||
nextAddr = IntPtr.Zero;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextAddr != IntPtr.Zero &&
|
|
||||||
!PtrGuard.IsReadable(nextAddr, (nuint)sizeof(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject)))
|
|
||||||
{
|
|
||||||
Logger.LogWarning("[{this}] Address not readable: 0x{addr:X}", this, (ulong)nextAddr);
|
|
||||||
nextAddr = IntPtr.Zero;
|
|
||||||
}
|
|
||||||
|
|
||||||
Address = nextAddr;
|
|
||||||
|
|
||||||
|
Address = _getAddress();
|
||||||
if (Address != IntPtr.Zero)
|
if (Address != IntPtr.Zero)
|
||||||
{
|
{
|
||||||
var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address;
|
var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address;
|
||||||
|
var drawObjAddr = (IntPtr)gameObject->DrawObject;
|
||||||
var draw = (nint)gameObject->DrawObject;
|
DrawObjectAddress = drawObjAddr;
|
||||||
|
|
||||||
if (!PtrGuard.LooksLikePtr(draw) || !PtrGuard.IsReadable(draw, (nuint)sizeof(DrawObject)))
|
|
||||||
draw = 0;
|
|
||||||
|
|
||||||
DrawObjectAddress = draw;
|
|
||||||
EntityId = gameObject->EntityId;
|
EntityId = gameObject->EntityId;
|
||||||
|
CurrentDrawCondition = DrawCondition.None;
|
||||||
if (PtrGuard.IsReadable(Address, (nuint)sizeof(Character)))
|
|
||||||
{
|
|
||||||
var chara = (Character*)Address;
|
|
||||||
nameString = chara->GameObject.NameString;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(nameString) && !string.Equals(nameString, Name, StringComparison.Ordinal))
|
|
||||||
Name = nameString;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
DrawObjectAddress = IntPtr.Zero;
|
DrawObjectAddress = IntPtr.Zero;
|
||||||
EntityId = uint.MaxValue;
|
EntityId = uint.MaxValue;
|
||||||
|
CurrentDrawCondition = DrawCondition.DrawObjectZero;
|
||||||
}
|
}
|
||||||
|
|
||||||
CurrentDrawCondition = (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero)
|
CurrentDrawCondition = IsBeingDrawnUnsafe();
|
||||||
? IsBeingDrawnUnsafe()
|
|
||||||
: DrawCondition.DrawObjectZero;
|
|
||||||
|
|
||||||
if (_haltProcessing || !allowPublish) return;
|
if (_haltProcessing) return;
|
||||||
|
|
||||||
bool drawObjDiff = DrawObjectAddress != prevDrawObj;
|
bool drawObjDiff = DrawObjectAddress != prevDrawObj;
|
||||||
bool addrDiff = Address != prevAddr;
|
bool addrDiff = Address != prevAddr;
|
||||||
|
|
||||||
if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero
|
if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero)
|
||||||
&& PtrGuard.IsReadable(Address, (nuint)sizeof(Character))
|
|
||||||
&& PtrGuard.IsReadable(DrawObjectAddress, (nuint)sizeof(DrawObject)))
|
|
||||||
{
|
{
|
||||||
var chara = (Character*)Address;
|
var chara = (Character*)Address;
|
||||||
var drawObj = (DrawObject*)DrawObjectAddress;
|
var name = chara->GameObject.NameString;
|
||||||
|
bool nameChange = !string.Equals(name, Name, StringComparison.Ordinal);
|
||||||
var objType = drawObj->Object.GetObjectType();
|
if (nameChange)
|
||||||
var isHuman = objType == ObjectType.CharacterBase
|
{
|
||||||
&& ((CharacterBase*)drawObj)->GetModelType() == CharacterBase.ModelType.Human;
|
Name = name;
|
||||||
|
}
|
||||||
nameString ??= chara->GameObject.NameString;
|
|
||||||
var nameChange = !string.Equals(nameString, Name, StringComparison.Ordinal);
|
|
||||||
if (nameChange) Name = nameString;
|
|
||||||
|
|
||||||
bool equipDiff = false;
|
bool equipDiff = false;
|
||||||
|
|
||||||
if (isHuman)
|
if (((DrawObject*)DrawObjectAddress)->Object.GetObjectType() == ObjectType.CharacterBase
|
||||||
|
&& ((CharacterBase*)DrawObjectAddress)->GetModelType() == CharacterBase.ModelType.Human)
|
||||||
{
|
{
|
||||||
if (PtrGuard.IsReadable(DrawObjectAddress, (nuint)sizeof(Human)))
|
var classJob = chara->CharacterData.ClassJob;
|
||||||
|
if (classJob != _classJob)
|
||||||
{
|
{
|
||||||
var classJob = chara->CharacterData.ClassJob;
|
Logger.LogTrace("[{this}] classjob changed from {old} to {new}", this, _classJob, classJob);
|
||||||
if (classJob != _classJob)
|
_classJob = classJob;
|
||||||
{
|
Mediator.Publish(new ClassJobChangedMessage(this));
|
||||||
Logger.LogTrace("[{this}] classjob changed from {old} to {new}", this, _classJob, classJob);
|
|
||||||
_classJob = classJob;
|
|
||||||
Mediator.Publish(new ClassJobChangedMessage(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
equipDiff = CompareAndUpdateEquipByteData((byte*)&((Human*)drawObj)->Head);
|
|
||||||
|
|
||||||
ref var mh = ref chara->DrawData.Weapon(WeaponSlot.MainHand);
|
|
||||||
ref var oh = ref chara->DrawData.Weapon(WeaponSlot.OffHand);
|
|
||||||
|
|
||||||
equipDiff |= CompareAndUpdateMainHand((Weapon*)mh.DrawObject);
|
|
||||||
equipDiff |= CompareAndUpdateOffHand((Weapon*)oh.DrawObject);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
isHuman = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
equipDiff = CompareAndUpdateEquipByteData((byte*)&((Human*)DrawObjectAddress)->Head);
|
||||||
|
|
||||||
|
ref var mh = ref chara->DrawData.Weapon(WeaponSlot.MainHand);
|
||||||
|
ref var oh = ref chara->DrawData.Weapon(WeaponSlot.OffHand);
|
||||||
|
equipDiff |= CompareAndUpdateMainHand((Weapon*)mh.DrawObject);
|
||||||
|
equipDiff |= CompareAndUpdateOffHand((Weapon*)oh.DrawObject);
|
||||||
|
|
||||||
|
if (equipDiff)
|
||||||
|
Logger.LogTrace("Checking [{this}] equip data as human from draw obj, result: {diff}", this, equipDiff);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
if (!isHuman)
|
|
||||||
{
|
{
|
||||||
equipDiff = CompareAndUpdateEquipByteData((byte*)Unsafe.AsPointer(ref chara->DrawData.EquipmentModelIds[0]));
|
equipDiff = CompareAndUpdateEquipByteData((byte*)Unsafe.AsPointer(ref chara->DrawData.EquipmentModelIds[0]));
|
||||||
|
if (equipDiff)
|
||||||
|
Logger.LogTrace("Checking [{this}] equip data from game obj, result: {diff}", this, equipDiff);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (equipDiff && !_isOwnedObject)
|
if (equipDiff && !_isOwnedObject) // send the message out immediately and cancel out, no reason to continue if not self
|
||||||
{
|
{
|
||||||
Logger.LogTrace("[{this}] Changed", this);
|
Logger.LogTrace("[{this}] Changed", this);
|
||||||
return;
|
return;
|
||||||
@@ -287,13 +241,12 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
|
|
||||||
bool customizeDiff = false;
|
bool customizeDiff = false;
|
||||||
|
|
||||||
if (isHuman && PtrGuard.IsReadable(DrawObjectAddress, (nuint)sizeof(Human)))
|
if (((DrawObject*)DrawObjectAddress)->Object.GetObjectType() == ObjectType.CharacterBase
|
||||||
|
&& ((CharacterBase*)DrawObjectAddress)->GetModelType() == CharacterBase.ModelType.Human)
|
||||||
{
|
{
|
||||||
var human = (Human*)drawObj;
|
var gender = ((Human*)DrawObjectAddress)->Customize.Sex;
|
||||||
|
var raceId = ((Human*)DrawObjectAddress)->Customize.Race;
|
||||||
var gender = human->Customize.Sex;
|
var tribeId = ((Human*)DrawObjectAddress)->Customize.Tribe;
|
||||||
var raceId = human->Customize.Race;
|
|
||||||
var tribeId = human->Customize.Tribe;
|
|
||||||
|
|
||||||
if (_isOwnedObject && ObjectKind == ObjectKind.Player
|
if (_isOwnedObject && ObjectKind == ObjectKind.Player
|
||||||
&& (gender != Gender || raceId != RaceId || tribeId != TribeId))
|
&& (gender != Gender || raceId != RaceId || tribeId != TribeId))
|
||||||
@@ -304,11 +257,15 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
TribeId = tribeId;
|
TribeId = tribeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
customizeDiff = CompareAndUpdateCustomizeData(human->Customize.Data);
|
customizeDiff = CompareAndUpdateCustomizeData(((Human*)DrawObjectAddress)->Customize.Data);
|
||||||
|
if (customizeDiff)
|
||||||
|
Logger.LogTrace("Checking [{this}] customize data as human from draw obj, result: {diff}", this, customizeDiff);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
customizeDiff = CompareAndUpdateCustomizeData(chara->DrawData.CustomizeData.Data);
|
customizeDiff = CompareAndUpdateCustomizeData(chara->DrawData.CustomizeData.Data);
|
||||||
|
if (customizeDiff)
|
||||||
|
Logger.LogTrace("Checking [{this}] customize data from game obj, result: {diff}", this, equipDiff);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((addrDiff || drawObjDiff || equipDiff || customizeDiff || nameChange) && _isOwnedObject)
|
if ((addrDiff || drawObjDiff || equipDiff || customizeDiff || nameChange) && _isOwnedObject)
|
||||||
@@ -322,11 +279,12 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
CurrentDrawCondition = DrawCondition.DrawObjectZero;
|
CurrentDrawCondition = DrawCondition.DrawObjectZero;
|
||||||
Logger.LogTrace("[{this}] Changed", this);
|
Logger.LogTrace("[{this}] Changed", this);
|
||||||
if (_isOwnedObject && ObjectKind != ObjectKind.Player)
|
if (_isOwnedObject && ObjectKind != ObjectKind.Player)
|
||||||
|
{
|
||||||
Mediator.Publish(new ClearCacheForObjectMessage(this));
|
Mediator.Publish(new ClearCacheForObjectMessage(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private unsafe bool CompareAndUpdateCustomizeData(Span<byte> customizeData)
|
private unsafe bool CompareAndUpdateCustomizeData(Span<byte> customizeData)
|
||||||
{
|
{
|
||||||
bool hasChanges = false;
|
bool hasChanges = false;
|
||||||
@@ -362,10 +320,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
|
|
||||||
private unsafe bool CompareAndUpdateMainHand(Weapon* weapon)
|
private unsafe bool CompareAndUpdateMainHand(Weapon* weapon)
|
||||||
{
|
{
|
||||||
var p = (nint)weapon;
|
if ((nint)weapon == nint.Zero) return false;
|
||||||
if (!PtrGuard.LooksLikePtr(p) || !PtrGuard.IsReadable(p, (nuint)sizeof(Weapon)))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
bool hasChanges = false;
|
bool hasChanges = false;
|
||||||
hasChanges |= weapon->ModelSetId != MainHandData[0];
|
hasChanges |= weapon->ModelSetId != MainHandData[0];
|
||||||
MainHandData[0] = weapon->ModelSetId;
|
MainHandData[0] = weapon->ModelSetId;
|
||||||
@@ -378,10 +333,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
|
|
||||||
private unsafe bool CompareAndUpdateOffHand(Weapon* weapon)
|
private unsafe bool CompareAndUpdateOffHand(Weapon* weapon)
|
||||||
{
|
{
|
||||||
var p = (nint)weapon;
|
if ((nint)weapon == nint.Zero) return false;
|
||||||
if (!PtrGuard.LooksLikePtr(p) || !PtrGuard.IsReadable(p, (nuint)sizeof(Weapon)))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
bool hasChanges = false;
|
bool hasChanges = false;
|
||||||
hasChanges |= weapon->ModelSetId != OffHandData[0];
|
hasChanges |= weapon->ModelSetId != OffHandData[0];
|
||||||
OffHandData[0] = weapon->ModelSetId;
|
OffHandData[0] = weapon->ModelSetId;
|
||||||
@@ -394,10 +346,12 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
|
|
||||||
private void FrameworkUpdate()
|
private void FrameworkUpdate()
|
||||||
{
|
{
|
||||||
|
if (!_delayedZoningTask?.IsCompleted ?? false) return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var zoningDelayActive = !(_delayedZoningTask?.IsCompleted ?? true);
|
_performanceCollector.LogPerformance(this, $"CheckAndUpdateObject>{(_isOwnedObject ? "Self" : "Other")}+{ObjectKind}/{(string.IsNullOrEmpty(Name) ? "Unk" : Name)}"
|
||||||
_performanceCollector.LogPerformance(this, $"CheckAndUpdateObject>{(_isOwnedObject ? "Self" : "Other")}+{ObjectKind}/{(string.IsNullOrEmpty(Name) ? "Unk" : Name)}", () => CheckAndUpdateObject(allowPublish: !zoningDelayActive));
|
+ $"+{Address.ToString("X")}", CheckAndUpdateObject);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -407,7 +361,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
|
|
||||||
private bool IsBeingDrawn()
|
private bool IsBeingDrawn()
|
||||||
{
|
{
|
||||||
EnsureLatestObjectState();
|
if (_haltProcessing) CheckAndUpdateObject();
|
||||||
|
|
||||||
if (_dalamudUtil.IsAnythingDrawing)
|
if (_dalamudUtil.IsAnythingDrawing)
|
||||||
{
|
{
|
||||||
@@ -419,28 +373,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
return CurrentDrawCondition != DrawCondition.None;
|
return CurrentDrawCondition != DrawCondition.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnsureLatestObjectState()
|
|
||||||
{
|
|
||||||
if (_haltProcessing || !_frameworkUpdateSubscribed)
|
|
||||||
{
|
|
||||||
CheckAndUpdateObject();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EnableFrameworkUpdates()
|
|
||||||
{
|
|
||||||
lock (_frameworkUpdateGate)
|
|
||||||
{
|
|
||||||
if (_frameworkUpdateSubscribed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Mediator.Subscribe<FrameworkUpdateMessage>(this, _ => FrameworkUpdate());
|
|
||||||
_frameworkUpdateSubscribed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe DrawCondition IsBeingDrawnUnsafe()
|
private unsafe DrawCondition IsBeingDrawnUnsafe()
|
||||||
{
|
{
|
||||||
if (Address == IntPtr.Zero) return DrawCondition.ObjectZero;
|
if (Address == IntPtr.Zero) return DrawCondition.ObjectZero;
|
||||||
@@ -498,6 +430,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
Logger.LogDebug("[{this}] Delay after zoning complete", this);
|
Logger.LogDebug("[{this}] Delay after zoning complete", this);
|
||||||
_zoningCts.Dispose();
|
_zoningCts.Dispose();
|
||||||
}
|
}
|
||||||
}, _zoningCts.Token);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,42 +1,38 @@
|
|||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
|
|
||||||
namespace LightlessSync.PlayerData.Pairs;
|
namespace LightlessSync.PlayerData.Pairs;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// orchestrates the lifecycle of a paired character
|
/// orchestrates the lifecycle of a paired character
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
|
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
|
||||||
{
|
{
|
||||||
new string Ident { get; }
|
new string Ident { get; }
|
||||||
bool Initialized { get; }
|
bool Initialized { get; }
|
||||||
bool IsVisible { get; }
|
bool IsVisible { get; }
|
||||||
bool ScheduledForDeletion { get; set; }
|
bool ScheduledForDeletion { get; set; }
|
||||||
CharacterData? LastReceivedCharacterData { get; }
|
CharacterData? LastReceivedCharacterData { get; }
|
||||||
long LastAppliedDataBytes { get; }
|
long LastAppliedDataBytes { get; }
|
||||||
new string? PlayerName { get; }
|
new string? PlayerName { get; }
|
||||||
string PlayerNameHash { get; }
|
string PlayerNameHash { get; }
|
||||||
uint PlayerCharacterId { get; }
|
uint PlayerCharacterId { get; }
|
||||||
DateTime? LastDataReceivedAt { get; }
|
DateTime? LastDataReceivedAt { get; }
|
||||||
DateTime? LastApplyAttemptAt { get; }
|
DateTime? LastApplyAttemptAt { get; }
|
||||||
DateTime? LastSuccessfulApplyAt { get; }
|
DateTime? LastSuccessfulApplyAt { get; }
|
||||||
string? LastFailureReason { get; }
|
string? LastFailureReason { get; }
|
||||||
IReadOnlyList<string> LastBlockingConditions { get; }
|
IReadOnlyList<string> LastBlockingConditions { get; }
|
||||||
bool IsApplying { get; }
|
bool IsApplying { get; }
|
||||||
bool IsDownloading { get; }
|
bool IsDownloading { get; }
|
||||||
int PendingDownloadCount { get; }
|
int PendingDownloadCount { get; }
|
||||||
int ForbiddenDownloadCount { get; }
|
int ForbiddenDownloadCount { get; }
|
||||||
bool PendingModReapply { get; }
|
DateTime? InvisibleSinceUtc { get; }
|
||||||
bool ModApplyDeferred { get; }
|
DateTime? VisibilityEvictionDueAtUtc { get; }
|
||||||
int MissingCriticalMods { get; }
|
|
||||||
int MissingNonCriticalMods { get; }
|
|
||||||
int MissingForbiddenMods { get; }
|
|
||||||
|
|
||||||
void Initialize();
|
void Initialize();
|
||||||
void ApplyData(CharacterData data);
|
void ApplyData(CharacterData data);
|
||||||
void ApplyLastReceivedData(bool forced = false);
|
void ApplyLastReceivedData(bool forced = false);
|
||||||
Task EnsurePerformanceMetricsAsync(CancellationToken cancellationToken);
|
bool FetchPerformanceMetricsFromCache();
|
||||||
bool FetchPerformanceMetricsFromCache();
|
void LoadCachedCharacterData(CharacterData data);
|
||||||
void LoadCachedCharacterData(CharacterData data);
|
void SetUploading(bool uploading);
|
||||||
void SetUploading(bool uploading);
|
void SetPaused(bool paused);
|
||||||
void SetPaused(bool paused);
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,5 +16,4 @@ public interface IPairPerformanceSubject
|
|||||||
long LastAppliedApproximateVRAMBytes { get; set; }
|
long LastAppliedApproximateVRAMBytes { get; set; }
|
||||||
long LastAppliedApproximateEffectiveVRAMBytes { get; set; }
|
long LastAppliedApproximateEffectiveVRAMBytes { get; set; }
|
||||||
long LastAppliedDataTris { get; set; }
|
long LastAppliedDataTris { get; set; }
|
||||||
long LastAppliedApproximateEffectiveTris { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ public class Pair
|
|||||||
public string? PlayerName => TryGetHandler()?.PlayerName ?? UserPair.User.AliasOrUID;
|
public string? PlayerName => TryGetHandler()?.PlayerName ?? UserPair.User.AliasOrUID;
|
||||||
public long LastAppliedDataBytes => TryGetHandler()?.LastAppliedDataBytes ?? -1;
|
public long LastAppliedDataBytes => TryGetHandler()?.LastAppliedDataBytes ?? -1;
|
||||||
public long LastAppliedDataTris => TryGetHandler()?.LastAppliedDataTris ?? -1;
|
public long LastAppliedDataTris => TryGetHandler()?.LastAppliedDataTris ?? -1;
|
||||||
public long LastAppliedApproximateEffectiveTris => TryGetHandler()?.LastAppliedApproximateEffectiveTris ?? -1;
|
|
||||||
public long LastAppliedApproximateVRAMBytes => TryGetHandler()?.LastAppliedApproximateVRAMBytes ?? -1;
|
public long LastAppliedApproximateVRAMBytes => TryGetHandler()?.LastAppliedApproximateVRAMBytes ?? -1;
|
||||||
public long LastAppliedApproximateEffectiveVRAMBytes => TryGetHandler()?.LastAppliedApproximateEffectiveVRAMBytes ?? -1;
|
public long LastAppliedApproximateEffectiveVRAMBytes => TryGetHandler()?.LastAppliedApproximateEffectiveVRAMBytes ?? -1;
|
||||||
public string Ident => TryGetHandler()?.Ident ?? TryGetConnection()?.Ident ?? string.Empty;
|
public string Ident => TryGetHandler()?.Ident ?? TryGetConnection()?.Ident ?? string.Empty;
|
||||||
@@ -88,25 +87,22 @@ public class Pair
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.Target is not MenuTargetDefault target || target.TargetObjectId != handler.PlayerCharacterId)
|
if (args.Target is not MenuTargetDefault target || target.TargetObjectId != handler.PlayerCharacterId || IsPaused)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsPaused)
|
UiSharedService.AddContextMenuItem(args, name: "Open Profile", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||||
{
|
{
|
||||||
UiSharedService.AddContextMenuItem(args, name: "Open Profile", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
_mediator.Publish(new ProfileOpenStandaloneMessage(this));
|
||||||
{
|
return Task.CompletedTask;
|
||||||
_mediator.Publish(new ProfileOpenStandaloneMessage(this));
|
});
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
|
|
||||||
UiSharedService.AddContextMenuItem(args, name: "Reapply last data", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
UiSharedService.AddContextMenuItem(args, name: "Reapply last data", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||||
{
|
{
|
||||||
ApplyLastReceivedData(forced: true);
|
ApplyLastReceivedData(forced: true);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
UiSharedService.AddContextMenuItem(args, name: "Change Permissions", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
UiSharedService.AddContextMenuItem(args, name: "Change Permissions", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||||
{
|
{
|
||||||
@@ -114,24 +110,7 @@ public class Pair
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (IsPaused)
|
UiSharedService.AddContextMenuItem(args, name: "Cycle pause state", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||||
{
|
|
||||||
UiSharedService.AddContextMenuItem(args, name: "Toggle Unpause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
|
||||||
{
|
|
||||||
_ = _apiController.Value.UnpauseAsync(UserData);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
UiSharedService.AddContextMenuItem(args, name: "Toggle Pause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
|
||||||
{
|
|
||||||
_ = _apiController.Value.PauseAsync(UserData);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
UiSharedService.AddContextMenuItem(args, name: "Cycle Pause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
|
||||||
{
|
{
|
||||||
TriggerCyclePause();
|
TriggerCyclePause();
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -217,6 +196,12 @@ public class Pair
|
|||||||
if (handler is null)
|
if (handler is null)
|
||||||
return PairDebugInfo.Empty;
|
return PairDebugInfo.Empty;
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var dueAt = handler.VisibilityEvictionDueAtUtc;
|
||||||
|
var remainingSeconds = dueAt.HasValue
|
||||||
|
? Math.Max(0, (dueAt.Value - now).TotalSeconds)
|
||||||
|
: (double?)null;
|
||||||
|
|
||||||
return new PairDebugInfo(
|
return new PairDebugInfo(
|
||||||
true,
|
true,
|
||||||
handler.Initialized,
|
handler.Initialized,
|
||||||
@@ -225,16 +210,14 @@ public class Pair
|
|||||||
handler.LastDataReceivedAt,
|
handler.LastDataReceivedAt,
|
||||||
handler.LastApplyAttemptAt,
|
handler.LastApplyAttemptAt,
|
||||||
handler.LastSuccessfulApplyAt,
|
handler.LastSuccessfulApplyAt,
|
||||||
|
handler.InvisibleSinceUtc,
|
||||||
|
handler.VisibilityEvictionDueAtUtc,
|
||||||
|
remainingSeconds,
|
||||||
handler.LastFailureReason,
|
handler.LastFailureReason,
|
||||||
handler.LastBlockingConditions,
|
handler.LastBlockingConditions,
|
||||||
handler.IsApplying,
|
handler.IsApplying,
|
||||||
handler.IsDownloading,
|
handler.IsDownloading,
|
||||||
handler.PendingDownloadCount,
|
handler.PendingDownloadCount,
|
||||||
handler.ForbiddenDownloadCount,
|
handler.ForbiddenDownloadCount);
|
||||||
handler.PendingModReapply,
|
|
||||||
handler.ModApplyDeferred,
|
|
||||||
handler.MissingCriticalMods,
|
|
||||||
handler.MissingNonCriticalMods,
|
|
||||||
handler.MissingForbiddenMods);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,7 +125,6 @@ public sealed partial class PairCoordinator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_mediator.Publish(new PairOnlineMessage(new PairUniqueIdentifier(dto.User.UID)));
|
|
||||||
PublishPairDataChanged();
|
PublishPairDataChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +136,7 @@ public sealed partial class PairCoordinator
|
|||||||
_pendingCharacterData.TryRemove(user.UID, out _);
|
_pendingCharacterData.TryRemove(user.UID, out _);
|
||||||
if (registrationResult.Value.CharacterIdent is not null)
|
if (registrationResult.Value.CharacterIdent is not null)
|
||||||
{
|
{
|
||||||
_ = _handlerRegistry.DeregisterOfflinePair(registrationResult.Value, forceDisposal: true);
|
_ = _handlerRegistry.DeregisterOfflinePair(registrationResult.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
_mediator.Publish(new ClearProfileUserDataMessage(user));
|
_mediator.Publish(new ClearProfileUserDataMessage(user));
|
||||||
|
|||||||
@@ -8,17 +8,15 @@ public sealed record PairDebugInfo(
|
|||||||
DateTime? LastDataReceivedAt,
|
DateTime? LastDataReceivedAt,
|
||||||
DateTime? LastApplyAttemptAt,
|
DateTime? LastApplyAttemptAt,
|
||||||
DateTime? LastSuccessfulApplyAt,
|
DateTime? LastSuccessfulApplyAt,
|
||||||
|
DateTime? InvisibleSinceUtc,
|
||||||
|
DateTime? VisibilityEvictionDueAtUtc,
|
||||||
|
double? VisibilityEvictionRemainingSeconds,
|
||||||
string? LastFailureReason,
|
string? LastFailureReason,
|
||||||
IReadOnlyList<string> BlockingConditions,
|
IReadOnlyList<string> BlockingConditions,
|
||||||
bool IsApplying,
|
bool IsApplying,
|
||||||
bool IsDownloading,
|
bool IsDownloading,
|
||||||
int PendingDownloadCount,
|
int PendingDownloadCount,
|
||||||
int ForbiddenDownloadCount,
|
int ForbiddenDownloadCount)
|
||||||
bool PendingModReapply,
|
|
||||||
bool ModApplyDeferred,
|
|
||||||
int MissingCriticalMods,
|
|
||||||
int MissingNonCriticalMods,
|
|
||||||
int MissingForbiddenMods)
|
|
||||||
{
|
{
|
||||||
public static PairDebugInfo Empty { get; } = new(
|
public static PairDebugInfo Empty { get; } = new(
|
||||||
false,
|
false,
|
||||||
@@ -29,14 +27,12 @@ public sealed record PairDebugInfo(
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
Array.Empty<string>(),
|
Array.Empty<string>(),
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
0,
|
0,
|
||||||
0,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0);
|
0);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,11 @@
|
|||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using LightlessSync.Interop.Ipc;
|
using LightlessSync.Interop.Ipc;
|
||||||
using LightlessSync.LightlessConfiguration;
|
|
||||||
using LightlessSync.PlayerData.Factories;
|
using LightlessSync.PlayerData.Factories;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.ActorTracking;
|
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Services.ModelDecimation;
|
|
||||||
using LightlessSync.Services.PairProcessing;
|
using LightlessSync.Services.PairProcessing;
|
||||||
using LightlessSync.Services.ServerConfiguration;
|
using LightlessSync.Services.ServerConfiguration;
|
||||||
using LightlessSync.Services.TextureCompression;
|
using LightlessSync.Services.TextureCompression;
|
||||||
using Dalamud.Plugin.Services;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -28,18 +24,12 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly IHostApplicationLifetime _lifetime;
|
private readonly IHostApplicationLifetime _lifetime;
|
||||||
private readonly FileCacheManager _fileCacheManager;
|
private readonly FileCacheManager _fileCacheManager;
|
||||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
|
||||||
private readonly PlayerPerformanceService _playerPerformanceService;
|
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||||
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||||
private readonly ServerConfigurationManager _serverConfigManager;
|
private readonly ServerConfigurationManager _serverConfigManager;
|
||||||
private readonly TextureDownscaleService _textureDownscaleService;
|
private readonly TextureDownscaleService _textureDownscaleService;
|
||||||
private readonly ModelDecimationService _modelDecimationService;
|
|
||||||
private readonly PairStateCache _pairStateCache;
|
private readonly PairStateCache _pairStateCache;
|
||||||
private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache;
|
private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache;
|
||||||
private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor;
|
|
||||||
private readonly LightlessConfigService _configService;
|
|
||||||
private readonly XivDataAnalyzer _modelAnalyzer;
|
|
||||||
private readonly IFramework _framework;
|
|
||||||
|
|
||||||
public PairHandlerAdapterFactory(
|
public PairHandlerAdapterFactory(
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
@@ -50,20 +40,14 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
FileDownloadManagerFactory fileDownloadManagerFactory,
|
FileDownloadManagerFactory fileDownloadManagerFactory,
|
||||||
PluginWarningNotificationService pluginWarningNotificationManager,
|
PluginWarningNotificationService pluginWarningNotificationManager,
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
||||||
IFramework framework,
|
|
||||||
IHostApplicationLifetime lifetime,
|
IHostApplicationLifetime lifetime,
|
||||||
FileCacheManager fileCacheManager,
|
FileCacheManager fileCacheManager,
|
||||||
PlayerPerformanceConfigService playerPerformanceConfigService,
|
|
||||||
PlayerPerformanceService playerPerformanceService,
|
PlayerPerformanceService playerPerformanceService,
|
||||||
PairProcessingLimiter pairProcessingLimiter,
|
PairProcessingLimiter pairProcessingLimiter,
|
||||||
ServerConfigurationManager serverConfigManager,
|
ServerConfigurationManager serverConfigManager,
|
||||||
TextureDownscaleService textureDownscaleService,
|
TextureDownscaleService textureDownscaleService,
|
||||||
ModelDecimationService modelDecimationService,
|
|
||||||
PairStateCache pairStateCache,
|
PairStateCache pairStateCache,
|
||||||
PairPerformanceMetricsCache pairPerformanceMetricsCache,
|
PairPerformanceMetricsCache pairPerformanceMetricsCache)
|
||||||
PenumbraTempCollectionJanitor tempCollectionJanitor,
|
|
||||||
XivDataAnalyzer modelAnalyzer,
|
|
||||||
LightlessConfigService configService)
|
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
@@ -73,27 +57,20 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
_fileDownloadManagerFactory = fileDownloadManagerFactory;
|
_fileDownloadManagerFactory = fileDownloadManagerFactory;
|
||||||
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
_framework = framework;
|
|
||||||
_lifetime = lifetime;
|
_lifetime = lifetime;
|
||||||
_fileCacheManager = fileCacheManager;
|
_fileCacheManager = fileCacheManager;
|
||||||
_playerPerformanceConfigService = playerPerformanceConfigService;
|
|
||||||
_playerPerformanceService = playerPerformanceService;
|
_playerPerformanceService = playerPerformanceService;
|
||||||
_pairProcessingLimiter = pairProcessingLimiter;
|
_pairProcessingLimiter = pairProcessingLimiter;
|
||||||
_serverConfigManager = serverConfigManager;
|
_serverConfigManager = serverConfigManager;
|
||||||
_textureDownscaleService = textureDownscaleService;
|
_textureDownscaleService = textureDownscaleService;
|
||||||
_modelDecimationService = modelDecimationService;
|
|
||||||
_pairStateCache = pairStateCache;
|
_pairStateCache = pairStateCache;
|
||||||
_pairPerformanceMetricsCache = pairPerformanceMetricsCache;
|
_pairPerformanceMetricsCache = pairPerformanceMetricsCache;
|
||||||
_tempCollectionJanitor = tempCollectionJanitor;
|
|
||||||
_modelAnalyzer = modelAnalyzer;
|
|
||||||
_configService = configService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IPairHandlerAdapter Create(string ident)
|
public IPairHandlerAdapter Create(string ident)
|
||||||
{
|
{
|
||||||
var downloadManager = _fileDownloadManagerFactory.Create();
|
var downloadManager = _fileDownloadManagerFactory.Create();
|
||||||
var dalamudUtilService = _serviceProvider.GetRequiredService<DalamudUtilService>();
|
var dalamudUtilService = _serviceProvider.GetRequiredService<DalamudUtilService>();
|
||||||
var actorObjectService = _serviceProvider.GetRequiredService<ActorObjectService>();
|
|
||||||
return new PairHandlerAdapter(
|
return new PairHandlerAdapter(
|
||||||
_loggerFactory.CreateLogger<PairHandlerAdapter>(),
|
_loggerFactory.CreateLogger<PairHandlerAdapter>(),
|
||||||
_mediator,
|
_mediator,
|
||||||
@@ -104,20 +81,13 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
downloadManager,
|
downloadManager,
|
||||||
_pluginWarningNotificationManager,
|
_pluginWarningNotificationManager,
|
||||||
dalamudUtilService,
|
dalamudUtilService,
|
||||||
_framework,
|
|
||||||
actorObjectService,
|
|
||||||
_lifetime,
|
_lifetime,
|
||||||
_fileCacheManager,
|
_fileCacheManager,
|
||||||
_playerPerformanceConfigService,
|
|
||||||
_playerPerformanceService,
|
_playerPerformanceService,
|
||||||
_pairProcessingLimiter,
|
_pairProcessingLimiter,
|
||||||
_serverConfigManager,
|
_serverConfigManager,
|
||||||
_textureDownscaleService,
|
_textureDownscaleService,
|
||||||
_modelDecimationService,
|
|
||||||
_pairStateCache,
|
_pairStateCache,
|
||||||
_pairPerformanceMetricsCache,
|
_pairPerformanceMetricsCache);
|
||||||
_tempCollectionJanitor,
|
|
||||||
_modelAnalyzer,
|
|
||||||
_configService);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ public sealed class PairHandlerRegistry : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (handler.LastReceivedCharacterData is not null &&
|
if (handler.LastReceivedCharacterData is not null &&
|
||||||
(handler.LastAppliedApproximateVRAMBytes < 0 || handler.LastAppliedDataTris < 0 || handler.LastAppliedApproximateEffectiveTris < 0))
|
(handler.LastAppliedApproximateVRAMBytes < 0 || handler.LastAppliedDataTris < 0))
|
||||||
{
|
{
|
||||||
handler.ApplyLastReceivedData(forced: true);
|
handler.ApplyLastReceivedData(forced: true);
|
||||||
}
|
}
|
||||||
@@ -136,7 +136,6 @@ public sealed class PairHandlerRegistry : IDisposable
|
|||||||
if (TryFinalizeHandlerRemoval(handler))
|
if (TryFinalizeHandlerRemoval(handler))
|
||||||
{
|
{
|
||||||
handler.Dispose();
|
handler.Dispose();
|
||||||
_pairStateCache.Clear(registration.CharacterIdent);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (shouldScheduleRemoval && handler is not null)
|
else if (shouldScheduleRemoval && handler is not null)
|
||||||
@@ -357,7 +356,6 @@ public sealed class PairHandlerRegistry : IDisposable
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_pairPerformanceMetricsCache.Clear(handler.Ident);
|
_pairPerformanceMetricsCache.Clear(handler.Ident);
|
||||||
_pairStateCache.Clear(handler.Ident);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -379,7 +377,6 @@ public sealed class PairHandlerRegistry : IDisposable
|
|||||||
{
|
{
|
||||||
handler.Dispose();
|
handler.Dispose();
|
||||||
_pairPerformanceMetricsCache.Clear(handler.Ident);
|
_pairPerformanceMetricsCache.Clear(handler.Ident);
|
||||||
_pairStateCache.Clear(handler.Ident);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,7 +401,6 @@ public sealed class PairHandlerRegistry : IDisposable
|
|||||||
if (TryFinalizeHandlerRemoval(handler))
|
if (TryFinalizeHandlerRemoval(handler))
|
||||||
{
|
{
|
||||||
handler.Dispose();
|
handler.Dispose();
|
||||||
_pairStateCache.Clear(handler.Ident);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -258,8 +258,7 @@ public sealed class PairLedger : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (handler.LastAppliedApproximateVRAMBytes >= 0
|
if (handler.LastAppliedApproximateVRAMBytes >= 0
|
||||||
&& handler.LastAppliedDataTris >= 0
|
&& handler.LastAppliedDataTris >= 0
|
||||||
&& handler.LastAppliedApproximateEffectiveVRAMBytes >= 0
|
&& handler.LastAppliedApproximateEffectiveVRAMBytes >= 0)
|
||||||
&& handler.LastAppliedApproximateEffectiveTris >= 0)
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -271,20 +270,7 @@ public sealed class PairLedger : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_ = Task.Run(async () =>
|
handler.ApplyLastReceivedData(forced: true);
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await handler.EnsurePerformanceMetricsAsync(CancellationToken.None).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
|
||||||
{
|
|
||||||
_logger.LogDebug(ex, "Failed to ensure performance metrics for {Ident}", handler.Ident);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -160,9 +160,8 @@ public sealed class PairManager
|
|||||||
return PairOperationResult<PairRegistration>.Fail($"Pair {user.UID} not found.");
|
return PairOperationResult<PairRegistration>.Fail($"Pair {user.UID} not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var ident = connection.Ident;
|
|
||||||
connection.SetOffline();
|
connection.SetOffline();
|
||||||
return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(user.UID), ident));
|
return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(user.UID), connection.Ident));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,7 +530,6 @@ public sealed class PairManager
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var ident = connection.Ident;
|
|
||||||
if (connection.IsOnline)
|
if (connection.IsOnline)
|
||||||
{
|
{
|
||||||
connection.SetOffline();
|
connection.SetOffline();
|
||||||
@@ -544,7 +542,7 @@ public sealed class PairManager
|
|||||||
shell.Users.Remove(userId);
|
shell.Users.Remove(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PairRegistration(new PairUniqueIdentifier(userId), ident);
|
return new PairRegistration(new PairUniqueIdentifier(userId), connection.Ident);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PairConnection CreateFromFullData(UserFullPairDto dto)
|
public static PairConnection CreateFromFullData(UserFullPairDto dto)
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ public sealed class PairConnection
|
|||||||
public void SetOffline()
|
public void SetOffline()
|
||||||
{
|
{
|
||||||
IsOnline = false;
|
IsOnline = false;
|
||||||
Ident = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdatePermissions(UserPermissions own, UserPermissions other)
|
public void UpdatePermissions(UserPermissions own, UserPermissions other)
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ namespace LightlessSync.PlayerData.Pairs;
|
|||||||
public readonly record struct PairPerformanceMetrics(
|
public readonly record struct PairPerformanceMetrics(
|
||||||
long TriangleCount,
|
long TriangleCount,
|
||||||
long ApproximateVramBytes,
|
long ApproximateVramBytes,
|
||||||
long ApproximateEffectiveVramBytes,
|
long ApproximateEffectiveVramBytes);
|
||||||
long ApproximateEffectiveTris);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// caches performance metrics keyed by pair ident
|
/// caches performance metrics keyed by pair ident
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
|||||||
});
|
});
|
||||||
|
|
||||||
Mediator.Subscribe<ConnectedMessage>(this, (_) => PushToAllVisibleUsers());
|
Mediator.Subscribe<ConnectedMessage>(this, (_) => PushToAllVisibleUsers());
|
||||||
Mediator.Subscribe<PairOnlineMessage>(this, (msg) => HandlePairOnline(msg.PairIdent));
|
|
||||||
Mediator.Subscribe<DisconnectedMessage>(this, (_) =>
|
Mediator.Subscribe<DisconnectedMessage>(this, (_) =>
|
||||||
{
|
{
|
||||||
_fileTransferManager.CancelUpload();
|
_fileTransferManager.CancelUpload();
|
||||||
@@ -112,20 +111,6 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
|||||||
_ = PushCharacterDataAsync(forced);
|
_ = PushCharacterDataAsync(forced);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandlePairOnline(PairUniqueIdentifier pairIdent)
|
|
||||||
{
|
|
||||||
if (!_apiController.IsConnected || !_pairLedger.IsPairVisible(pairIdent))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_pairLedger.GetHandler(pairIdent)?.UserData is { } user)
|
|
||||||
{
|
|
||||||
_usersToPushDataTo.Add(user);
|
|
||||||
PushCharacterData(forced: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PushCharacterDataAsync(bool forced = false)
|
private async Task PushCharacterDataAsync(bool forced = false)
|
||||||
{
|
{
|
||||||
await _pushLock.WaitAsync(_runtimeCts.Token).ConfigureAwait(false);
|
await _pushLock.WaitAsync(_runtimeCts.Token).ConfigureAwait(false);
|
||||||
@@ -167,6 +152,5 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<UserData> GetVisibleUsers()
|
private List<UserData> GetVisibleUsers() => [.. _pairLedger.GetVisiblePairs().Select(connection => connection.User)];
|
||||||
=> [.. _pairLedger.GetVisiblePairs().Where(connection => connection.IsOnline).Select(connection => connection.User)];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,8 +40,6 @@ using System.Reflection;
|
|||||||
using OtterTex;
|
using OtterTex;
|
||||||
using LightlessSync.Services.LightFinder;
|
using LightlessSync.Services.LightFinder;
|
||||||
using LightlessSync.Services.PairProcessing;
|
using LightlessSync.Services.PairProcessing;
|
||||||
using LightlessSync.Services.ModelDecimation;
|
|
||||||
using LightlessSync.UI.Models;
|
|
||||||
|
|
||||||
namespace LightlessSync;
|
namespace LightlessSync;
|
||||||
|
|
||||||
@@ -53,7 +51,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
IFramework framework, IObjectTable objectTable, IClientState clientState, ICondition condition, IChatGui chatGui,
|
IFramework framework, IObjectTable objectTable, IClientState clientState, ICondition condition, IChatGui chatGui,
|
||||||
IGameGui gameGui, IDtrBar dtrBar, IPluginLog pluginLog, ITargetManager targetManager, INotificationManager notificationManager,
|
IGameGui gameGui, IDtrBar dtrBar, IPluginLog pluginLog, ITargetManager targetManager, INotificationManager notificationManager,
|
||||||
ITextureProvider textureProvider, IContextMenu contextMenu, IGameInteropProvider gameInteropProvider, IGameConfig gameConfig,
|
ITextureProvider textureProvider, IContextMenu contextMenu, IGameInteropProvider gameInteropProvider, IGameConfig gameConfig,
|
||||||
ISigScanner sigScanner, INamePlateGui namePlateGui, IAddonLifecycle addonLifecycle, IPlayerState playerState)
|
ISigScanner sigScanner, INamePlateGui namePlateGui, IAddonLifecycle addonLifecycle)
|
||||||
{
|
{
|
||||||
NativeDll.Initialize(pluginInterface.AssemblyLocation.DirectoryName);
|
NativeDll.Initialize(pluginInterface.AssemblyLocation.DirectoryName);
|
||||||
if (!Directory.Exists(pluginInterface.ConfigDirectory.FullName))
|
if (!Directory.Exists(pluginInterface.ConfigDirectory.FullName))
|
||||||
@@ -106,9 +104,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
services.AddSingleton(new WindowSystem("LightlessSync"));
|
services.AddSingleton(new WindowSystem("LightlessSync"));
|
||||||
services.AddSingleton<FileDialogManager>();
|
services.AddSingleton<FileDialogManager>();
|
||||||
services.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", string.Empty, useEmbedded: true));
|
services.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", string.Empty, useEmbedded: true));
|
||||||
services.AddSingleton(framework);
|
|
||||||
services.AddSingleton(gameGui);
|
services.AddSingleton(gameGui);
|
||||||
services.AddSingleton(gameInteropProvider);
|
|
||||||
services.AddSingleton(addonLifecycle);
|
services.AddSingleton(addonLifecycle);
|
||||||
services.AddSingleton<IUiBuilder>(pluginInterface.UiBuilder);
|
services.AddSingleton<IUiBuilder>(pluginInterface.UiBuilder);
|
||||||
|
|
||||||
@@ -119,7 +115,6 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
services.AddSingleton<ProfileTagService>();
|
services.AddSingleton<ProfileTagService>();
|
||||||
services.AddSingleton<ApiController>();
|
services.AddSingleton<ApiController>();
|
||||||
services.AddSingleton<PerformanceCollectorService>();
|
services.AddSingleton<PerformanceCollectorService>();
|
||||||
services.AddSingleton<NameplateUpdateHookService>();
|
|
||||||
services.AddSingleton<HubFactory>();
|
services.AddSingleton<HubFactory>();
|
||||||
services.AddSingleton<FileUploadManager>();
|
services.AddSingleton<FileUploadManager>();
|
||||||
services.AddSingleton<FileTransferOrchestrator>();
|
services.AddSingleton<FileTransferOrchestrator>();
|
||||||
@@ -127,26 +122,19 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
services.AddSingleton<LightlessProfileManager>();
|
services.AddSingleton<LightlessProfileManager>();
|
||||||
services.AddSingleton<TextureCompressionService>();
|
services.AddSingleton<TextureCompressionService>();
|
||||||
services.AddSingleton<TextureDownscaleService>();
|
services.AddSingleton<TextureDownscaleService>();
|
||||||
services.AddSingleton<ModelDecimationService>();
|
|
||||||
services.AddSingleton<GameObjectHandlerFactory>();
|
services.AddSingleton<GameObjectHandlerFactory>();
|
||||||
services.AddSingleton<FileDownloadDeduplicator>();
|
|
||||||
services.AddSingleton<FileDownloadManagerFactory>();
|
services.AddSingleton<FileDownloadManagerFactory>();
|
||||||
services.AddSingleton<PairProcessingLimiter>();
|
services.AddSingleton<PairProcessingLimiter>();
|
||||||
services.AddSingleton<XivDataAnalyzer>();
|
services.AddSingleton<XivDataAnalyzer>();
|
||||||
services.AddSingleton<CharacterAnalyzer>();
|
services.AddSingleton<CharacterAnalyzer>();
|
||||||
services.AddSingleton<TokenProvider>();
|
services.AddSingleton<TokenProvider>();
|
||||||
services.AddSingleton<PluginWarningNotificationService>();
|
services.AddSingleton<PluginWarningNotificationService>();
|
||||||
services.AddSingleton<ICompactorContext, PluginCompactorContext>();
|
|
||||||
services.AddSingleton<ICompactionExecutor, ExternalCompactionExecutor>();
|
|
||||||
services.AddSingleton<FileCompactor>();
|
services.AddSingleton<FileCompactor>();
|
||||||
services.AddSingleton<TagHandler>();
|
services.AddSingleton<TagHandler>();
|
||||||
services.AddSingleton<PairRequestService>();
|
services.AddSingleton<PairRequestService>();
|
||||||
services.AddSingleton<ZoneChatService>();
|
services.AddSingleton<ZoneChatService>();
|
||||||
services.AddSingleton<ChatEmoteService>();
|
|
||||||
services.AddSingleton<IdDisplayHandler>();
|
services.AddSingleton<IdDisplayHandler>();
|
||||||
services.AddSingleton<PlayerPerformanceService>();
|
services.AddSingleton<PlayerPerformanceService>();
|
||||||
services.AddSingleton<PenumbraTempCollectionJanitor>();
|
|
||||||
services.AddSingleton<LocationShareService>();
|
|
||||||
|
|
||||||
services.AddSingleton<TextureMetadataHelper>(sp =>
|
services.AddSingleton<TextureMetadataHelper>(sp =>
|
||||||
new TextureMetadataHelper(sp.GetRequiredService<ILogger<TextureMetadataHelper>>(), gameData));
|
new TextureMetadataHelper(sp.GetRequiredService<ILogger<TextureMetadataHelper>>(), gameData));
|
||||||
@@ -183,8 +171,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
|
|
||||||
services.AddSingleton(sp => new BlockedCharacterHandler(
|
services.AddSingleton(sp => new BlockedCharacterHandler(
|
||||||
sp.GetRequiredService<ILogger<BlockedCharacterHandler>>(),
|
sp.GetRequiredService<ILogger<BlockedCharacterHandler>>(),
|
||||||
gameInteropProvider,
|
gameInteropProvider));
|
||||||
objectTable));
|
|
||||||
|
|
||||||
services.AddSingleton(sp => new IpcProvider(
|
services.AddSingleton(sp => new IpcProvider(
|
||||||
sp.GetRequiredService<ILogger<IpcProvider>>(),
|
sp.GetRequiredService<ILogger<IpcProvider>>(),
|
||||||
@@ -214,7 +201,6 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
gameInteropProvider,
|
gameInteropProvider,
|
||||||
objectTable,
|
objectTable,
|
||||||
clientState,
|
clientState,
|
||||||
condition,
|
|
||||||
sp.GetRequiredService<LightlessMediator>()));
|
sp.GetRequiredService<LightlessMediator>()));
|
||||||
|
|
||||||
services.AddSingleton(sp => new DalamudUtilService(
|
services.AddSingleton(sp => new DalamudUtilService(
|
||||||
@@ -227,7 +213,6 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
gameData,
|
gameData,
|
||||||
targetManager,
|
targetManager,
|
||||||
gameConfig,
|
gameConfig,
|
||||||
playerState,
|
|
||||||
sp.GetRequiredService<ActorObjectService>(),
|
sp.GetRequiredService<ActorObjectService>(),
|
||||||
sp.GetRequiredService<BlockedCharacterHandler>(),
|
sp.GetRequiredService<BlockedCharacterHandler>(),
|
||||||
sp.GetRequiredService<LightlessMediator>(),
|
sp.GetRequiredService<LightlessMediator>(),
|
||||||
@@ -282,7 +267,6 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
sp.GetRequiredService<ILogger<LightFinderPlateHandler>>(),
|
sp.GetRequiredService<ILogger<LightFinderPlateHandler>>(),
|
||||||
addonLifecycle,
|
addonLifecycle,
|
||||||
gameGui,
|
gameGui,
|
||||||
clientState,
|
|
||||||
sp.GetRequiredService<LightlessConfigService>(),
|
sp.GetRequiredService<LightlessConfigService>(),
|
||||||
sp.GetRequiredService<LightlessMediator>(),
|
sp.GetRequiredService<LightlessMediator>(),
|
||||||
objectTable,
|
objectTable,
|
||||||
@@ -290,22 +274,12 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
pluginInterface,
|
pluginInterface,
|
||||||
sp.GetRequiredService<PictomancyService>()));
|
sp.GetRequiredService<PictomancyService>()));
|
||||||
|
|
||||||
services.AddSingleton(sp => new LightFinderNativePlateHandler(
|
|
||||||
sp.GetRequiredService<ILogger<LightFinderNativePlateHandler>>(),
|
|
||||||
clientState,
|
|
||||||
sp.GetRequiredService<LightlessConfigService>(),
|
|
||||||
sp.GetRequiredService<LightlessMediator>(),
|
|
||||||
objectTable,
|
|
||||||
sp.GetRequiredService<PairUiService>(),
|
|
||||||
sp.GetRequiredService<NameplateUpdateHookService>()));
|
|
||||||
|
|
||||||
services.AddSingleton(sp => new LightFinderScannerService(
|
services.AddSingleton(sp => new LightFinderScannerService(
|
||||||
sp.GetRequiredService<ILogger<LightFinderScannerService>>(),
|
sp.GetRequiredService<ILogger<LightFinderScannerService>>(),
|
||||||
framework,
|
framework,
|
||||||
sp.GetRequiredService<LightFinderService>(),
|
sp.GetRequiredService<LightFinderService>(),
|
||||||
sp.GetRequiredService<LightlessMediator>(),
|
sp.GetRequiredService<LightlessMediator>(),
|
||||||
sp.GetRequiredService<LightFinderPlateHandler>(),
|
sp.GetRequiredService<LightFinderPlateHandler>(),
|
||||||
sp.GetRequiredService<LightFinderNativePlateHandler>(),
|
|
||||||
sp.GetRequiredService<ActorObjectService>()));
|
sp.GetRequiredService<ActorObjectService>()));
|
||||||
|
|
||||||
services.AddSingleton(sp => new ContextMenuService(
|
services.AddSingleton(sp => new ContextMenuService(
|
||||||
@@ -323,10 +297,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
sp.GetRequiredService<LightFinderScannerService>(),
|
sp.GetRequiredService<LightFinderScannerService>(),
|
||||||
sp.GetRequiredService<LightFinderService>(),
|
sp.GetRequiredService<LightFinderService>(),
|
||||||
sp.GetRequiredService<LightlessProfileManager>(),
|
sp.GetRequiredService<LightlessProfileManager>(),
|
||||||
sp.GetRequiredService<LightlessMediator>(),
|
sp.GetRequiredService<LightlessMediator>()));
|
||||||
chatGui,
|
|
||||||
sp.GetRequiredService<NotificationService>())
|
|
||||||
);
|
|
||||||
|
|
||||||
// IPC callers / manager
|
// IPC callers / manager
|
||||||
services.AddSingleton(sp => new IpcCallerPenumbra(
|
services.AddSingleton(sp => new IpcCallerPenumbra(
|
||||||
@@ -334,7 +305,8 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
pluginInterface,
|
pluginInterface,
|
||||||
sp.GetRequiredService<DalamudUtilService>(),
|
sp.GetRequiredService<DalamudUtilService>(),
|
||||||
sp.GetRequiredService<LightlessMediator>(),
|
sp.GetRequiredService<LightlessMediator>(),
|
||||||
sp.GetRequiredService<RedrawManager>()));
|
sp.GetRequiredService<RedrawManager>(),
|
||||||
|
sp.GetRequiredService<ActorObjectService>()));
|
||||||
|
|
||||||
services.AddSingleton(sp => new IpcCallerGlamourer(
|
services.AddSingleton(sp => new IpcCallerGlamourer(
|
||||||
sp.GetRequiredService<ILogger<IpcCallerGlamourer>>(),
|
sp.GetRequiredService<ILogger<IpcCallerGlamourer>>(),
|
||||||
@@ -379,11 +351,6 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
sp.GetRequiredService<DalamudUtilService>(),
|
sp.GetRequiredService<DalamudUtilService>(),
|
||||||
sp.GetRequiredService<LightlessMediator>()));
|
sp.GetRequiredService<LightlessMediator>()));
|
||||||
|
|
||||||
services.AddSingleton(sp => new IpcCallerLifestream(
|
|
||||||
pluginInterface,
|
|
||||||
sp.GetRequiredService<LightlessMediator>(),
|
|
||||||
sp.GetRequiredService<ILogger<IpcCallerLifestream>>()));
|
|
||||||
|
|
||||||
services.AddSingleton(sp => new IpcManager(
|
services.AddSingleton(sp => new IpcManager(
|
||||||
sp.GetRequiredService<ILogger<IpcManager>>(),
|
sp.GetRequiredService<ILogger<IpcManager>>(),
|
||||||
sp.GetRequiredService<LightlessMediator>(),
|
sp.GetRequiredService<LightlessMediator>(),
|
||||||
@@ -394,9 +361,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
sp.GetRequiredService<IpcCallerHonorific>(),
|
sp.GetRequiredService<IpcCallerHonorific>(),
|
||||||
sp.GetRequiredService<IpcCallerMoodles>(),
|
sp.GetRequiredService<IpcCallerMoodles>(),
|
||||||
sp.GetRequiredService<IpcCallerPetNames>(),
|
sp.GetRequiredService<IpcCallerPetNames>(),
|
||||||
sp.GetRequiredService<IpcCallerBrio>(),
|
sp.GetRequiredService<IpcCallerBrio>()));
|
||||||
sp.GetRequiredService<IpcCallerLifestream>()
|
|
||||||
));
|
|
||||||
|
|
||||||
// Notifications / HTTP
|
// Notifications / HTTP
|
||||||
services.AddSingleton(sp => new NotificationService(
|
services.AddSingleton(sp => new NotificationService(
|
||||||
@@ -429,7 +394,6 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
LightlessSync.UI.Style.MainStyle.Init(cfg, theme);
|
LightlessSync.UI.Style.MainStyle.Init(cfg, theme);
|
||||||
return cfg;
|
return cfg;
|
||||||
});
|
});
|
||||||
services.AddSingleton(sp => new TempCollectionConfigService(configDir));
|
|
||||||
services.AddSingleton(sp => new ServerConfigService(configDir));
|
services.AddSingleton(sp => new ServerConfigService(configDir));
|
||||||
services.AddSingleton(sp => new NotesConfigService(configDir));
|
services.AddSingleton(sp => new NotesConfigService(configDir));
|
||||||
services.AddSingleton(sp => new PairTagConfigService(configDir));
|
services.AddSingleton(sp => new PairTagConfigService(configDir));
|
||||||
@@ -443,7 +407,6 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<LightlessConfigService>());
|
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<LightlessConfigService>());
|
||||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<UiThemeConfigService>());
|
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<UiThemeConfigService>());
|
||||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<ChatConfigService>());
|
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<ChatConfigService>());
|
||||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<TempCollectionConfigService>());
|
|
||||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<ServerConfigService>());
|
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<ServerConfigService>());
|
||||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<NotesConfigService>());
|
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<NotesConfigService>());
|
||||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<PairTagConfigService>());
|
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<PairTagConfigService>());
|
||||||
@@ -495,12 +458,19 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
sp.GetRequiredService<LightlessConfigService>(),
|
sp.GetRequiredService<LightlessConfigService>(),
|
||||||
sp.GetRequiredService<UiSharedService>(),
|
sp.GetRequiredService<UiSharedService>(),
|
||||||
sp.GetRequiredService<ApiController>(),
|
sp.GetRequiredService<ApiController>(),
|
||||||
|
sp.GetRequiredService<LightFinderScannerService>()));
|
||||||
|
|
||||||
|
services.AddScoped<WindowMediatorSubscriberBase, SyncshellFinderUI>(sp => new SyncshellFinderUI(
|
||||||
|
sp.GetRequiredService<ILogger<SyncshellFinderUI>>(),
|
||||||
|
sp.GetRequiredService<LightlessMediator>(),
|
||||||
|
sp.GetRequiredService<PerformanceCollectorService>(),
|
||||||
|
sp.GetRequiredService<LightFinderService>(),
|
||||||
|
sp.GetRequiredService<UiSharedService>(),
|
||||||
|
sp.GetRequiredService<ApiController>(),
|
||||||
sp.GetRequiredService<LightFinderScannerService>(),
|
sp.GetRequiredService<LightFinderScannerService>(),
|
||||||
sp.GetRequiredService<PairUiService>(),
|
sp.GetRequiredService<PairUiService>(),
|
||||||
sp.GetRequiredService<DalamudUtilService>(),
|
sp.GetRequiredService<DalamudUtilService>(),
|
||||||
sp.GetRequiredService<LightlessProfileManager>(),
|
sp.GetRequiredService<LightlessProfileManager>()));
|
||||||
sp.GetRequiredService<ActorObjectService>(),
|
|
||||||
sp.GetRequiredService<LightFinderPlateHandler>()));
|
|
||||||
|
|
||||||
services.AddScoped<IPopupHandler, BanUserPopupHandler>();
|
services.AddScoped<IPopupHandler, BanUserPopupHandler>();
|
||||||
services.AddScoped<IPopupHandler, CensusPopupHandler>();
|
services.AddScoped<IPopupHandler, CensusPopupHandler>();
|
||||||
@@ -520,7 +490,6 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
sp.GetRequiredService<ILogger<UiService>>(),
|
sp.GetRequiredService<ILogger<UiService>>(),
|
||||||
pluginInterface.UiBuilder,
|
pluginInterface.UiBuilder,
|
||||||
sp.GetRequiredService<LightlessConfigService>(),
|
sp.GetRequiredService<LightlessConfigService>(),
|
||||||
sp.GetRequiredService<DalamudUtilService>(),
|
|
||||||
sp.GetRequiredService<WindowSystem>(),
|
sp.GetRequiredService<WindowSystem>(),
|
||||||
sp.GetServices<WindowMediatorSubscriberBase>(),
|
sp.GetServices<WindowMediatorSubscriberBase>(),
|
||||||
sp.GetRequiredService<UiFactory>(),
|
sp.GetRequiredService<UiFactory>(),
|
||||||
@@ -558,9 +527,9 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
clientState,
|
clientState,
|
||||||
gameGui,
|
gameGui,
|
||||||
objectTable,
|
objectTable,
|
||||||
|
gameInteropProvider,
|
||||||
sp.GetRequiredService<LightlessMediator>(),
|
sp.GetRequiredService<LightlessMediator>(),
|
||||||
sp.GetRequiredService<PairUiService>(),
|
sp.GetRequiredService<PairUiService>()));
|
||||||
sp.GetRequiredService<NameplateUpdateHookService>()));
|
|
||||||
|
|
||||||
// Hosted services
|
// Hosted services
|
||||||
services.AddHostedService(sp => sp.GetRequiredService<ConfigurationSaveService>());
|
services.AddHostedService(sp => sp.GetRequiredService<ConfigurationSaveService>());
|
||||||
@@ -579,7 +548,6 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
services.AddHostedService(sp => sp.GetRequiredService<ContextMenuService>());
|
services.AddHostedService(sp => sp.GetRequiredService<ContextMenuService>());
|
||||||
services.AddHostedService(sp => sp.GetRequiredService<LightFinderService>());
|
services.AddHostedService(sp => sp.GetRequiredService<LightFinderService>());
|
||||||
services.AddHostedService(sp => sp.GetRequiredService<LightFinderPlateHandler>());
|
services.AddHostedService(sp => sp.GetRequiredService<LightFinderPlateHandler>());
|
||||||
services.AddHostedService(sp => sp.GetRequiredService<LightFinderNativePlateHandler>());
|
|
||||||
}).Build();
|
}).Build();
|
||||||
|
|
||||||
_ = _host.StartAsync();
|
_ = _host.StartAsync();
|
||||||
@@ -587,6 +555,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_host.StopAsync().ContinueWith(_ => _host.Dispose()).Wait(TimeSpan.FromSeconds(5));
|
_host.StopAsync().GetAwaiter().GetResult();
|
||||||
|
_host.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
namespace LightlessSync.Resources;
|
|
||||||
|
|
||||||
public static class LocalizationExtensions
|
|
||||||
{
|
|
||||||
public static string F(this string mask, params object[] args)
|
|
||||||
{
|
|
||||||
return string.Format(mask, args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
171
LightlessSync/Resources/Resources.Designer.cs
generated
171
LightlessSync/Resources/Resources.Designer.cs
generated
@@ -1,171 +0,0 @@
|
|||||||
//------------------------------------------------------------------------------
|
|
||||||
// <auto-generated>
|
|
||||||
// This code was generated by a tool.
|
|
||||||
// Runtime Version:4.0.30319.42000
|
|
||||||
//
|
|
||||||
// Changes to this file may cause incorrect behavior and will be lost if
|
|
||||||
// the code is regenerated.
|
|
||||||
// </auto-generated>
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
namespace LightlessSync.Resources {
|
|
||||||
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", "18.0.0.0")]
|
|
||||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
|
||||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
|
||||||
public class Resources {
|
|
||||||
|
|
||||||
private static global::System.Resources.ResourceManager resourceMan;
|
|
||||||
|
|
||||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
|
||||||
|
|
||||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
|
||||||
internal Resources() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the cached ResourceManager instance used by this class.
|
|
||||||
/// </summary>
|
|
||||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
|
||||||
public static global::System.Resources.ResourceManager ResourceManager {
|
|
||||||
get {
|
|
||||||
if (object.ReferenceEquals(resourceMan, null)) {
|
|
||||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("LightlessSync.Resources.Resources", typeof(Resources).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)]
|
|
||||||
public static global::System.Globalization.CultureInfo Culture {
|
|
||||||
get {
|
|
||||||
return resourceCulture;
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
resourceCulture = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to I agree.
|
|
||||||
/// </summary>
|
|
||||||
public static string ToSStrings_AgreeLabel {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("ToSStrings_AgreeLabel", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Agreement of Usage of Service.
|
|
||||||
/// </summary>
|
|
||||||
public static string ToSStrings_AgreementLabel {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("ToSStrings_AgreementLabel", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to 'I agree' button will be available in.
|
|
||||||
/// </summary>
|
|
||||||
public static string ToSStrings_ButtonWillBeAvailableIn {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("ToSStrings_ButtonWillBeAvailableIn", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Language.
|
|
||||||
/// </summary>
|
|
||||||
public static string ToSStrings_LanguageLabel {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("ToSStrings_LanguageLabel", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to All of the mod files currently active on your character as well as your current character state will be uploaded to the service you registered yourself at automatically. The plugin will exclusively upload the necessary mod files and not the whole mod..
|
|
||||||
/// </summary>
|
|
||||||
public static string ToSStrings_Paragraph1 {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("ToSStrings_Paragraph1", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to If you are on a data capped internet connection, higher fees due to data usage depending on the amount of downloaded and uploaded mod files might occur. Mod files will be compressed on up- and download to save on bandwidth usage. Due to varying up- and download speeds, changes in characters might not be visible immediately. Files present on the service that already represent your active mod files will not be uploaded again..
|
|
||||||
/// </summary>
|
|
||||||
public static string ToSStrings_Paragraph2 {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("ToSStrings_Paragraph2", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to The mod files you are uploading are confidential and will not be distributed to parties other than the ones who are requesting the exact same mod files. Please think about who you are going to pair since it is unavoidable that they will receive and locally cache the necessary mod files that you have currently in use. Locally cached mod files will have arbitrary file names to discourage attempts at replicating the original mod..
|
|
||||||
/// </summary>
|
|
||||||
public static string ToSStrings_Paragraph3 {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("ToSStrings_Paragraph3", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to The plugin creator tried their best to keep you secure. However, there is no guarantee for 100% security. Do not blindly pair your client with everyone..
|
|
||||||
/// </summary>
|
|
||||||
public static string ToSStrings_Paragraph4 {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("ToSStrings_Paragraph4", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Mod files that are saved on the service will remain on the service as long as there are requests for the files from clients. After a period of not being used, the mod files will be automatically deleted. You will also be able to wipe all the files you have personally uploaded on request. The service holds no information about which mod files belong to which mod..
|
|
||||||
/// </summary>
|
|
||||||
public static string ToSStrings_Paragraph5 {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("ToSStrings_Paragraph5", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to This service is provided as-is. In case of abuse join the Lightless Sync Discord..
|
|
||||||
/// </summary>
|
|
||||||
public static string ToSStrings_Paragraph6 {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("ToSStrings_Paragraph6", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to READ THIS CAREFULLY.
|
|
||||||
/// </summary>
|
|
||||||
public static string ToSStrings_ReadLabel {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("ToSStrings_ReadLabel", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Looks up a localized string similar to Users Online.
|
|
||||||
/// </summary>
|
|
||||||
public static string Users_Online {
|
|
||||||
get {
|
|
||||||
return ResourceManager.GetString("Users_Online", resourceCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<root>
|
|
||||||
<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>
|
|
||||||
<data name="ToSStrings_LanguageLabel" xml:space="preserve">
|
|
||||||
<value>Language</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_AgreementLabel" xml:space="preserve">
|
|
||||||
<value>Nutzungsbedingungen</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_ReadLabel" xml:space="preserve">
|
|
||||||
<value>BITTE LIES DIES SORGFÄLTIG</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_Paragraph1" xml:space="preserve">
|
|
||||||
<value>Alle Moddateien, die aktuell auf deinem Charakter aktiv sind und dein Charakterzustand werden automatisch zu dem Service, an dem du dich registriert hast, hochgeladen. Das Plugin wird ausschließlich die nötigen Moddateien hochladen und nicht die gesamte Modifikation.</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_Paragraph2" xml:space="preserve">
|
|
||||||
<value>Falls du mit einer getakteten Internetverbindung verbunden bist, können durch den Datentransfer von Hoch- und Runtergeladenen Moddateien höhere Kosten entstehen. Moddateien werden beim Hoch- und Runterladen komprimiert um Bandbreite zu sparen. Durch unterschiedliche Hoch- und Runterladgeschwindigkeiten ist es möglich, dass Änderungen an Charakteren nicht sofort sichtbar sind. Dateien die bereits auf dem Service existieren, werden nicht nochmals hochgeladen.</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_Paragraph3" xml:space="preserve">
|
|
||||||
<value>Die Moddateien die du hochlädst sind vertraulich und werden nicht mit anderen Nutzern geteilt, die nicht die exakt selben Dateien anfordern. Bitte überlege dir sorgfältig mit wem du deinen Identifikationscode teilst, da es unvermeidlich ist, dass die andere Person deine Moddateien erhält und lokal zwischenspeichert. Lokal zwischengespeicherte Dateien haben willkürrliche Namen um vor Versuchen abzuschrecken die originalen Moddateien aus diesen wiederherzustellen.</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_Paragraph4" xml:space="preserve">
|
|
||||||
<value>Der Ersteller des Plugins hat sein Bestes getan, um deine Sicherheit zu gewährleisten. Es gibt jedoch keine Garantie für 100%ige Sicherheit. Teile deinen Identifikationscode nicht blind mit jedem.</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_Paragraph5" xml:space="preserve">
|
|
||||||
<value>Moddateien, die auf dem Service gespeichert sind, verbleiben auf dem Service, solange es Anforderungen für diese Dateien gibt. Nach einer Zeitspanne in der die Dateien nicht verwendet wurden, werden diese automatisch gelöscht. Du hast auch die Möglichkeit manuell alle Dateien auf dem Service zu löschen. Der Service hat keine Informationen welche Moddateien zu welcher Modifikation gehören.</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_Paragraph6" xml:space="preserve">
|
|
||||||
<value>Dieser Dienst wird ohne Gewähr angeboten. Im Falle eines Missbrauchs tretet dem Lightless Sync Discord bei.</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_AgreeLabel" xml:space="preserve">
|
|
||||||
<value>Ich Stimme zu</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_ButtonWillBeAvailableIn" xml:space="preserve">
|
|
||||||
<value>"Ich stimme zu" Knopf verfügbar in</value>
|
|
||||||
</data>
|
|
||||||
</root>
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<root>
|
|
||||||
<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>
|
|
||||||
<data name="ToSStrings_LanguageLabel" xml:space="preserve">
|
|
||||||
<value>Language</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_AgreementLabel" xml:space="preserve">
|
|
||||||
<value>Conditions d'Utilisation</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_ReadLabel" xml:space="preserve">
|
|
||||||
<value>LISEZ CES INFORMATIONS ATTENTIVEMENT</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_Paragraph1" xml:space="preserve">
|
|
||||||
<value>Tous les fichiers moddés actuellement en cours d'utilisation ainsi que le statut actuel de votre personnage vont être mix en ligne via le service sur lequel vous vous êtes automatiquement enregistré. Seuls les fichiers nécessaires seront téléversés par le plugin et non pas le mod en entier.</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_Paragraph2" xml:space="preserve">
|
|
||||||
<value>Si le débit de votre connexion internet est limité, le téléchargement et téléversement d'un grand nombre de fichiers peut entraîner des coûts supplémentaires. Les fichiers seront compressés au chargement et versement pour réduire l'impact sur votre bande passants. Selon la rapidité de vos téléchargements et téléversements, les changements ne seront peut-être pas visibles instantanément sur les personnages. Les fichiers déja présents sur le service qui correspondent à ceux de vos mods en cours d'utilisation ne seront pas remis en ligne.</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_Paragraph3" xml:space="preserve">
|
|
||||||
<value>Les fichiers que vous allez partager sont confidentiels et ne seront envoyés qu'aux utilisateurs qui feront une requête exacte de ceux-çi. Nous vous demandons de (re)considérer qui sera synchronisé avec vous, puisqu'ils recevront et stockeront inévitablement en local les fichiers nécéssaires utilisés à cet instant. Les noms des fichiers stockés localement sont changés de manière arbitraire afin de décourager toute tentative de réplication des originaux.</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_Paragraph4" xml:space="preserve">
|
|
||||||
<value>Le créateur de ce plugin a tenté de sécuriser l'application du mieux possible. Cependant, il ne peut pas garantir une protection 100% infaillible. Pour votre sécurité, ne vous synchronisez pas aveuglément et avec n'importe qui.</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_Paragraph5" xml:space="preserve">
|
|
||||||
<value>Les fichiers sauvegardés sur le service resteront en ligne tant que des utilisateurs en feront usage. Ils seront effacés automatiquement après une certaine période d'inactivité. Vous pouvez également demander l'effacement de tous les fichiers que vous avez mis en ligne vous-même. Le service en soi ne contient aucune information pouvant identifier quel fichier appartient à quel mod.</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_Paragraph6" xml:space="preserve">
|
|
||||||
<value>Ce service et ses composants vous sont fournis en l'état. En cas d'abus rejoindre le serveur Discord Lightless Sync.</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_AgreeLabel" xml:space="preserve">
|
|
||||||
<value>J'accept</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_ButtonWillBeAvailableIn" xml:space="preserve">
|
|
||||||
<value>Bouton "J'accept" disposible dans</value>
|
|
||||||
</data>
|
|
||||||
</root>
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
<?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>
|
|
||||||
<data name="ToSStrings_AgreeLabel" xml:space="preserve">
|
|
||||||
<value>I agree</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_AgreementLabel" xml:space="preserve">
|
|
||||||
<value>Agreement of Usage of Service</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_ButtonWillBeAvailableIn" xml:space="preserve">
|
|
||||||
<value>'I agree' button will be available in</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_Paragraph1" xml:space="preserve">
|
|
||||||
<value>All of the mod files currently active on your character as well as your current character state will be uploaded to the service you registered yourself at automatically. The plugin will exclusively upload the necessary mod files and not the whole mod.</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_Paragraph2" xml:space="preserve">
|
|
||||||
<value>If you are on a data capped internet connection, higher fees due to data usage depending on the amount of downloaded and uploaded mod files might occur. Mod files will be compressed on up- and download to save on bandwidth usage. Due to varying up- and download speeds, changes in characters might not be visible immediately. Files present on the service that already represent your active mod files will not be uploaded again.</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_Paragraph3" xml:space="preserve">
|
|
||||||
<value>The mod files you are uploading are confidential and will not be distributed to parties other than the ones who are requesting the exact same mod files. Please think about who you are going to pair since it is unavoidable that they will receive and locally cache the necessary mod files that you have currently in use. Locally cached mod files will have arbitrary file names to discourage attempts at replicating the original mod.</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_Paragraph4" xml:space="preserve">
|
|
||||||
<value>The plugin creator tried their best to keep you secure. However, there is no guarantee for 100% security. Do not blindly pair your client with everyone.</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_Paragraph5" xml:space="preserve">
|
|
||||||
<value>Mod files that are saved on the service will remain on the service as long as there are requests for the files from clients. After a period of not being used, the mod files will be automatically deleted. You will also be able to wipe all the files you have personally uploaded on request. The service holds no information about which mod files belong to which mod.</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_Paragraph6" xml:space="preserve">
|
|
||||||
<value>This service is provided as-is. In case of abuse join the Lightless Sync Discord.</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_ReadLabel" xml:space="preserve">
|
|
||||||
<value>READ THIS CAREFULLY</value>
|
|
||||||
</data>
|
|
||||||
<data name="ToSStrings_LanguageLabel" xml:space="preserve">
|
|
||||||
<value>Language</value>
|
|
||||||
</data>
|
|
||||||
<data name="Users_Online" xml:space="preserve">
|
|
||||||
<value>Users Online</value>
|
|
||||||
</data>
|
|
||||||
</root>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<root>
|
|
||||||
<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>
|
|
||||||
<data name="ToSStrings_LanguageLabel" xml:space="preserve">
|
|
||||||
<value>语言</value>
|
|
||||||
</data>
|
|
||||||
<data name="Users_Online" xml:space="preserve">
|
|
||||||
<value>用户在线</value>
|
|
||||||
</data>
|
|
||||||
</root>
|
|
||||||
@@ -1,23 +1,20 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using Dalamud.Game.ClientState.Conditions;
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
using Dalamud.Hooking;
|
using Dalamud.Hooking;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using FFXIVClientStructs.Interop;
|
using FFXIVClientStructs.Interop;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
using LightlessSync.PlayerData.Handlers;
|
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using BattleNpcSubKind = FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSubKind;
|
|
||||||
using IPlayerCharacter = Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter;
|
|
||||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||||
using LightlessObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
using LightlessObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||||
|
|
||||||
namespace LightlessSync.Services.ActorTracking;
|
namespace LightlessSync.Services.ActorTracking;
|
||||||
|
|
||||||
public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorSubscriber
|
public sealed class ActorObjectService : IHostedService, IDisposable
|
||||||
{
|
{
|
||||||
public readonly record struct ActorDescriptor(
|
public readonly record struct ActorDescriptor(
|
||||||
string Name,
|
string Name,
|
||||||
@@ -34,19 +31,13 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
private readonly IFramework _framework;
|
private readonly IFramework _framework;
|
||||||
private readonly IGameInteropProvider _interop;
|
private readonly IGameInteropProvider _interop;
|
||||||
private readonly IObjectTable _objectTable;
|
private readonly IObjectTable _objectTable;
|
||||||
private readonly IClientState _clientState;
|
|
||||||
private readonly ICondition _condition;
|
|
||||||
private readonly LightlessMediator _mediator;
|
private readonly LightlessMediator _mediator;
|
||||||
private readonly object _playerRelatedHandlerLock = new();
|
|
||||||
private readonly HashSet<GameObjectHandler> _playerRelatedHandlers = [];
|
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<nint, ActorDescriptor> _activePlayers = new();
|
private readonly ConcurrentDictionary<nint, ActorDescriptor> _activePlayers = new();
|
||||||
private readonly ConcurrentDictionary<nint, ActorDescriptor> _gposePlayers = new();
|
|
||||||
private readonly ConcurrentDictionary<string, ActorDescriptor> _actorsByHash = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, ActorDescriptor> _actorsByHash = new(StringComparer.Ordinal);
|
||||||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<nint, ActorDescriptor>> _actorsByName = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, ConcurrentDictionary<nint, ActorDescriptor>> _actorsByName = new(StringComparer.Ordinal);
|
||||||
private readonly ConcurrentDictionary<nint, byte> _pendingHashResolutions = new();
|
private readonly OwnedObjectTracker _ownedTracker = new();
|
||||||
private ActorSnapshot _snapshot = ActorSnapshot.Empty;
|
private ActorSnapshot _snapshot = ActorSnapshot.Empty;
|
||||||
private GposeSnapshot _gposeSnapshot = GposeSnapshot.Empty;
|
|
||||||
|
|
||||||
private Hook<Character.Delegates.OnInitialize>? _onInitializeHook;
|
private Hook<Character.Delegates.OnInitialize>? _onInitializeHook;
|
||||||
private Hook<Character.Delegates.Terminate>? _onTerminateHook;
|
private Hook<Character.Delegates.Terminate>? _onTerminateHook;
|
||||||
@@ -64,50 +55,21 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
IGameInteropProvider interop,
|
IGameInteropProvider interop,
|
||||||
IObjectTable objectTable,
|
IObjectTable objectTable,
|
||||||
IClientState clientState,
|
IClientState clientState,
|
||||||
ICondition condition,
|
|
||||||
LightlessMediator mediator)
|
LightlessMediator mediator)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_framework = framework;
|
_framework = framework;
|
||||||
_interop = interop;
|
_interop = interop;
|
||||||
_objectTable = objectTable;
|
_objectTable = objectTable;
|
||||||
_clientState = clientState;
|
|
||||||
_condition = condition;
|
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
|
|
||||||
_mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, (msg) =>
|
|
||||||
{
|
|
||||||
if (!msg.OwnedObject) return;
|
|
||||||
lock (_playerRelatedHandlerLock)
|
|
||||||
{
|
|
||||||
_playerRelatedHandlers.Add(msg.GameObjectHandler);
|
|
||||||
}
|
|
||||||
RefreshTrackedActors(force: true);
|
|
||||||
});
|
|
||||||
_mediator.Subscribe<GameObjectHandlerDestroyedMessage>(this, (msg) =>
|
|
||||||
{
|
|
||||||
if (!msg.OwnedObject) return;
|
|
||||||
lock (_playerRelatedHandlerLock)
|
|
||||||
{
|
|
||||||
_playerRelatedHandlers.Remove(msg.GameObjectHandler);
|
|
||||||
}
|
|
||||||
RefreshTrackedActors(force: true);
|
|
||||||
});
|
|
||||||
_mediator.Subscribe<DalamudLogoutMessage>(this, _ => ClearTrackingState());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
|
|
||||||
|
|
||||||
private ActorSnapshot Snapshot => Volatile.Read(ref _snapshot);
|
private ActorSnapshot Snapshot => Volatile.Read(ref _snapshot);
|
||||||
private GposeSnapshot CurrentGposeSnapshot => Volatile.Read(ref _gposeSnapshot);
|
|
||||||
|
|
||||||
public IReadOnlyList<nint> PlayerAddresses => Snapshot.PlayerAddresses;
|
public IReadOnlyList<nint> PlayerAddresses => Snapshot.PlayerAddresses;
|
||||||
|
|
||||||
public IEnumerable<ActorDescriptor> ObjectDescriptors => _activePlayers.Values;
|
public IEnumerable<ActorDescriptor> PlayerDescriptors => _activePlayers.Values;
|
||||||
public IReadOnlyList<ActorDescriptor> PlayerDescriptors => Snapshot.PlayerDescriptors;
|
public IReadOnlyList<ActorDescriptor> PlayerCharacterDescriptors => Snapshot.PlayerDescriptors;
|
||||||
public IReadOnlyList<ActorDescriptor> OwnedDescriptors => Snapshot.OwnedDescriptors;
|
|
||||||
public IReadOnlyList<ActorDescriptor> GposeDescriptors => CurrentGposeSnapshot.GposeDescriptors;
|
|
||||||
public LightlessMediator Mediator => _mediator;
|
|
||||||
|
|
||||||
public bool TryGetActorByHash(string hash, out ActorDescriptor descriptor) => _actorsByHash.TryGetValue(hash, out descriptor);
|
public bool TryGetActorByHash(string hash, out ActorDescriptor descriptor) => _actorsByHash.TryGetValue(hash, out descriptor);
|
||||||
public bool TryGetValidatedActorByHash(string hash, out ActorDescriptor descriptor)
|
public bool TryGetValidatedActorByHash(string hash, out ActorDescriptor descriptor)
|
||||||
@@ -151,7 +113,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
public bool HooksActive => _hooksActive;
|
public bool HooksActive => _hooksActive;
|
||||||
public bool HasPendingHashResolutions => !_pendingHashResolutions.IsEmpty;
|
|
||||||
public IReadOnlyList<nint> RenderedPlayerAddresses => Snapshot.OwnedObjects.RenderedPlayers;
|
public IReadOnlyList<nint> RenderedPlayerAddresses => Snapshot.OwnedObjects.RenderedPlayers;
|
||||||
public IReadOnlyList<nint> RenderedCompanionAddresses => Snapshot.OwnedObjects.RenderedCompanions;
|
public IReadOnlyList<nint> RenderedCompanionAddresses => Snapshot.OwnedObjects.RenderedCompanions;
|
||||||
public IReadOnlyList<nint> OwnedObjectAddresses => Snapshot.OwnedObjects.OwnedAddresses;
|
public IReadOnlyList<nint> OwnedObjectAddresses => Snapshot.OwnedObjects.OwnedAddresses;
|
||||||
@@ -175,16 +136,15 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
public bool TryGetOwnedObjectByIndex(ushort objectIndex, out LightlessObjectKind ownedKind)
|
public bool TryGetOwnedObjectByIndex(ushort objectIndex, out LightlessObjectKind ownedKind)
|
||||||
{
|
{
|
||||||
ownedKind = default;
|
ownedKind = default;
|
||||||
var ownedDescriptors = OwnedDescriptors;
|
var ownedSnapshot = OwnedObjects;
|
||||||
for (var i = 0; i < ownedDescriptors.Count; i++)
|
foreach (var (address, kind) in ownedSnapshot)
|
||||||
{
|
{
|
||||||
var descriptor = ownedDescriptors[i];
|
if (!TryGetDescriptor(address, out var descriptor))
|
||||||
if (descriptor.ObjectIndex != objectIndex)
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (descriptor.OwnedKind is { } resolvedKind)
|
if (descriptor.ObjectIndex == objectIndex)
|
||||||
{
|
{
|
||||||
ownedKind = resolvedKind;
|
ownedKind = kind;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,25 +197,18 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> WaitForFullyLoadedAsync(nint address, CancellationToken cancellationToken = default, int timeOutMs = 30000)
|
public async Task WaitForFullyLoadedAsync(nint address, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (address == nint.Zero)
|
if (address == nint.Zero)
|
||||||
throw new ArgumentException("Address cannot be zero.", nameof(address));
|
throw new ArgumentException("Address cannot be zero.", nameof(address));
|
||||||
|
|
||||||
var timeoutAt = timeOutMs > 0 ? Environment.TickCount64 + timeOutMs : long.MaxValue;
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var loadState = await _framework.RunOnFrameworkThread(() => GetObjectLoadState(address)).ConfigureAwait(false);
|
var isLoaded = await _framework.RunOnFrameworkThread(() => IsObjectFullyLoaded(address)).ConfigureAwait(false);
|
||||||
if (!loadState.IsValid)
|
if (isLoaded)
|
||||||
return false;
|
return;
|
||||||
|
|
||||||
if (!IsZoning && loadState.IsLoaded)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (Environment.TickCount64 >= timeoutAt)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -343,8 +296,11 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
public Task StopAsync(CancellationToken cancellationToken)
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
DisposeHooks();
|
DisposeHooks();
|
||||||
ClearTrackingState();
|
_activePlayers.Clear();
|
||||||
_mediator.UnsubscribeAll(this);
|
_actorsByHash.Clear();
|
||||||
|
_actorsByName.Clear();
|
||||||
|
_ownedTracker.Reset();
|
||||||
|
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,7 +336,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
_onCompanionTerminateHook.Enable();
|
_onCompanionTerminateHook.Enable();
|
||||||
|
|
||||||
_hooksActive = true;
|
_hooksActive = true;
|
||||||
_logger.LogTrace("ActorObjectService hooks enabled.");
|
_logger.LogDebug("ActorObjectService hooks enabled.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task WarmupExistingActors()
|
private Task WarmupExistingActors()
|
||||||
@@ -394,21 +350,36 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
|
|
||||||
private unsafe void OnCharacterInitialized(Character* chara)
|
private unsafe void OnCharacterInitialized(Character* chara)
|
||||||
{
|
{
|
||||||
ExecuteOriginal(() => _onInitializeHook!.Original(chara), "Error invoking original character initialize.");
|
try
|
||||||
QueueTrack((GameObject*)chara);
|
{
|
||||||
|
_onInitializeHook!.Original(chara);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error invoking original character initialize.");
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueFrameworkUpdate(() => TrackGameObject((GameObject*)chara));
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void OnCharacterTerminated(Character* chara)
|
private unsafe void OnCharacterTerminated(Character* chara)
|
||||||
{
|
{
|
||||||
var address = (nint)chara;
|
var address = (nint)chara;
|
||||||
QueueUntrack(address);
|
QueueFrameworkUpdate(() => UntrackGameObject(address));
|
||||||
ExecuteOriginal(() => _onTerminateHook!.Original(chara), "Error invoking original character terminate.");
|
try
|
||||||
|
{
|
||||||
|
_onTerminateHook!.Original(chara);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error invoking original character terminate.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe GameObject* OnCharacterDisposed(Character* chara, byte freeMemory)
|
private unsafe GameObject* OnCharacterDisposed(Character* chara, byte freeMemory)
|
||||||
{
|
{
|
||||||
var address = (nint)chara;
|
var address = (nint)chara;
|
||||||
QueueUntrack(address);
|
QueueFrameworkUpdate(() => UntrackGameObject(address));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return _onDestructorHook!.Original(chara, freeMemory);
|
return _onDestructorHook!.Original(chara, freeMemory);
|
||||||
@@ -445,7 +416,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
|
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
if (_logger.IsEnabled(LogLevel.Debug))
|
||||||
{
|
{
|
||||||
_logger.LogTrace("Actor tracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind} local={Local} gpose={Gpose}",
|
_logger.LogDebug("Actor tracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind} local={Local} gpose={Gpose}",
|
||||||
descriptor.Name,
|
descriptor.Name,
|
||||||
descriptor.Address,
|
descriptor.Address,
|
||||||
descriptor.ObjectIndex,
|
descriptor.ObjectIndex,
|
||||||
@@ -507,169 +478,50 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
return (isLocalPlayer ? LightlessObjectKind.Player : null, entityId);
|
return (isLocalPlayer ? LightlessObjectKind.Player : null, entityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var ownerId = ResolveOwnerId(gameObject);
|
if (isLocalPlayer)
|
||||||
var localPlayerAddress = _objectTable.LocalPlayer?.Address ?? nint.Zero;
|
|
||||||
if (localPlayerAddress == nint.Zero)
|
|
||||||
return (null, ownerId);
|
|
||||||
|
|
||||||
var localEntityId = ((Character*)localPlayerAddress)->EntityId;
|
|
||||||
if (localEntityId == 0)
|
|
||||||
return (null, ownerId);
|
|
||||||
|
|
||||||
if (objectKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
|
|
||||||
{
|
{
|
||||||
var expectedMinionOrMount = GetMinionOrMountAddress(localPlayerAddress, localEntityId);
|
var entityId = ((Character*)gameObject)->EntityId;
|
||||||
if (expectedMinionOrMount != nint.Zero
|
return (LightlessObjectKind.Player, entityId);
|
||||||
&& (nint)gameObject == expectedMinionOrMount
|
}
|
||||||
&& IsPlayerRelatedOwnedAddress(expectedMinionOrMount, LightlessObjectKind.MinionOrMount))
|
|
||||||
|
if (_objectTable.LocalPlayer is not { } localPlayer)
|
||||||
|
return (null, 0);
|
||||||
|
|
||||||
|
var ownerId = gameObject->OwnerId;
|
||||||
|
if (ownerId == 0)
|
||||||
|
{
|
||||||
|
var character = (Character*)gameObject;
|
||||||
|
if (character != null)
|
||||||
{
|
{
|
||||||
var resolvedOwner = ownerId != 0 ? ownerId : localEntityId;
|
ownerId = character->CompanionOwnerId;
|
||||||
return (LightlessObjectKind.MinionOrMount, resolvedOwner);
|
if (ownerId == 0)
|
||||||
|
{
|
||||||
|
var parent = character->GetParentCharacter();
|
||||||
|
if (parent != null)
|
||||||
|
{
|
||||||
|
ownerId = parent->EntityId;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (objectKind != DalamudObjectKind.BattleNpc)
|
if (ownerId == 0 || ownerId != localPlayer.EntityId)
|
||||||
return (null, ownerId);
|
return (null, ownerId);
|
||||||
|
|
||||||
if (ownerId != localEntityId)
|
var ownedKind = objectKind switch
|
||||||
return (null, ownerId);
|
|
||||||
|
|
||||||
var expectedPet = GetPetAddress(localPlayerAddress, localEntityId);
|
|
||||||
if (expectedPet != nint.Zero
|
|
||||||
&& (nint)gameObject == expectedPet
|
|
||||||
&& IsPlayerRelatedOwnedAddress(expectedPet, LightlessObjectKind.Pet))
|
|
||||||
return (LightlessObjectKind.Pet, ownerId);
|
|
||||||
|
|
||||||
var expectedCompanion = GetCompanionAddress(localPlayerAddress, localEntityId);
|
|
||||||
if (expectedCompanion != nint.Zero
|
|
||||||
&& (nint)gameObject == expectedCompanion
|
|
||||||
&& IsPlayerRelatedOwnedAddress(expectedCompanion, LightlessObjectKind.Companion))
|
|
||||||
return (LightlessObjectKind.Companion, ownerId);
|
|
||||||
|
|
||||||
return (null, ownerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsPlayerRelatedOwnedAddress(nint address, LightlessObjectKind expectedKind)
|
|
||||||
{
|
|
||||||
if (address == nint.Zero)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
lock (_playerRelatedHandlerLock)
|
|
||||||
{
|
{
|
||||||
foreach (var handler in _playerRelatedHandlers)
|
DalamudObjectKind.MountType => LightlessObjectKind.MinionOrMount,
|
||||||
|
DalamudObjectKind.Companion => LightlessObjectKind.MinionOrMount,
|
||||||
|
DalamudObjectKind.BattleNpc => gameObject->BattleNpcSubKind switch
|
||||||
{
|
{
|
||||||
if (handler.Address == address && handler.ObjectKind == expectedKind)
|
BattleNpcSubKind.Buddy => LightlessObjectKind.Companion,
|
||||||
return true;
|
BattleNpcSubKind.Pet => LightlessObjectKind.Pet,
|
||||||
}
|
_ => (LightlessObjectKind?)null,
|
||||||
}
|
},
|
||||||
|
_ => (LightlessObjectKind?)null,
|
||||||
|
};
|
||||||
|
|
||||||
return false;
|
return (ownedKind, ownerId);
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe nint GetMinionOrMountAddress(nint localPlayerAddress, uint ownerEntityId)
|
|
||||||
{
|
|
||||||
if (localPlayerAddress == nint.Zero)
|
|
||||||
return nint.Zero;
|
|
||||||
|
|
||||||
if (ownerEntityId == 0)
|
|
||||||
return nint.Zero;
|
|
||||||
|
|
||||||
var playerObject = (GameObject*)localPlayerAddress;
|
|
||||||
var candidateAddress = _objectTable.GetObjectAddress(playerObject->ObjectIndex + 1);
|
|
||||||
if (candidateAddress == nint.Zero)
|
|
||||||
return nint.Zero;
|
|
||||||
|
|
||||||
var candidate = (GameObject*)candidateAddress;
|
|
||||||
var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
|
|
||||||
return candidateKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion
|
|
||||||
? candidateAddress
|
|
||||||
: nint.Zero;
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe nint GetPetAddress(nint localPlayerAddress, uint ownerEntityId)
|
|
||||||
{
|
|
||||||
if (localPlayerAddress == nint.Zero || ownerEntityId == 0)
|
|
||||||
return nint.Zero;
|
|
||||||
|
|
||||||
var manager = CharacterManager.Instance();
|
|
||||||
if (manager != null)
|
|
||||||
{
|
|
||||||
var candidate = (nint)manager->LookupPetByOwnerObject((BattleChara*)localPlayerAddress);
|
|
||||||
if (candidate != nint.Zero)
|
|
||||||
{
|
|
||||||
var candidateObj = (GameObject*)candidate;
|
|
||||||
if (IsPetMatch(candidateObj, ownerEntityId))
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nint.Zero;
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe nint GetCompanionAddress(nint localPlayerAddress, uint ownerEntityId)
|
|
||||||
{
|
|
||||||
if (localPlayerAddress == nint.Zero || ownerEntityId == 0)
|
|
||||||
return nint.Zero;
|
|
||||||
|
|
||||||
var manager = CharacterManager.Instance();
|
|
||||||
if (manager != null)
|
|
||||||
{
|
|
||||||
var candidate = (nint)manager->LookupBuddyByOwnerObject((BattleChara*)localPlayerAddress);
|
|
||||||
if (candidate != nint.Zero)
|
|
||||||
{
|
|
||||||
var candidateObj = (GameObject*)candidate;
|
|
||||||
if (IsCompanionMatch(candidateObj, ownerEntityId))
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nint.Zero;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static unsafe bool IsPetMatch(GameObject* candidate, uint ownerEntityId)
|
|
||||||
{
|
|
||||||
if (candidate == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if ((DalamudObjectKind)candidate->ObjectKind != DalamudObjectKind.BattleNpc)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (candidate->BattleNpcSubKind != BattleNpcSubKind.Pet)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return ResolveOwnerId(candidate) == ownerEntityId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static unsafe bool IsCompanionMatch(GameObject* candidate, uint ownerEntityId)
|
|
||||||
{
|
|
||||||
if (candidate == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if ((DalamudObjectKind)candidate->ObjectKind != DalamudObjectKind.BattleNpc)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (candidate->BattleNpcSubKind != BattleNpcSubKind.Buddy)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return ResolveOwnerId(candidate) == ownerEntityId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static unsafe uint ResolveOwnerId(GameObject* gameObject)
|
|
||||||
{
|
|
||||||
if (gameObject == null)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
if (gameObject->OwnerId != 0)
|
|
||||||
return gameObject->OwnerId;
|
|
||||||
|
|
||||||
var character = (Character*)gameObject;
|
|
||||||
if (character == null)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
if (character->CompanionOwnerId != 0)
|
|
||||||
return character->CompanionOwnerId;
|
|
||||||
|
|
||||||
var parent = character->GetParentCharacter();
|
|
||||||
return parent != null ? parent->EntityId : 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UntrackGameObject(nint address)
|
private void UntrackGameObject(nint address)
|
||||||
@@ -682,7 +534,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
RemoveDescriptor(descriptor);
|
RemoveDescriptor(descriptor);
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
if (_logger.IsEnabled(LogLevel.Debug))
|
||||||
{
|
{
|
||||||
_logger.LogTrace("Actor untracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind}",
|
_logger.LogDebug("Actor untracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind}",
|
||||||
descriptor.Name,
|
descriptor.Name,
|
||||||
descriptor.Address,
|
descriptor.Address,
|
||||||
descriptor.ObjectIndex,
|
descriptor.ObjectIndex,
|
||||||
@@ -706,14 +558,10 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
if (!seen.Add(address))
|
if (!seen.Add(address))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var gameObject = (GameObject*)address;
|
if (_activePlayers.ContainsKey(address))
|
||||||
if (_activePlayers.TryGetValue(address, out var existing))
|
|
||||||
{
|
|
||||||
RefreshDescriptorIfNeeded(existing, gameObject);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
TrackGameObject(gameObject);
|
TrackGameObject((GameObject*)address);
|
||||||
}
|
}
|
||||||
|
|
||||||
var stale = _activePlayers.Keys.Where(addr => !seen.Contains(addr)).ToList();
|
var stale = _activePlayers.Keys.Where(addr => !seen.Contains(addr)).ToList();
|
||||||
@@ -726,47 +574,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
{
|
{
|
||||||
_nextRefreshAllowed = DateTime.UtcNow + SnapshotRefreshInterval;
|
_nextRefreshAllowed = DateTime.UtcNow + SnapshotRefreshInterval;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_clientState.IsGPosing)
|
|
||||||
{
|
|
||||||
RefreshGposeActorsInternal();
|
|
||||||
}
|
|
||||||
else if (!_gposePlayers.IsEmpty)
|
|
||||||
{
|
|
||||||
_gposePlayers.Clear();
|
|
||||||
PublishGposeSnapshot();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe void RefreshDescriptorIfNeeded(ActorDescriptor existing, GameObject* gameObject)
|
|
||||||
{
|
|
||||||
if (gameObject == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (existing.ObjectKind != DalamudObjectKind.Player || !string.IsNullOrEmpty(existing.HashedContentId))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var objectKind = (DalamudObjectKind)gameObject->ObjectKind;
|
|
||||||
if (!IsSupportedObjectKind(objectKind))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (BuildDescriptor(gameObject, objectKind) is not { } updated)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(updated.HashedContentId))
|
|
||||||
return;
|
|
||||||
|
|
||||||
ReplaceDescriptor(existing, updated);
|
|
||||||
_mediator.Publish(new ActorTrackedMessage(updated));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReplaceDescriptor(ActorDescriptor existing, ActorDescriptor updated)
|
|
||||||
{
|
|
||||||
RemoveDescriptorFromIndexes(existing);
|
|
||||||
_activePlayers[updated.Address] = updated;
|
|
||||||
IndexDescriptor(updated);
|
|
||||||
UpdatePendingHashResolutions(updated);
|
|
||||||
PublishSnapshot();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void IndexDescriptor(ActorDescriptor descriptor)
|
private void IndexDescriptor(ActorDescriptor descriptor)
|
||||||
@@ -798,15 +605,30 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
|
|
||||||
private unsafe void OnCompanionInitialized(Companion* companion)
|
private unsafe void OnCompanionInitialized(Companion* companion)
|
||||||
{
|
{
|
||||||
ExecuteOriginal(() => _onCompanionInitializeHook!.Original(companion), "Error invoking original companion initialize.");
|
try
|
||||||
QueueTrack((GameObject*)companion);
|
{
|
||||||
|
_onCompanionInitializeHook!.Original(companion);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error invoking original companion initialize.");
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueFrameworkUpdate(() => TrackGameObject((GameObject*)companion));
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void OnCompanionTerminated(Companion* companion)
|
private unsafe void OnCompanionTerminated(Companion* companion)
|
||||||
{
|
{
|
||||||
var address = (nint)companion;
|
var address = (nint)companion;
|
||||||
QueueUntrack(address);
|
QueueFrameworkUpdate(() => UntrackGameObject(address));
|
||||||
ExecuteOriginal(() => _onCompanionTerminateHook!.Original(companion), "Error invoking original companion terminate.");
|
try
|
||||||
|
{
|
||||||
|
_onCompanionTerminateHook!.Original(companion);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error invoking original companion terminate.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveDescriptorFromIndexes(ActorDescriptor descriptor)
|
private void RemoveDescriptorFromIndexes(ActorDescriptor descriptor)
|
||||||
@@ -832,122 +654,29 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
{
|
{
|
||||||
_activePlayers[descriptor.Address] = descriptor;
|
_activePlayers[descriptor.Address] = descriptor;
|
||||||
IndexDescriptor(descriptor);
|
IndexDescriptor(descriptor);
|
||||||
UpdatePendingHashResolutions(descriptor);
|
_ownedTracker.OnDescriptorAdded(descriptor);
|
||||||
PublishSnapshot();
|
PublishSnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveDescriptor(ActorDescriptor descriptor)
|
private void RemoveDescriptor(ActorDescriptor descriptor)
|
||||||
{
|
{
|
||||||
RemoveDescriptorFromIndexes(descriptor);
|
RemoveDescriptorFromIndexes(descriptor);
|
||||||
_pendingHashResolutions.TryRemove(descriptor.Address, out _);
|
_ownedTracker.OnDescriptorRemoved(descriptor);
|
||||||
PublishSnapshot();
|
PublishSnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdatePendingHashResolutions(ActorDescriptor descriptor)
|
|
||||||
{
|
|
||||||
if (descriptor.ObjectKind != DalamudObjectKind.Player)
|
|
||||||
{
|
|
||||||
_pendingHashResolutions.TryRemove(descriptor.Address, out _);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(descriptor.HashedContentId))
|
|
||||||
{
|
|
||||||
_pendingHashResolutions[descriptor.Address] = 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_pendingHashResolutions.TryRemove(descriptor.Address, out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PublishSnapshot()
|
private void PublishSnapshot()
|
||||||
{
|
{
|
||||||
var descriptors = _activePlayers.Values.ToArray();
|
var playerDescriptors = _activePlayers.Values
|
||||||
var playerCount = 0;
|
.Where(descriptor => descriptor.ObjectKind == DalamudObjectKind.Player)
|
||||||
var ownedCount = 0;
|
.ToArray();
|
||||||
var companionCount = 0;
|
var playerAddresses = new nint[playerDescriptors.Length];
|
||||||
|
for (var i = 0; i < playerDescriptors.Length; i++)
|
||||||
|
playerAddresses[i] = playerDescriptors[i].Address;
|
||||||
|
|
||||||
foreach (var descriptor in descriptors)
|
var ownedSnapshot = _ownedTracker.CreateSnapshot();
|
||||||
{
|
|
||||||
if (descriptor.ObjectKind == DalamudObjectKind.Player)
|
|
||||||
playerCount++;
|
|
||||||
|
|
||||||
if (descriptor.OwnedKind is not null)
|
|
||||||
ownedCount++;
|
|
||||||
|
|
||||||
if (descriptor.ObjectKind == DalamudObjectKind.Companion)
|
|
||||||
companionCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
var playerDescriptors = new ActorDescriptor[playerCount];
|
|
||||||
var ownedDescriptors = new ActorDescriptor[ownedCount];
|
|
||||||
var playerAddresses = new nint[playerCount];
|
|
||||||
var renderedCompanions = new nint[companionCount];
|
|
||||||
var ownedAddresses = new nint[ownedCount];
|
|
||||||
var ownedMap = new Dictionary<nint, LightlessObjectKind>(ownedCount);
|
|
||||||
nint localPlayer = nint.Zero;
|
|
||||||
nint localPet = nint.Zero;
|
|
||||||
nint localMinionOrMount = nint.Zero;
|
|
||||||
nint localCompanion = nint.Zero;
|
|
||||||
|
|
||||||
var playerIndex = 0;
|
|
||||||
var ownedIndex = 0;
|
|
||||||
var companionIndex = 0;
|
|
||||||
|
|
||||||
foreach (var descriptor in descriptors)
|
|
||||||
{
|
|
||||||
if (descriptor.ObjectKind == DalamudObjectKind.Player)
|
|
||||||
{
|
|
||||||
playerDescriptors[playerIndex] = descriptor;
|
|
||||||
playerAddresses[playerIndex] = descriptor.Address;
|
|
||||||
playerIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (descriptor.ObjectKind == DalamudObjectKind.Companion)
|
|
||||||
{
|
|
||||||
renderedCompanions[companionIndex] = descriptor.Address;
|
|
||||||
companionIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (descriptor.OwnedKind is not { } ownedKind)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ownedDescriptors[ownedIndex] = descriptor;
|
|
||||||
ownedAddresses[ownedIndex] = descriptor.Address;
|
|
||||||
ownedMap[descriptor.Address] = ownedKind;
|
|
||||||
|
|
||||||
switch (ownedKind)
|
|
||||||
{
|
|
||||||
case LightlessObjectKind.Player:
|
|
||||||
localPlayer = descriptor.Address;
|
|
||||||
break;
|
|
||||||
case LightlessObjectKind.Pet:
|
|
||||||
localPet = descriptor.Address;
|
|
||||||
break;
|
|
||||||
case LightlessObjectKind.MinionOrMount:
|
|
||||||
localMinionOrMount = descriptor.Address;
|
|
||||||
break;
|
|
||||||
case LightlessObjectKind.Companion:
|
|
||||||
localCompanion = descriptor.Address;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
ownedIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ownedSnapshot = new OwnedObjectSnapshot(
|
|
||||||
playerAddresses,
|
|
||||||
renderedCompanions,
|
|
||||||
ownedAddresses,
|
|
||||||
ownedMap,
|
|
||||||
localPlayer,
|
|
||||||
localPet,
|
|
||||||
localMinionOrMount,
|
|
||||||
localCompanion);
|
|
||||||
var nextGeneration = Snapshot.Generation + 1;
|
var nextGeneration = Snapshot.Generation + 1;
|
||||||
var snapshot = new ActorSnapshot(playerDescriptors, ownedDescriptors, playerAddresses, ownedSnapshot, nextGeneration);
|
var snapshot = new ActorSnapshot(playerDescriptors, playerAddresses, ownedSnapshot, nextGeneration);
|
||||||
Volatile.Write(ref _snapshot, snapshot);
|
Volatile.Write(ref _snapshot, snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -965,24 +694,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
_ = _framework.RunOnFrameworkThread(action);
|
_ = _framework.RunOnFrameworkThread(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteOriginal(Action action, string errorMessage)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
action();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, errorMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe void QueueTrack(GameObject* gameObject)
|
|
||||||
=> QueueFrameworkUpdate(() => TrackGameObject(gameObject));
|
|
||||||
|
|
||||||
private void QueueUntrack(nint address)
|
|
||||||
=> QueueFrameworkUpdate(() => UntrackGameObject(address));
|
|
||||||
|
|
||||||
private void DisposeHooks()
|
private void DisposeHooks()
|
||||||
{
|
{
|
||||||
var hadHooks = _hooksActive
|
var hadHooks = _hooksActive
|
||||||
@@ -1014,30 +725,13 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
|
|
||||||
if (hadHooks)
|
if (hadHooks)
|
||||||
{
|
{
|
||||||
_logger.LogTrace("ActorObjectService hooks disabled.");
|
_logger.LogDebug("ActorObjectService hooks disabled.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ClearTrackingState()
|
|
||||||
{
|
|
||||||
_activePlayers.Clear();
|
|
||||||
_gposePlayers.Clear();
|
|
||||||
_actorsByHash.Clear();
|
|
||||||
_actorsByName.Clear();
|
|
||||||
_pendingHashResolutions.Clear();
|
|
||||||
lock (_playerRelatedHandlerLock)
|
|
||||||
{
|
|
||||||
_playerRelatedHandlers.Clear();
|
|
||||||
}
|
|
||||||
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
|
|
||||||
Volatile.Write(ref _gposeSnapshot, GposeSnapshot.Empty);
|
|
||||||
_nextRefreshAllowed = DateTime.MinValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
DisposeHooks();
|
DisposeHooks();
|
||||||
_mediator.UnsubscribeAll(this);
|
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1076,101 +770,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void RefreshGposeActorsInternal()
|
|
||||||
{
|
|
||||||
var addresses = EnumerateGposeCharacterAddresses();
|
|
||||||
HashSet<nint> seen = new(addresses.Count);
|
|
||||||
|
|
||||||
foreach (var address in addresses)
|
|
||||||
{
|
|
||||||
if (address == nint.Zero)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!seen.Add(address))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (_gposePlayers.ContainsKey(address))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
TrackGposeObject((GameObject*)address);
|
|
||||||
}
|
|
||||||
|
|
||||||
var stale = _gposePlayers.Keys.Where(addr => !seen.Contains(addr)).ToList();
|
|
||||||
foreach (var staleAddress in stale)
|
|
||||||
{
|
|
||||||
UntrackGposeObject(staleAddress);
|
|
||||||
}
|
|
||||||
|
|
||||||
PublishGposeSnapshot();
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe void TrackGposeObject(GameObject* gameObject)
|
|
||||||
{
|
|
||||||
if (gameObject == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var objectKind = (DalamudObjectKind)gameObject->ObjectKind;
|
|
||||||
if (objectKind != DalamudObjectKind.Player)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (BuildDescriptor(gameObject, objectKind) is not { } descriptor)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!descriptor.IsInGpose)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_gposePlayers[descriptor.Address] = descriptor;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UntrackGposeObject(nint address)
|
|
||||||
{
|
|
||||||
if (address == nint.Zero)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_gposePlayers.TryRemove(address, out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PublishGposeSnapshot()
|
|
||||||
{
|
|
||||||
var gposeDescriptors = _gposePlayers.Values.ToArray();
|
|
||||||
var gposeAddresses = new nint[gposeDescriptors.Length];
|
|
||||||
for (var i = 0; i < gposeDescriptors.Length; i++)
|
|
||||||
gposeAddresses[i] = gposeDescriptors[i].Address;
|
|
||||||
|
|
||||||
var nextGeneration = CurrentGposeSnapshot.Generation + 1;
|
|
||||||
var snapshot = new GposeSnapshot(gposeDescriptors, gposeAddresses, nextGeneration);
|
|
||||||
Volatile.Write(ref _gposeSnapshot, snapshot);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<nint> EnumerateGposeCharacterAddresses()
|
|
||||||
{
|
|
||||||
var results = new List<nint>(16);
|
|
||||||
foreach (var obj in _objectTable)
|
|
||||||
{
|
|
||||||
if (obj.ObjectKind != DalamudObjectKind.Player)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (obj.ObjectIndex < 200)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
results.Add(obj.Address);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
private LoadState GetObjectLoadState(nint address)
|
|
||||||
{
|
|
||||||
if (address == nint.Zero)
|
|
||||||
return LoadState.Invalid;
|
|
||||||
|
|
||||||
var obj = _objectTable.CreateObjectReference(address);
|
|
||||||
if (obj is null || obj.Address != address)
|
|
||||||
return LoadState.Invalid;
|
|
||||||
|
|
||||||
return new LoadState(true, IsObjectFullyLoaded(address));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static unsafe bool IsObjectFullyLoaded(nint address)
|
private static unsafe bool IsObjectFullyLoaded(nint address)
|
||||||
{
|
{
|
||||||
if (address == nint.Zero)
|
if (address == nint.Zero)
|
||||||
@@ -1184,10 +783,13 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
if (drawObject == null)
|
if (drawObject == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if ((ulong)gameObject->RenderFlags == 2048)
|
if ((gameObject->RenderFlags & VisibilityFlags.Nameplate) != VisibilityFlags.None)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var characterBase = (CharacterBase*)drawObject;
|
var characterBase = (CharacterBase*)drawObject;
|
||||||
|
if (characterBase == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
if (characterBase->HasModelInSlotLoaded != 0)
|
if (characterBase->HasModelInSlotLoaded != 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -1197,9 +799,107 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly record struct LoadState(bool IsValid, bool IsLoaded)
|
private sealed class OwnedObjectTracker
|
||||||
{
|
{
|
||||||
public static LoadState Invalid => new(false, false);
|
private readonly HashSet<nint> _renderedPlayers = new();
|
||||||
|
private readonly HashSet<nint> _renderedCompanions = new();
|
||||||
|
private readonly Dictionary<nint, LightlessObjectKind> _ownedObjects = new();
|
||||||
|
private nint _localPlayerAddress = nint.Zero;
|
||||||
|
private nint _localPetAddress = nint.Zero;
|
||||||
|
private nint _localMinionMountAddress = nint.Zero;
|
||||||
|
private nint _localCompanionAddress = nint.Zero;
|
||||||
|
|
||||||
|
public void OnDescriptorAdded(ActorDescriptor descriptor)
|
||||||
|
{
|
||||||
|
if (descriptor.ObjectKind == DalamudObjectKind.Player)
|
||||||
|
{
|
||||||
|
_renderedPlayers.Add(descriptor.Address);
|
||||||
|
if (descriptor.IsLocalPlayer)
|
||||||
|
_localPlayerAddress = descriptor.Address;
|
||||||
|
}
|
||||||
|
else if (descriptor.ObjectKind == DalamudObjectKind.Companion)
|
||||||
|
{
|
||||||
|
_renderedCompanions.Add(descriptor.Address);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (descriptor.OwnedKind is { } ownedKind)
|
||||||
|
{
|
||||||
|
_ownedObjects[descriptor.Address] = ownedKind;
|
||||||
|
switch (ownedKind)
|
||||||
|
{
|
||||||
|
case LightlessObjectKind.Player:
|
||||||
|
_localPlayerAddress = descriptor.Address;
|
||||||
|
break;
|
||||||
|
case LightlessObjectKind.Pet:
|
||||||
|
_localPetAddress = descriptor.Address;
|
||||||
|
break;
|
||||||
|
case LightlessObjectKind.MinionOrMount:
|
||||||
|
_localMinionMountAddress = descriptor.Address;
|
||||||
|
break;
|
||||||
|
case LightlessObjectKind.Companion:
|
||||||
|
_localCompanionAddress = descriptor.Address;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnDescriptorRemoved(ActorDescriptor descriptor)
|
||||||
|
{
|
||||||
|
if (descriptor.ObjectKind == DalamudObjectKind.Player)
|
||||||
|
{
|
||||||
|
_renderedPlayers.Remove(descriptor.Address);
|
||||||
|
if (descriptor.IsLocalPlayer && _localPlayerAddress == descriptor.Address)
|
||||||
|
_localPlayerAddress = nint.Zero;
|
||||||
|
}
|
||||||
|
else if (descriptor.ObjectKind == DalamudObjectKind.Companion)
|
||||||
|
{
|
||||||
|
_renderedCompanions.Remove(descriptor.Address);
|
||||||
|
if (_localCompanionAddress == descriptor.Address)
|
||||||
|
_localCompanionAddress = nint.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (descriptor.OwnedKind is { } ownedKind)
|
||||||
|
{
|
||||||
|
_ownedObjects.Remove(descriptor.Address);
|
||||||
|
switch (ownedKind)
|
||||||
|
{
|
||||||
|
case LightlessObjectKind.Player when _localPlayerAddress == descriptor.Address:
|
||||||
|
_localPlayerAddress = nint.Zero;
|
||||||
|
break;
|
||||||
|
case LightlessObjectKind.Pet when _localPetAddress == descriptor.Address:
|
||||||
|
_localPetAddress = nint.Zero;
|
||||||
|
break;
|
||||||
|
case LightlessObjectKind.MinionOrMount when _localMinionMountAddress == descriptor.Address:
|
||||||
|
_localMinionMountAddress = nint.Zero;
|
||||||
|
break;
|
||||||
|
case LightlessObjectKind.Companion when _localCompanionAddress == descriptor.Address:
|
||||||
|
_localCompanionAddress = nint.Zero;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public OwnedObjectSnapshot CreateSnapshot()
|
||||||
|
=> new(
|
||||||
|
_renderedPlayers.ToArray(),
|
||||||
|
_renderedCompanions.ToArray(),
|
||||||
|
_ownedObjects.Keys.ToArray(),
|
||||||
|
new Dictionary<nint, LightlessObjectKind>(_ownedObjects),
|
||||||
|
_localPlayerAddress,
|
||||||
|
_localPetAddress,
|
||||||
|
_localMinionMountAddress,
|
||||||
|
_localCompanionAddress);
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
_renderedPlayers.Clear();
|
||||||
|
_renderedCompanions.Clear();
|
||||||
|
_ownedObjects.Clear();
|
||||||
|
_localPlayerAddress = nint.Zero;
|
||||||
|
_localPetAddress = nint.Zero;
|
||||||
|
_localMinionMountAddress = nint.Zero;
|
||||||
|
_localCompanionAddress = nint.Zero;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record OwnedObjectSnapshot(
|
private sealed record OwnedObjectSnapshot(
|
||||||
@@ -1225,27 +925,14 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
|
|
||||||
private sealed record ActorSnapshot(
|
private sealed record ActorSnapshot(
|
||||||
IReadOnlyList<ActorDescriptor> PlayerDescriptors,
|
IReadOnlyList<ActorDescriptor> PlayerDescriptors,
|
||||||
IReadOnlyList<ActorDescriptor> OwnedDescriptors,
|
|
||||||
IReadOnlyList<nint> PlayerAddresses,
|
IReadOnlyList<nint> PlayerAddresses,
|
||||||
OwnedObjectSnapshot OwnedObjects,
|
OwnedObjectSnapshot OwnedObjects,
|
||||||
int Generation)
|
int Generation)
|
||||||
{
|
{
|
||||||
public static ActorSnapshot Empty { get; } = new(
|
public static ActorSnapshot Empty { get; } = new(
|
||||||
Array.Empty<ActorDescriptor>(),
|
|
||||||
Array.Empty<ActorDescriptor>(),
|
Array.Empty<ActorDescriptor>(),
|
||||||
Array.Empty<nint>(),
|
Array.Empty<nint>(),
|
||||||
OwnedObjectSnapshot.Empty,
|
OwnedObjectSnapshot.Empty,
|
||||||
0);
|
0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record GposeSnapshot(
|
|
||||||
IReadOnlyList<ActorDescriptor> GposeDescriptors,
|
|
||||||
IReadOnlyList<nint> GposeAddresses,
|
|
||||||
int Generation)
|
|
||||||
{
|
|
||||||
public static GposeSnapshot Empty { get; } = new(
|
|
||||||
Array.Empty<ActorDescriptor>(),
|
|
||||||
Array.Empty<nint>(),
|
|
||||||
0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
using K4os.Compression.LZ4.Legacy;
|
using K4os.Compression.LZ4.Legacy;
|
||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Data.Enum;
|
using LightlessSync.API.Data.Enum;
|
||||||
using LightlessSync.API.Dto.CharaData;
|
using LightlessSync.API.Dto.CharaData;
|
||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using LightlessSync.LightlessConfiguration.Models;
|
|
||||||
using LightlessSync.PlayerData.Factories;
|
using LightlessSync.PlayerData.Factories;
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
using LightlessSync.Services.CharaData;
|
using LightlessSync.Services.CharaData;
|
||||||
using LightlessSync.Services.CharaData.Models;
|
using LightlessSync.Services.CharaData.Models;
|
||||||
using LightlessSync.Services.Mediator;
|
|
||||||
using LightlessSync.UI.Models;
|
|
||||||
using LightlessSync.Utils;
|
using LightlessSync.Utils;
|
||||||
using LightlessSync.WebAPI.Files;
|
using LightlessSync.WebAPI.Files;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -27,11 +24,10 @@ public sealed class CharaDataFileHandler : IDisposable
|
|||||||
private readonly ILogger<CharaDataFileHandler> _logger;
|
private readonly ILogger<CharaDataFileHandler> _logger;
|
||||||
private readonly LightlessCharaFileDataFactory _lightlessCharaFileDataFactory;
|
private readonly LightlessCharaFileDataFactory _lightlessCharaFileDataFactory;
|
||||||
private readonly PlayerDataFactory _playerDataFactory;
|
private readonly PlayerDataFactory _playerDataFactory;
|
||||||
private readonly NotificationService _notificationService;
|
|
||||||
private int _globalFileCounter = 0;
|
private int _globalFileCounter = 0;
|
||||||
|
|
||||||
public CharaDataFileHandler(ILogger<CharaDataFileHandler> logger, FileDownloadManagerFactory fileDownloadManagerFactory, FileUploadManager fileUploadManager, FileCacheManager fileCacheManager,
|
public CharaDataFileHandler(ILogger<CharaDataFileHandler> logger, FileDownloadManagerFactory fileDownloadManagerFactory, FileUploadManager fileUploadManager, FileCacheManager fileCacheManager,
|
||||||
DalamudUtilService dalamudUtilService, GameObjectHandlerFactory gameObjectHandlerFactory, PlayerDataFactory playerDataFactory, NotificationService notificationService)
|
DalamudUtilService dalamudUtilService, GameObjectHandlerFactory gameObjectHandlerFactory, PlayerDataFactory playerDataFactory)
|
||||||
{
|
{
|
||||||
_fileDownloadManager = fileDownloadManagerFactory.Create();
|
_fileDownloadManager = fileDownloadManagerFactory.Create();
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -40,7 +36,6 @@ public sealed class CharaDataFileHandler : IDisposable
|
|||||||
_dalamudUtilService = dalamudUtilService;
|
_dalamudUtilService = dalamudUtilService;
|
||||||
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||||
_playerDataFactory = playerDataFactory;
|
_playerDataFactory = playerDataFactory;
|
||||||
_notificationService = notificationService;
|
|
||||||
_lightlessCharaFileDataFactory = new(fileCacheManager);
|
_lightlessCharaFileDataFactory = new(fileCacheManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,161 +248,54 @@ public sealed class CharaDataFileHandler : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal async Task SaveCharaFileAsync(string description, string filePath)
|
internal async Task SaveCharaFileAsync(string description, string filePath)
|
||||||
{
|
|
||||||
var createPlayerDataStopwatch = System.Diagnostics.Stopwatch.StartNew();
|
|
||||||
var data = await CreatePlayerData().ConfigureAwait(false);
|
|
||||||
createPlayerDataStopwatch.Stop();
|
|
||||||
_logger.LogInformation("CreatePlayerData took {elapsed}ms", createPlayerDataStopwatch.ElapsedMilliseconds);
|
|
||||||
|
|
||||||
if (data == null) return;
|
|
||||||
|
|
||||||
await Task.Run(async () => await SaveCharaFileAsyncInternal(description, filePath, data).ConfigureAwait(false)).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SaveCharaFileAsyncInternal(string description, string filePath, CharacterData data)
|
|
||||||
{
|
{
|
||||||
var tempFilePath = filePath + ".tmp";
|
var tempFilePath = filePath + ".tmp";
|
||||||
var overallStopwatch = System.Diagnostics.Stopwatch.StartNew();
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var data = await CreatePlayerData().ConfigureAwait(false);
|
||||||
|
if (data == null) return;
|
||||||
|
|
||||||
var lightlessCharaFileData = _lightlessCharaFileDataFactory.Create(description, data);
|
var lightlessCharaFileData = _lightlessCharaFileDataFactory.Create(description, data);
|
||||||
LightlessCharaFileHeader output = new(LightlessCharaFileHeader.CurrentVersion, lightlessCharaFileData);
|
LightlessCharaFileHeader output = new(LightlessCharaFileHeader.CurrentVersion, lightlessCharaFileData);
|
||||||
|
|
||||||
using var fs = new FileStream(tempFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None, bufferSize: 65536, useAsync: false);
|
using var fs = new FileStream(tempFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
|
||||||
using var lz4 = new LZ4Stream(fs, LZ4StreamMode.Compress, LZ4StreamFlags.HighCompression);
|
using var lz4 = new LZ4Stream(fs, LZ4StreamMode.Compress, LZ4StreamFlags.HighCompression);
|
||||||
using var writer = new BinaryWriter(lz4);
|
using var writer = new BinaryWriter(lz4);
|
||||||
output.WriteToStream(writer);
|
output.WriteToStream(writer);
|
||||||
|
|
||||||
int fileIndex = 0;
|
|
||||||
long totalBytesWritten = 0;
|
|
||||||
long totalBytesToWrite = output.CharaFileData.Files.Sum(f => f.Length);
|
|
||||||
var fileWriteStopwatch = System.Diagnostics.Stopwatch.StartNew();
|
|
||||||
const long updateIntervalMs = 1000;
|
|
||||||
|
|
||||||
foreach (var item in output.CharaFileData.Files)
|
foreach (var item in output.CharaFileData.Files)
|
||||||
{
|
{
|
||||||
fileIndex++;
|
|
||||||
var fileStopwatch = System.Diagnostics.Stopwatch.StartNew();
|
|
||||||
|
|
||||||
var file = _fileCacheManager.GetFileCacheByHash(item.Hash)!;
|
var file = _fileCacheManager.GetFileCacheByHash(item.Hash)!;
|
||||||
_logger.LogDebug("Saving to MCDF [{fileNum}/{totalFiles}]: {hash}:{file}", fileIndex, output.CharaFileData.Files.Count, item.Hash, file.ResolvedFilepath);
|
_logger.LogDebug("Saving to MCDF: {hash}:{file}", item.Hash, file.ResolvedFilepath);
|
||||||
_logger.LogDebug("\tAssociated GamePaths:");
|
_logger.LogDebug("\tAssociated GamePaths:");
|
||||||
foreach (var path in item.GamePaths)
|
foreach (var path in item.GamePaths)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("\t{path}", path);
|
_logger.LogDebug("\t{path}", path);
|
||||||
}
|
}
|
||||||
|
|
||||||
using var fsRead = File.OpenRead(file.ResolvedFilepath);
|
var fsRead = File.OpenRead(file.ResolvedFilepath);
|
||||||
using var br = new BinaryReader(fsRead);
|
await using (fsRead.ConfigureAwait(false))
|
||||||
byte[] buffer = new byte[item.Length];
|
|
||||||
int bytesRead = br.Read(buffer, 0, item.Length);
|
|
||||||
|
|
||||||
if (bytesRead != item.Length)
|
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Expected to read {expected} bytes but got {actual} bytes from {file}", item.Length, bytesRead, file.ResolvedFilepath);
|
using var br = new BinaryReader(fsRead);
|
||||||
}
|
byte[] buffer = new byte[item.Length];
|
||||||
|
br.Read(buffer, 0, item.Length);
|
||||||
writer.Write(buffer);
|
writer.Write(buffer);
|
||||||
totalBytesWritten += bytesRead;
|
|
||||||
|
|
||||||
fileStopwatch.Stop();
|
|
||||||
_logger.LogDebug("Wrote file [{fileNum}/{totalFiles}] in {elapsed}ms ({sizeKb}kb)", fileIndex, output.CharaFileData.Files.Count, fileStopwatch.ElapsedMilliseconds, item.Length / 1024);
|
|
||||||
|
|
||||||
if (fileWriteStopwatch.ElapsedMilliseconds >= updateIntervalMs && totalBytesToWrite > 0)
|
|
||||||
{
|
|
||||||
float progress = (float)totalBytesWritten / totalBytesToWrite;
|
|
||||||
var elapsed = overallStopwatch.Elapsed;
|
|
||||||
var eta = CalculateEta(elapsed, progress);
|
|
||||||
|
|
||||||
var notification = new LightlessNotification
|
|
||||||
{
|
|
||||||
Id = "chara_file_save_progress",
|
|
||||||
Title = "Character Data",
|
|
||||||
Message = $"Compressing and saving character file... {(progress * 100):F0}%\nETA: {FormatTimespan(eta)}",
|
|
||||||
Type = NotificationType.Info,
|
|
||||||
Duration = TimeSpan.FromMinutes(5),
|
|
||||||
ShowProgress = true,
|
|
||||||
Progress = progress
|
|
||||||
};
|
|
||||||
|
|
||||||
_notificationService.Mediator.Publish(new LightlessNotificationMessage(notification));
|
|
||||||
|
|
||||||
fileWriteStopwatch.Restart();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var flushStopwatch = System.Diagnostics.Stopwatch.StartNew();
|
|
||||||
writer.Flush();
|
writer.Flush();
|
||||||
lz4.Flush();
|
await lz4.FlushAsync().ConfigureAwait(false);
|
||||||
fs.Flush();
|
await fs.FlushAsync().ConfigureAwait(false);
|
||||||
fs.Close();
|
fs.Close();
|
||||||
flushStopwatch.Stop();
|
|
||||||
_logger.LogInformation("Flush operations took {elapsed}ms", flushStopwatch.ElapsedMilliseconds);
|
|
||||||
|
|
||||||
var moveStopwatch = System.Diagnostics.Stopwatch.StartNew();
|
|
||||||
File.Move(tempFilePath, filePath, true);
|
File.Move(tempFilePath, filePath, true);
|
||||||
moveStopwatch.Stop();
|
|
||||||
_logger.LogInformation("File move took {elapsed}ms", moveStopwatch.ElapsedMilliseconds);
|
|
||||||
|
|
||||||
overallStopwatch.Stop();
|
|
||||||
_logger.LogInformation("SaveCharaFileAsync completed successfully in {elapsed}ms. Total bytes written: {totalBytes}mb", overallStopwatch.ElapsedMilliseconds, totalBytesWritten / (1024 * 1024));
|
|
||||||
|
|
||||||
_notificationService.ShowNotification(
|
|
||||||
"Character Data",
|
|
||||||
"Character file saved successfully!",
|
|
||||||
NotificationType.Info,
|
|
||||||
duration: TimeSpan.FromSeconds(5));
|
|
||||||
|
|
||||||
_notificationService.Mediator.Publish(new LightlessNotificationDismissMessage("chara_file_save_progress"));
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
overallStopwatch.Stop();
|
_logger.LogError(ex, "Failure Saving Lightless Chara File, deleting output");
|
||||||
_logger.LogError(ex, "Failure Saving Lightless Chara File after {elapsed}ms, deleting output", overallStopwatch.ElapsedMilliseconds);
|
File.Delete(tempFilePath);
|
||||||
try
|
|
||||||
{
|
|
||||||
File.Delete(tempFilePath);
|
|
||||||
}
|
|
||||||
catch (Exception deleteEx)
|
|
||||||
{
|
|
||||||
_logger.LogError(deleteEx, "Failed to delete temporary file {file}", tempFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
_notificationService.ShowErrorNotification(
|
|
||||||
"Character Data Save Failed",
|
|
||||||
"Failed to save character file",
|
|
||||||
ex);
|
|
||||||
|
|
||||||
_notificationService.Mediator.Publish(new LightlessNotificationDismissMessage("chara_file_save_progress"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TimeSpan CalculateEta(TimeSpan elapsed, float progress)
|
|
||||||
{
|
|
||||||
if (progress <= 0 || elapsed.TotalSeconds < 0.1)
|
|
||||||
return TimeSpan.Zero;
|
|
||||||
|
|
||||||
double totalSeconds = elapsed.TotalSeconds / progress;
|
|
||||||
double remainingSeconds = totalSeconds - elapsed.TotalSeconds;
|
|
||||||
|
|
||||||
return TimeSpan.FromSeconds(Math.Max(0, remainingSeconds));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatTimespan(TimeSpan ts)
|
|
||||||
{
|
|
||||||
if (ts.TotalSeconds < 1)
|
|
||||||
return "< 1s";
|
|
||||||
|
|
||||||
if (ts.TotalSeconds < 60)
|
|
||||||
return $"{ts.TotalSeconds:F0}s";
|
|
||||||
|
|
||||||
if (ts.TotalMinutes < 60)
|
|
||||||
return $"{ts.TotalMinutes:F1}m";
|
|
||||||
|
|
||||||
return $"{ts.TotalHours:F1}h";
|
|
||||||
}
|
|
||||||
|
|
||||||
internal async Task<List<string>> UploadFiles(List<string> fileList, ValueProgress<string> uploadProgress, CancellationToken token)
|
internal async Task<List<string>> UploadFiles(List<string> fileList, ValueProgress<string> uploadProgress, CancellationToken token)
|
||||||
{
|
{
|
||||||
return await _fileUploadManager.UploadFiles(fileList, uploadProgress, token).ConfigureAwait(false);
|
return await _fileUploadManager.UploadFiles(fileList, uploadProgress, token).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -450,7 +450,7 @@ public class CharaDataGposeTogetherManager : DisposableMediatorSubscriberBase
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var loc = _dalamudUtil.GetMapData();
|
var loc = await _dalamudUtil.GetMapDataAsync().ConfigureAwait(false);
|
||||||
worldData.LocationInfo = loc;
|
worldData.LocationInfo = loc;
|
||||||
|
|
||||||
if (_forceResendWorldData || worldData != _lastWorldData)
|
if (_forceResendWorldData || worldData != _lastWorldData)
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
Logger.LogTrace("Attaching World data {data}", worldData);
|
Logger.LogTrace("Attaching World data {data}", worldData);
|
||||||
|
|
||||||
worldData.LocationInfo = _dalamudUtilService.GetMapData();
|
worldData.LocationInfo = await _dalamudUtilService.GetMapDataAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
Logger.LogTrace("World data serialized: {data}", worldData);
|
Logger.LogTrace("World data serialized: {data}", worldData);
|
||||||
|
|
||||||
|
|||||||
@@ -186,8 +186,8 @@ public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase
|
|||||||
var previousPoses = _nearbyData.Keys.ToList();
|
var previousPoses = _nearbyData.Keys.ToList();
|
||||||
_nearbyData.Clear();
|
_nearbyData.Clear();
|
||||||
|
|
||||||
var ownLocation = _dalamudUtilService.GetMapData();
|
var ownLocation = await _dalamudUtilService.RunOnFrameworkThread(() => _dalamudUtilService.GetMapData()).ConfigureAwait(false);
|
||||||
var player = await _dalamudUtilService.GetPlayerCharacterAsync().ConfigureAwait(false);
|
var player = await _dalamudUtilService.RunOnFrameworkThread(() => _dalamudUtilService.GetPlayerCharacter()).ConfigureAwait(false);
|
||||||
var currentServer = player.CurrentWorld;
|
var currentServer = player.CurrentWorld;
|
||||||
var playerPos = player.Position;
|
var playerPos = player.Position;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
|
|
||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Data.Enum;
|
using LightlessSync.API.Data.Enum;
|
||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
@@ -28,7 +27,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
|||||||
{
|
{
|
||||||
_baseAnalysisCts = _baseAnalysisCts.CancelRecreate();
|
_baseAnalysisCts = _baseAnalysisCts.CancelRecreate();
|
||||||
var token = _baseAnalysisCts.Token;
|
var token = _baseAnalysisCts.Token;
|
||||||
_ = Task.Run(async () => await BaseAnalysis(msg.CharacterData, token).ConfigureAwait(false), token);
|
_ = BaseAnalysis(msg.CharacterData, token);
|
||||||
});
|
});
|
||||||
_fileCacheManager = fileCacheManager;
|
_fileCacheManager = fileCacheManager;
|
||||||
_xivDataAnalyzer = modelAnalyzer;
|
_xivDataAnalyzer = modelAnalyzer;
|
||||||
@@ -99,14 +98,12 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
|||||||
_analysisCts = null;
|
_analysisCts = null;
|
||||||
if (print) PrintAnalysis();
|
if (print) PrintAnalysis();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_analysisCts.CancelDispose();
|
_analysisCts.CancelDispose();
|
||||||
_baseAnalysisCts.Dispose();
|
_baseAnalysisCts.Dispose();
|
||||||
}
|
}
|
||||||
|
public async Task UpdateFileEntriesAsync(IEnumerable<string> filePaths, CancellationToken token)
|
||||||
public async Task UpdateFileEntriesAsync(IEnumerable<string> filePaths, CancellationToken token, bool force = false)
|
|
||||||
{
|
{
|
||||||
var normalized = new HashSet<string>(
|
var normalized = new HashSet<string>(
|
||||||
filePaths.Where(path => !string.IsNullOrWhiteSpace(path)),
|
filePaths.Where(path => !string.IsNullOrWhiteSpace(path)),
|
||||||
@@ -115,8 +112,6 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var updated = false;
|
|
||||||
foreach (var objectEntries in LastAnalysis.Values)
|
foreach (var objectEntries in LastAnalysis.Values)
|
||||||
{
|
{
|
||||||
foreach (var entry in objectEntries.Values)
|
foreach (var entry in objectEntries.Values)
|
||||||
@@ -126,28 +121,10 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
await entry.ComputeSizes(_fileCacheManager, token, force).ConfigureAwait(false);
|
await entry.ComputeSizes(_fileCacheManager, token).ConfigureAwait(false);
|
||||||
|
|
||||||
if (string.Equals(entry.FileType, "mdl", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
var sourcePath = entry.FilePaths.FirstOrDefault(path => !string.IsNullOrWhiteSpace(path));
|
|
||||||
if (!string.IsNullOrWhiteSpace(sourcePath))
|
|
||||||
{
|
|
||||||
entry.UpdateTriangles(_xivDataAnalyzer.RefreshTrianglesForPath(entry.Hash, sourcePath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updated = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updated)
|
|
||||||
{
|
|
||||||
RecalculateSummary();
|
|
||||||
Mediator.Publish(new CharacterDataAnalyzedMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task BaseAnalysis(CharacterData charaData, CancellationToken token)
|
private async Task BaseAnalysis(CharacterData charaData, CancellationToken token)
|
||||||
{
|
{
|
||||||
if (string.Equals(charaData.DataHash.Value, _lastDataHash, StringComparison.Ordinal)) return;
|
if (string.Equals(charaData.DataHash.Value, _lastDataHash, StringComparison.Ordinal)) return;
|
||||||
@@ -159,47 +136,29 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
|||||||
{
|
{
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var fileCacheEntries = (await _fileCacheManager
|
var fileCacheEntries = (await _fileCacheManager.GetAllFileCachesByHashAsync(fileEntry.Hash, ignoreCacheEntries: true, validate: false, token).ConfigureAwait(false)).ToList();
|
||||||
.GetAllFileCachesByHashAsync(fileEntry.Hash, ignoreCacheEntries: true, validate: false, token)
|
if (fileCacheEntries.Count == 0) continue;
|
||||||
.ConfigureAwait(false))
|
var filePath = fileCacheEntries[0].ResolvedFilepath;
|
||||||
.ToList();
|
FileInfo fi = new(filePath);
|
||||||
|
string ext = "unk?";
|
||||||
if (fileCacheEntries.Count == 0)
|
try
|
||||||
continue;
|
|
||||||
|
|
||||||
var resolved = fileCacheEntries[0].ResolvedFilepath;
|
|
||||||
|
|
||||||
var extWithDot = Path.GetExtension(resolved);
|
|
||||||
var ext = string.IsNullOrEmpty(extWithDot) ? "unk?" : extWithDot.TrimStart('.');
|
|
||||||
|
|
||||||
var tris = await _xivDataAnalyzer.GetTrianglesByHash(fileEntry.Hash).ConfigureAwait(false);
|
|
||||||
|
|
||||||
var distinctFilePaths = fileCacheEntries
|
|
||||||
.Select(c => c.ResolvedFilepath)
|
|
||||||
.Where(p => !string.IsNullOrWhiteSpace(p))
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
long orig = 0, comp = 0;
|
|
||||||
var first = fileCacheEntries[0];
|
|
||||||
if (first.Size > 0) orig = first.Size.Value;
|
|
||||||
if (first.CompressedSize > 0) comp = first.CompressedSize.Value;
|
|
||||||
|
|
||||||
if (_fileCacheManager.TryGetSizeInfo(fileEntry.Hash, out var cached))
|
|
||||||
{
|
{
|
||||||
if (orig <= 0 && cached.Original > 0) orig = cached.Original;
|
ext = fi.Extension[1..];
|
||||||
if (comp <= 0 && cached.Compressed > 0) comp = cached.Compressed;
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Could not identify extension for {path}", filePath);
|
||||||
|
}
|
||||||
|
var tris = await _xivDataAnalyzer.GetTrianglesByHash(fileEntry.Hash).ConfigureAwait(false);
|
||||||
|
foreach (var entry in fileCacheEntries)
|
||||||
|
{
|
||||||
|
data[fileEntry.Hash] = new FileDataEntry(fileEntry.Hash, ext,
|
||||||
|
[.. fileEntry.GamePaths],
|
||||||
|
[.. fileCacheEntries.Select(c => c.ResolvedFilepath).Distinct(StringComparer.Ordinal)],
|
||||||
|
entry.Size > 0 ? entry.Size.Value : 0,
|
||||||
|
entry.CompressedSize > 0 ? entry.CompressedSize.Value : 0,
|
||||||
|
tris);
|
||||||
}
|
}
|
||||||
|
|
||||||
data[fileEntry.Hash] = new FileDataEntry(
|
|
||||||
fileEntry.Hash,
|
|
||||||
ext,
|
|
||||||
[.. fileEntry.GamePaths],
|
|
||||||
distinctFilePaths,
|
|
||||||
orig,
|
|
||||||
comp,
|
|
||||||
tris,
|
|
||||||
fileCacheEntries);
|
|
||||||
}
|
}
|
||||||
LastAnalysis[obj.Key] = data;
|
LastAnalysis[obj.Key] = data;
|
||||||
}
|
}
|
||||||
@@ -208,7 +167,6 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
|||||||
Mediator.Publish(new CharacterDataAnalyzedMessage());
|
Mediator.Publish(new CharacterDataAnalyzedMessage());
|
||||||
_lastDataHash = charaData.DataHash.Value;
|
_lastDataHash = charaData.DataHash.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RecalculateSummary()
|
private void RecalculateSummary()
|
||||||
{
|
{
|
||||||
var builder = ImmutableDictionary.CreateBuilder<ObjectKind, CharacterAnalysisObjectSummary>();
|
var builder = ImmutableDictionary.CreateBuilder<ObjectKind, CharacterAnalysisObjectSummary>();
|
||||||
@@ -234,7 +192,6 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
|||||||
|
|
||||||
_latestSummary = new CharacterAnalysisSummary(builder.ToImmutable());
|
_latestSummary = new CharacterAnalysisSummary(builder.ToImmutable());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PrintAnalysis()
|
private void PrintAnalysis()
|
||||||
{
|
{
|
||||||
if (LastAnalysis.Count == 0) return;
|
if (LastAnalysis.Count == 0) return;
|
||||||
@@ -278,84 +235,42 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
|||||||
UiSharedService.ByteToString(LastAnalysis.Values.Sum(c => c.Values.Sum(v => v.CompressedSize))));
|
UiSharedService.ByteToString(LastAnalysis.Values.Sum(c => c.Values.Sum(v => v.CompressedSize))));
|
||||||
Logger.LogInformation("IMPORTANT NOTES:\n\r- For Lightless up- and downloads only the compressed size is relevant.\n\r- An unusually high total files count beyond 200 and up will also increase your download time to others significantly.");
|
Logger.LogInformation("IMPORTANT NOTES:\n\r- For Lightless up- and downloads only the compressed size is relevant.\n\r- An unusually high total files count beyond 200 and up will also increase your download time to others significantly.");
|
||||||
}
|
}
|
||||||
|
internal sealed record FileDataEntry(string Hash, string FileType, List<string> GamePaths, List<string> FilePaths, long OriginalSize, long CompressedSize, long Triangles)
|
||||||
internal sealed class FileDataEntry
|
|
||||||
{
|
{
|
||||||
public string Hash { get; }
|
|
||||||
public string FileType { get; }
|
|
||||||
public List<string> GamePaths { get; }
|
|
||||||
public List<string> FilePaths { get; }
|
|
||||||
|
|
||||||
public long OriginalSize { get; private set; }
|
|
||||||
public long CompressedSize { get; private set; }
|
|
||||||
public long Triangles { get; private set; }
|
|
||||||
|
|
||||||
public IReadOnlyList<FileCacheEntity> CacheEntries { get; }
|
|
||||||
|
|
||||||
public bool IsComputed => OriginalSize > 0 && CompressedSize > 0;
|
public bool IsComputed => OriginalSize > 0 && CompressedSize > 0;
|
||||||
|
public async Task ComputeSizes(FileCacheManager fileCacheManager, CancellationToken token)
|
||||||
public FileDataEntry(
|
|
||||||
string hash,
|
|
||||||
string fileType,
|
|
||||||
List<string> gamePaths,
|
|
||||||
List<string> filePaths,
|
|
||||||
long originalSize,
|
|
||||||
long compressedSize,
|
|
||||||
long triangles,
|
|
||||||
IReadOnlyList<FileCacheEntity> cacheEntries)
|
|
||||||
{
|
{
|
||||||
Hash = hash;
|
var compressedsize = await fileCacheManager.GetCompressedFileData(Hash, token).ConfigureAwait(false);
|
||||||
FileType = fileType;
|
var normalSize = new FileInfo(FilePaths[0]).Length;
|
||||||
GamePaths = gamePaths;
|
var entries = await fileCacheManager.GetAllFileCachesByHashAsync(Hash, ignoreCacheEntries: true, validate: false, token).ConfigureAwait(false);
|
||||||
FilePaths = filePaths;
|
foreach (var entry in entries)
|
||||||
OriginalSize = originalSize;
|
|
||||||
CompressedSize = compressedSize;
|
|
||||||
Triangles = triangles;
|
|
||||||
CacheEntries = cacheEntries;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ComputeSizes(FileCacheManager fileCacheManager, CancellationToken token, bool force = false)
|
|
||||||
{
|
|
||||||
if (!force && IsComputed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (FilePaths.Count == 0 || string.IsNullOrWhiteSpace(FilePaths[0]))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var path = FilePaths[0];
|
|
||||||
|
|
||||||
if (!File.Exists(path))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var original = new FileInfo(path).Length;
|
|
||||||
|
|
||||||
var compressedLen = await fileCacheManager.GetCompressedSizeAsync(Hash, token).ConfigureAwait(false);
|
|
||||||
if (compressedLen <= 0 && !string.Equals(FileType, "tex", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
compressedLen = original;
|
entry.Size = normalSize;
|
||||||
|
entry.CompressedSize = compressedsize.Item2.LongLength;
|
||||||
}
|
}
|
||||||
|
OriginalSize = normalSize;
|
||||||
fileCacheManager.SetSizeInfo(Hash, original, compressedLen);
|
CompressedSize = compressedsize.Item2.LongLength;
|
||||||
FileCacheManager.ApplySizesToEntries(CacheEntries, original, compressedLen);
|
RefreshFormat();
|
||||||
|
|
||||||
OriginalSize = original;
|
|
||||||
CompressedSize = compressedLen;
|
|
||||||
|
|
||||||
if (string.Equals(FileType, "tex", StringComparison.OrdinalIgnoreCase))
|
|
||||||
RefreshFormat();
|
|
||||||
}
|
}
|
||||||
|
public long OriginalSize { get; private set; } = OriginalSize;
|
||||||
|
public long CompressedSize { get; private set; } = CompressedSize;
|
||||||
|
public long Triangles { get; private set; } = Triangles;
|
||||||
public Lazy<string> Format => _format ??= CreateFormatValue();
|
public Lazy<string> Format => _format ??= CreateFormatValue();
|
||||||
|
|
||||||
private Lazy<string>? _format;
|
private Lazy<string>? _format;
|
||||||
|
|
||||||
public void RefreshFormat() => _format = CreateFormatValue();
|
public void RefreshFormat()
|
||||||
public void UpdateTriangles(long triangles) => Triangles = triangles;
|
{
|
||||||
|
_format = CreateFormatValue();
|
||||||
|
}
|
||||||
|
|
||||||
private Lazy<string> CreateFormatValue()
|
private Lazy<string> CreateFormatValue()
|
||||||
=> new(() =>
|
=> new(() =>
|
||||||
{
|
{
|
||||||
if (!string.Equals(FileType, "tex", StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(FileType, "tex", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,770 +0,0 @@
|
|||||||
using Dalamud.Interface.Textures.TextureWraps;
|
|
||||||
using LightlessSync.LightlessConfiguration;
|
|
||||||
using LightlessSync.UI;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Text.Json;
|
|
||||||
using SixLabors.ImageSharp;
|
|
||||||
using SixLabors.ImageSharp.Formats.Gif;
|
|
||||||
using SixLabors.ImageSharp.Formats.Webp;
|
|
||||||
using SixLabors.ImageSharp.Metadata;
|
|
||||||
using SixLabors.ImageSharp.PixelFormats;
|
|
||||||
using SixLabors.ImageSharp.Processing;
|
|
||||||
|
|
||||||
namespace LightlessSync.Services.Chat;
|
|
||||||
|
|
||||||
public sealed class ChatEmoteService : IDisposable
|
|
||||||
{
|
|
||||||
private const string GlobalEmoteSetUrl = "https://7tv.io/v3/emote-sets/global";
|
|
||||||
private const int DefaultFrameDelayMs = 100;
|
|
||||||
private const int MinFrameDelayMs = 20;
|
|
||||||
|
|
||||||
private readonly ILogger<ChatEmoteService> _logger;
|
|
||||||
private readonly HttpClient _httpClient;
|
|
||||||
private readonly UiSharedService _uiSharedService;
|
|
||||||
private readonly ChatConfigService _chatConfigService;
|
|
||||||
private readonly ConcurrentDictionary<string, EmoteEntry> _emotes = new(StringComparer.Ordinal);
|
|
||||||
private readonly SemaphoreSlim _downloadGate = new(3, 3);
|
|
||||||
|
|
||||||
private readonly object _loadLock = new();
|
|
||||||
private Task? _loadTask;
|
|
||||||
|
|
||||||
public ChatEmoteService(ILogger<ChatEmoteService> logger, HttpClient httpClient, UiSharedService uiSharedService, ChatConfigService chatConfigService)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_httpClient = httpClient;
|
|
||||||
_uiSharedService = uiSharedService;
|
|
||||||
_chatConfigService = chatConfigService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void EnsureGlobalEmotesLoaded()
|
|
||||||
{
|
|
||||||
lock (_loadLock)
|
|
||||||
{
|
|
||||||
if (_loadTask is not null && !_loadTask.IsCompleted)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_emotes.Count > 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_loadTask = Task.Run(LoadGlobalEmotesAsync);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IReadOnlyList<string> GetEmoteNames()
|
|
||||||
{
|
|
||||||
EnsureGlobalEmotesLoaded();
|
|
||||||
var names = _emotes.Keys.ToArray();
|
|
||||||
Array.Sort(names, StringComparer.OrdinalIgnoreCase);
|
|
||||||
return names;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryGetEmote(string code, out IDalamudTextureWrap? texture)
|
|
||||||
{
|
|
||||||
texture = null;
|
|
||||||
EnsureGlobalEmotesLoaded();
|
|
||||||
|
|
||||||
if (!_emotes.TryGetValue(code, out var entry))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var allowAnimation = _chatConfigService.Current.EnableAnimatedEmotes;
|
|
||||||
if (entry.TryGetTexture(allowAnimation, out texture))
|
|
||||||
{
|
|
||||||
if (allowAnimation && entry.NeedsAnimationLoad && !entry.HasAttemptedAnimation)
|
|
||||||
{
|
|
||||||
entry.EnsureLoading(allowAnimation, QueueEmoteDownload, allowWhenStaticLoaded: true);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.EnsureLoading(allowAnimation, QueueEmoteDownload);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
foreach (var entry in _emotes.Values)
|
|
||||||
{
|
|
||||||
entry.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
_downloadGate.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadGlobalEmotesAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var stream = await _httpClient.GetStreamAsync(GlobalEmoteSetUrl).ConfigureAwait(false);
|
|
||||||
using var document = await JsonDocument.ParseAsync(stream).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (!document.RootElement.TryGetProperty("emotes", out var emotes))
|
|
||||||
{
|
|
||||||
_logger.LogWarning("7TV emote set response missing emotes array");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var emoteElement in emotes.EnumerateArray())
|
|
||||||
{
|
|
||||||
if (!emoteElement.TryGetProperty("name", out var nameElement))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var name = nameElement.GetString();
|
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var source = TryBuildEmoteSource(emoteElement);
|
|
||||||
if (source is null || (!source.Value.HasStatic && !source.Value.HasAnimation))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
_emotes.TryAdd(name, new EmoteEntry(name, source.Value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Failed to load 7TV emote set");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static EmoteSource? TryBuildEmoteSource(JsonElement emoteElement)
|
|
||||||
{
|
|
||||||
if (!emoteElement.TryGetProperty("data", out var dataElement))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!dataElement.TryGetProperty("host", out var hostElement))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hostElement.TryGetProperty("url", out var urlElement))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var baseUrl = urlElement.GetString();
|
|
||||||
if (string.IsNullOrWhiteSpace(baseUrl))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (baseUrl.StartsWith("//", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
baseUrl = "https:" + baseUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hostElement.TryGetProperty("files", out var filesElement))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var files = ReadEmoteFiles(filesElement);
|
|
||||||
if (files.Count == 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var animatedFile = PickBestAnimatedFile(files);
|
|
||||||
var animatedUrl = animatedFile is null ? null : BuildEmoteUrl(baseUrl, animatedFile.Value.Name);
|
|
||||||
|
|
||||||
var staticName = animatedFile?.StaticName;
|
|
||||||
if (string.IsNullOrWhiteSpace(staticName))
|
|
||||||
{
|
|
||||||
staticName = PickBestStaticFileName(files);
|
|
||||||
}
|
|
||||||
|
|
||||||
var staticUrl = string.IsNullOrWhiteSpace(staticName) ? null : BuildEmoteUrl(baseUrl, staticName);
|
|
||||||
if (string.IsNullOrWhiteSpace(animatedUrl) && string.IsNullOrWhiteSpace(staticUrl))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new EmoteSource(staticUrl, animatedUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildEmoteUrl(string baseUrl, string fileName)
|
|
||||||
=> baseUrl.TrimEnd('/') + "/" + fileName;
|
|
||||||
|
|
||||||
private static List<EmoteFile> ReadEmoteFiles(JsonElement filesElement)
|
|
||||||
{
|
|
||||||
var files = new List<EmoteFile>();
|
|
||||||
foreach (var file in filesElement.EnumerateArray())
|
|
||||||
{
|
|
||||||
if (!file.TryGetProperty("name", out var nameElement))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var name = nameElement.GetString();
|
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
string? staticName = null;
|
|
||||||
if (file.TryGetProperty("static_name", out var staticNameElement) && staticNameElement.ValueKind == JsonValueKind.String)
|
|
||||||
{
|
|
||||||
staticName = staticNameElement.GetString();
|
|
||||||
}
|
|
||||||
|
|
||||||
var frameCount = 1;
|
|
||||||
if (file.TryGetProperty("frame_count", out var frameCountElement) && frameCountElement.ValueKind == JsonValueKind.Number)
|
|
||||||
{
|
|
||||||
frameCountElement.TryGetInt32(out frameCount);
|
|
||||||
frameCount = Math.Max(frameCount, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
string? format = null;
|
|
||||||
if (file.TryGetProperty("format", out var formatElement) && formatElement.ValueKind == JsonValueKind.String)
|
|
||||||
{
|
|
||||||
format = formatElement.GetString();
|
|
||||||
}
|
|
||||||
|
|
||||||
files.Add(new EmoteFile(name, staticName, frameCount, format));
|
|
||||||
}
|
|
||||||
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static EmoteFile? PickBestAnimatedFile(IReadOnlyList<EmoteFile> files)
|
|
||||||
{
|
|
||||||
EmoteFile? webp1x = null;
|
|
||||||
EmoteFile? gif1x = null;
|
|
||||||
EmoteFile? webpFallback = null;
|
|
||||||
EmoteFile? gifFallback = null;
|
|
||||||
|
|
||||||
foreach (var file in files)
|
|
||||||
{
|
|
||||||
if (file.FrameCount <= 1 || !IsAnimatedFormatSupported(file))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.Name.Equals("1x.webp", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
webp1x = file;
|
|
||||||
}
|
|
||||||
else if (file.Name.Equals("1x.gif", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
gif1x = file;
|
|
||||||
}
|
|
||||||
else if (file.Name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase) && webpFallback is null)
|
|
||||||
{
|
|
||||||
webpFallback = file;
|
|
||||||
}
|
|
||||||
else if (file.Name.EndsWith(".gif", StringComparison.OrdinalIgnoreCase) && gifFallback is null)
|
|
||||||
{
|
|
||||||
gifFallback = file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return webp1x ?? gif1x ?? webpFallback ?? gifFallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? PickBestStaticFileName(IReadOnlyList<EmoteFile> files)
|
|
||||||
{
|
|
||||||
string? png1x = null;
|
|
||||||
string? webp1x = null;
|
|
||||||
string? gif1x = null;
|
|
||||||
string? pngFallback = null;
|
|
||||||
string? webpFallback = null;
|
|
||||||
string? gifFallback = null;
|
|
||||||
|
|
||||||
foreach (var file in files)
|
|
||||||
{
|
|
||||||
if (file.FrameCount > 1)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var name = file.StaticName ?? file.Name;
|
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.Equals("1x.png", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
png1x = name;
|
|
||||||
}
|
|
||||||
else if (name.Equals("1x.webp", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
webp1x = name;
|
|
||||||
}
|
|
||||||
else if (name.Equals("1x.gif", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
gif1x = name;
|
|
||||||
}
|
|
||||||
else if (name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) && pngFallback is null)
|
|
||||||
{
|
|
||||||
pngFallback = name;
|
|
||||||
}
|
|
||||||
else if (name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase) && webpFallback is null)
|
|
||||||
{
|
|
||||||
webpFallback = name;
|
|
||||||
}
|
|
||||||
else if (name.EndsWith(".gif", StringComparison.OrdinalIgnoreCase) && gifFallback is null)
|
|
||||||
{
|
|
||||||
gifFallback = name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return png1x ?? webp1x ?? gif1x ?? pngFallback ?? webpFallback ?? gifFallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsAnimatedFormatSupported(EmoteFile file)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(file.Format))
|
|
||||||
{
|
|
||||||
return file.Format.Equals("WEBP", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| file.Format.Equals("GIF", StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
return file.Name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| file.Name.EndsWith(".gif", StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly record struct EmoteSource(string? StaticUrl, string? AnimatedUrl)
|
|
||||||
{
|
|
||||||
public bool HasStatic => !string.IsNullOrWhiteSpace(StaticUrl);
|
|
||||||
public bool HasAnimation => !string.IsNullOrWhiteSpace(AnimatedUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly record struct EmoteFile(string Name, string? StaticName, int FrameCount, string? Format);
|
|
||||||
|
|
||||||
private void QueueEmoteDownload(EmoteEntry entry, bool allowAnimation)
|
|
||||||
{
|
|
||||||
_ = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await _downloadGate.WaitAsync().ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (allowAnimation)
|
|
||||||
{
|
|
||||||
if (entry.HasAnimatedSource)
|
|
||||||
{
|
|
||||||
entry.MarkAnimationAttempted();
|
|
||||||
if (await TryLoadAnimatedEmoteAsync(entry).ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.HasStaticSource && !entry.HasStaticTexture && await TryLoadStaticEmoteAsync(entry).ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (entry.HasStaticSource && await TryLoadStaticEmoteAsync(entry).ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.HasAnimatedSource)
|
|
||||||
{
|
|
||||||
entry.MarkAnimationAttempted();
|
|
||||||
if (await TryLoadAnimatedEmoteAsync(entry).ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.MarkFailed();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogDebug(ex, "Failed to load 7TV emote {Emote}", entry.Code);
|
|
||||||
entry.MarkFailed();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_downloadGate.Release();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> TryLoadAnimatedEmoteAsync(EmoteEntry entry)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(entry.AnimatedUrl))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var data = await _httpClient.GetByteArrayAsync(entry.AnimatedUrl).ConfigureAwait(false);
|
|
||||||
var isWebp = entry.AnimatedUrl.EndsWith(".webp", StringComparison.OrdinalIgnoreCase);
|
|
||||||
if (!TryDecodeAnimation(data, isWebp, out var animation))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.SetAnimation(animation);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogDebug(ex, "Failed to decode animated 7TV emote {Emote}", entry.Code);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> TryLoadStaticEmoteAsync(EmoteEntry entry)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(entry.StaticUrl))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var data = await _httpClient.GetByteArrayAsync(entry.StaticUrl).ConfigureAwait(false);
|
|
||||||
var texture = _uiSharedService.LoadImage(data);
|
|
||||||
entry.SetStaticTexture(texture);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogDebug(ex, "Failed to decode static 7TV emote {Emote}", entry.Code);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryDecodeAnimation(byte[] data, bool isWebp, out EmoteAnimation? animation)
|
|
||||||
{
|
|
||||||
animation = null;
|
|
||||||
List<EmoteFrame>? frames = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Image<Rgba32> image;
|
|
||||||
if (isWebp)
|
|
||||||
{
|
|
||||||
using var stream = new MemoryStream(data);
|
|
||||||
image = WebpDecoder.Instance.Decode<Rgba32>(
|
|
||||||
new WebpDecoderOptions { BackgroundColorHandling = BackgroundColorHandling.Ignore },
|
|
||||||
stream);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
image = Image.Load<Rgba32>(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
using (image)
|
|
||||||
{
|
|
||||||
if (image.Frames.Count <= 1)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var composite = new Image<Rgba32>(image.Width, image.Height, Color.Transparent);
|
|
||||||
Image<Rgba32>? restoreCanvas = null;
|
|
||||||
GifDisposalMethod? pendingGifDisposal = null;
|
|
||||||
WebpDisposalMethod? pendingWebpDisposal = null;
|
|
||||||
|
|
||||||
frames = new List<EmoteFrame>(image.Frames.Count);
|
|
||||||
for (var i = 0; i < image.Frames.Count; i++)
|
|
||||||
{
|
|
||||||
var frameMetadata = image.Frames[i].Metadata;
|
|
||||||
var delayMs = GetFrameDelayMs(frameMetadata);
|
|
||||||
|
|
||||||
ApplyDisposal(composite, ref restoreCanvas, pendingGifDisposal, pendingWebpDisposal);
|
|
||||||
|
|
||||||
GifDisposalMethod? currentGifDisposal = null;
|
|
||||||
WebpDisposalMethod? currentWebpDisposal = null;
|
|
||||||
var blendMethod = WebpBlendMethod.Over;
|
|
||||||
|
|
||||||
if (isWebp)
|
|
||||||
{
|
|
||||||
if (frameMetadata.TryGetWebpFrameMetadata(out var webpMetadata))
|
|
||||||
{
|
|
||||||
currentWebpDisposal = webpMetadata.DisposalMethod;
|
|
||||||
blendMethod = webpMetadata.BlendMethod;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (frameMetadata.TryGetGifMetadata(out var gifMetadata))
|
|
||||||
{
|
|
||||||
currentGifDisposal = gifMetadata.DisposalMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentGifDisposal == GifDisposalMethod.RestoreToPrevious)
|
|
||||||
{
|
|
||||||
restoreCanvas?.Dispose();
|
|
||||||
restoreCanvas = composite.Clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
using var frameImage = image.Frames.CloneFrame(i);
|
|
||||||
var alphaMode = blendMethod == WebpBlendMethod.Source
|
|
||||||
? PixelAlphaCompositionMode.Src
|
|
||||||
: PixelAlphaCompositionMode.SrcOver;
|
|
||||||
composite.Mutate(ctx => ctx.DrawImage(frameImage, PixelColorBlendingMode.Normal, alphaMode, 1f));
|
|
||||||
|
|
||||||
using var renderedFrame = composite.Clone();
|
|
||||||
using var ms = new MemoryStream();
|
|
||||||
renderedFrame.SaveAsPng(ms);
|
|
||||||
|
|
||||||
var texture = _uiSharedService.LoadImage(ms.ToArray());
|
|
||||||
frames.Add(new EmoteFrame(texture, delayMs));
|
|
||||||
|
|
||||||
pendingGifDisposal = currentGifDisposal;
|
|
||||||
pendingWebpDisposal = currentWebpDisposal;
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreCanvas?.Dispose();
|
|
||||||
|
|
||||||
animation = new EmoteAnimation(frames);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
if (frames is not null)
|
|
||||||
{
|
|
||||||
foreach (var frame in frames)
|
|
||||||
{
|
|
||||||
frame.Texture.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int GetFrameDelayMs(ImageFrameMetadata metadata)
|
|
||||||
{
|
|
||||||
if (metadata.TryGetGifMetadata(out var gifMetadata))
|
|
||||||
{
|
|
||||||
var delayMs = (long)gifMetadata.FrameDelay * 10L;
|
|
||||||
return NormalizeFrameDelayMs(delayMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadata.TryGetWebpFrameMetadata(out var webpMetadata))
|
|
||||||
{
|
|
||||||
return NormalizeFrameDelayMs(webpMetadata.FrameDelay);
|
|
||||||
}
|
|
||||||
|
|
||||||
return DefaultFrameDelayMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int NormalizeFrameDelayMs(long delayMs)
|
|
||||||
{
|
|
||||||
if (delayMs <= 0)
|
|
||||||
{
|
|
||||||
return DefaultFrameDelayMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
var clamped = delayMs > int.MaxValue ? int.MaxValue : (int)delayMs;
|
|
||||||
return Math.Max(clamped, MinFrameDelayMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ApplyDisposal(
|
|
||||||
Image<Rgba32> composite,
|
|
||||||
ref Image<Rgba32>? restoreCanvas,
|
|
||||||
GifDisposalMethod? gifDisposal,
|
|
||||||
WebpDisposalMethod? webpDisposal)
|
|
||||||
{
|
|
||||||
if (gifDisposal is not null)
|
|
||||||
{
|
|
||||||
switch (gifDisposal)
|
|
||||||
{
|
|
||||||
case GifDisposalMethod.RestoreToBackground:
|
|
||||||
composite.Mutate(ctx => ctx.BackgroundColor(Color.Transparent));
|
|
||||||
break;
|
|
||||||
case GifDisposalMethod.RestoreToPrevious:
|
|
||||||
if (restoreCanvas is not null)
|
|
||||||
{
|
|
||||||
composite.Mutate(ctx => ctx.BackgroundColor(Color.Transparent));
|
|
||||||
var restoreSnapshot = restoreCanvas;
|
|
||||||
composite.Mutate(ctx => ctx.DrawImage(restoreSnapshot, PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.Src, 1f));
|
|
||||||
restoreCanvas.Dispose();
|
|
||||||
restoreCanvas = null;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (webpDisposal == WebpDisposalMethod.RestoreToBackground)
|
|
||||||
{
|
|
||||||
composite.Mutate(ctx => ctx.BackgroundColor(Color.Transparent));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class EmoteAnimation : IDisposable
|
|
||||||
{
|
|
||||||
private readonly EmoteFrame[] _frames;
|
|
||||||
private readonly int _durationMs;
|
|
||||||
private readonly long _startTimestamp;
|
|
||||||
|
|
||||||
public EmoteAnimation(IReadOnlyList<EmoteFrame> frames)
|
|
||||||
{
|
|
||||||
_frames = frames.ToArray();
|
|
||||||
_durationMs = Math.Max(1, frames.Sum(frame => frame.DurationMs));
|
|
||||||
_startTimestamp = Stopwatch.GetTimestamp();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IDalamudTextureWrap? GetCurrentFrame()
|
|
||||||
{
|
|
||||||
if (_frames.Length == 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_frames.Length == 1)
|
|
||||||
{
|
|
||||||
return _frames[0].Texture;
|
|
||||||
}
|
|
||||||
|
|
||||||
var elapsedTicks = Stopwatch.GetTimestamp() - _startTimestamp;
|
|
||||||
var elapsedMs = (elapsedTicks * 1000L) / Stopwatch.Frequency;
|
|
||||||
var targetMs = (int)(elapsedMs % _durationMs);
|
|
||||||
var accumulated = 0;
|
|
||||||
|
|
||||||
foreach (var frame in _frames)
|
|
||||||
{
|
|
||||||
accumulated += frame.DurationMs;
|
|
||||||
if (targetMs < accumulated)
|
|
||||||
{
|
|
||||||
return frame.Texture;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return _frames[^1].Texture;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IDalamudTextureWrap? GetStaticFrame()
|
|
||||||
{
|
|
||||||
if (_frames.Length == 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _frames[0].Texture;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
foreach (var frame in _frames)
|
|
||||||
{
|
|
||||||
frame.Texture.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly record struct EmoteFrame(IDalamudTextureWrap Texture, int DurationMs);
|
|
||||||
|
|
||||||
private sealed class EmoteEntry : IDisposable
|
|
||||||
{
|
|
||||||
private int _loadingState;
|
|
||||||
private int _animationAttempted;
|
|
||||||
private IDalamudTextureWrap? _staticTexture;
|
|
||||||
private EmoteAnimation? _animation;
|
|
||||||
|
|
||||||
public EmoteEntry(string code, EmoteSource source)
|
|
||||||
{
|
|
||||||
Code = code;
|
|
||||||
StaticUrl = source.StaticUrl;
|
|
||||||
AnimatedUrl = source.AnimatedUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Code { get; }
|
|
||||||
public string? StaticUrl { get; }
|
|
||||||
public string? AnimatedUrl { get; }
|
|
||||||
public bool HasStaticSource => !string.IsNullOrWhiteSpace(StaticUrl);
|
|
||||||
public bool HasAnimatedSource => !string.IsNullOrWhiteSpace(AnimatedUrl);
|
|
||||||
public bool HasStaticTexture => _staticTexture is not null;
|
|
||||||
public bool HasAttemptedAnimation => Interlocked.CompareExchange(ref _animationAttempted, 0, 0) != 0;
|
|
||||||
public bool NeedsAnimationLoad => _animation is null && HasAnimatedSource;
|
|
||||||
|
|
||||||
public void MarkAnimationAttempted()
|
|
||||||
{
|
|
||||||
Interlocked.Exchange(ref _animationAttempted, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryGetTexture(bool allowAnimation, out IDalamudTextureWrap? texture)
|
|
||||||
{
|
|
||||||
if (allowAnimation && _animation is not null)
|
|
||||||
{
|
|
||||||
texture = _animation.GetCurrentFrame();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_staticTexture is not null)
|
|
||||||
{
|
|
||||||
texture = _staticTexture;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!allowAnimation && _animation is not null)
|
|
||||||
{
|
|
||||||
texture = _animation.GetStaticFrame();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
texture = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void EnsureLoading(bool allowAnimation, Action<EmoteEntry, bool> queueDownload, bool allowWhenStaticLoaded = false)
|
|
||||||
{
|
|
||||||
if (_animation is not null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!allowWhenStaticLoaded && _staticTexture is not null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Interlocked.CompareExchange(ref _loadingState, 1, 0) != 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
queueDownload(this, allowAnimation);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetAnimation(EmoteAnimation animation)
|
|
||||||
{
|
|
||||||
_staticTexture?.Dispose();
|
|
||||||
_staticTexture = null;
|
|
||||||
_animation?.Dispose();
|
|
||||||
_animation = animation;
|
|
||||||
Interlocked.Exchange(ref _loadingState, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetStaticTexture(IDalamudTextureWrap texture)
|
|
||||||
{
|
|
||||||
_staticTexture?.Dispose();
|
|
||||||
_staticTexture = texture;
|
|
||||||
Interlocked.Exchange(ref _loadingState, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void MarkFailed()
|
|
||||||
{
|
|
||||||
Interlocked.Exchange(ref _loadingState, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_animation?.Dispose();
|
|
||||||
_staticTexture?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +1,29 @@
|
|||||||
using LightlessSync.API.Dto.Chat;
|
using LightlessSync.API.Dto.Chat;
|
||||||
using LightlessSync.API.Data.Extensions;
|
|
||||||
using LightlessSync.Services.ActorTracking;
|
using LightlessSync.Services.ActorTracking;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Services.ServerConfiguration;
|
|
||||||
using LightlessSync.WebAPI;
|
using LightlessSync.WebAPI;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using LightlessSync.UI.Services;
|
using LightlessSync.UI.Services;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.LightlessConfiguration.Models;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace LightlessSync.Services.Chat;
|
namespace LightlessSync.Services.Chat;
|
||||||
|
|
||||||
public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedService
|
public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedService
|
||||||
{
|
{
|
||||||
private const int MaxMessageHistory = 200;
|
private const int MaxMessageHistory = 150;
|
||||||
internal const int MaxOutgoingLength = 200;
|
internal const int MaxOutgoingLength = 200;
|
||||||
private const int MaxUnreadCount = 999;
|
private const int MaxUnreadCount = 999;
|
||||||
private const string ZoneUnavailableMessage = "Zone chat is only available in major cities.";
|
private const string ZoneUnavailableMessage = "Zone chat is only available in major cities.";
|
||||||
private const string ZoneChannelKey = "zone";
|
private const string ZoneChannelKey = "zone";
|
||||||
private const int MaxReportReasonLength = 100;
|
private const int MaxReportReasonLength = 100;
|
||||||
private const int MaxReportContextLength = 1000;
|
private const int MaxReportContextLength = 1000;
|
||||||
private static readonly JsonSerializerOptions PersistedHistorySerializerOptions = new()
|
|
||||||
{
|
|
||||||
PropertyNameCaseInsensitive = true,
|
|
||||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly ApiController _apiController;
|
private readonly ApiController _apiController;
|
||||||
private readonly DalamudUtilService _dalamudUtilService;
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
private readonly ActorObjectService _actorObjectService;
|
private readonly ActorObjectService _actorObjectService;
|
||||||
private readonly PairUiService _pairUiService;
|
private readonly PairUiService _pairUiService;
|
||||||
private readonly ChatConfigService _chatConfigService;
|
private readonly ChatConfigService _chatConfigService;
|
||||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
|
||||||
|
|
||||||
private readonly Lock _sync = new();
|
private readonly Lock _sync = new();
|
||||||
|
|
||||||
@@ -47,9 +36,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
private readonly Dictionary<string, bool> _lastPresenceStates = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, bool> _lastPresenceStates = new(StringComparer.Ordinal);
|
||||||
private readonly Dictionary<string, string> _selfTokens = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, string> _selfTokens = new(StringComparer.Ordinal);
|
||||||
private readonly List<PendingSelfMessage> _pendingSelfMessages = new();
|
private readonly List<PendingSelfMessage> _pendingSelfMessages = new();
|
||||||
private readonly Dictionary<string, List<ChatMessageEntry>> _messageHistoryCache = new(StringComparer.Ordinal);
|
|
||||||
private List<ChatChannelSnapshot>? _cachedChannelSnapshots;
|
|
||||||
private bool _channelsSnapshotDirty = true;
|
|
||||||
|
|
||||||
private bool _isLoggedIn;
|
private bool _isLoggedIn;
|
||||||
private bool _isConnected;
|
private bool _isConnected;
|
||||||
@@ -65,8 +51,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
ApiController apiController,
|
ApiController apiController,
|
||||||
DalamudUtilService dalamudUtilService,
|
DalamudUtilService dalamudUtilService,
|
||||||
ActorObjectService actorObjectService,
|
ActorObjectService actorObjectService,
|
||||||
PairUiService pairUiService,
|
PairUiService pairUiService)
|
||||||
ServerConfigurationManager serverConfigurationManager)
|
|
||||||
: base(logger, mediator)
|
: base(logger, mediator)
|
||||||
{
|
{
|
||||||
_apiController = apiController;
|
_apiController = apiController;
|
||||||
@@ -74,7 +59,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
_actorObjectService = actorObjectService;
|
_actorObjectService = actorObjectService;
|
||||||
_pairUiService = pairUiService;
|
_pairUiService = pairUiService;
|
||||||
_chatConfigService = chatConfigService;
|
_chatConfigService = chatConfigService;
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
|
||||||
|
|
||||||
_isLoggedIn = _dalamudUtilService.IsLoggedIn;
|
_isLoggedIn = _dalamudUtilService.IsLoggedIn;
|
||||||
_isConnected = _apiController.IsConnected;
|
_isConnected = _apiController.IsConnected;
|
||||||
@@ -85,11 +69,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
{
|
{
|
||||||
using (_sync.EnterScope())
|
using (_sync.EnterScope())
|
||||||
{
|
{
|
||||||
if (!_channelsSnapshotDirty && _cachedChannelSnapshots is not null)
|
|
||||||
{
|
|
||||||
return _cachedChannelSnapshots;
|
|
||||||
}
|
|
||||||
|
|
||||||
var snapshots = new List<ChatChannelSnapshot>(_channelOrder.Count);
|
var snapshots = new List<ChatChannelSnapshot>(_channelOrder.Count);
|
||||||
foreach (var key in _channelOrder)
|
foreach (var key in _channelOrder)
|
||||||
{
|
{
|
||||||
@@ -119,8 +98,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
state.Messages.ToList()));
|
state.Messages.ToList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
_cachedChannelSnapshots = snapshots;
|
|
||||||
_channelsSnapshotDirty = false;
|
|
||||||
return snapshots;
|
return snapshots;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,8 +135,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
state.UnreadCount = 0;
|
state.UnreadCount = 0;
|
||||||
_lastReadCounts[key] = state.Messages.Count;
|
_lastReadCounts[key] = state.Messages.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
MarkChannelsSnapshotDirtyLocked();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +186,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
if (!wasEnabled)
|
if (!wasEnabled)
|
||||||
{
|
{
|
||||||
_chatEnabled = true;
|
_chatEnabled = true;
|
||||||
MarkChannelsSnapshotDirtyLocked();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,8 +231,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
state.IsAvailable = false;
|
state.IsAvailable = false;
|
||||||
state.StatusText = "Chat services disabled";
|
state.StatusText = "Chat services disabled";
|
||||||
}
|
}
|
||||||
|
|
||||||
MarkChannelsSnapshotDirtyLocked();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UnregisterChatHandler();
|
UnregisterChatHandler();
|
||||||
@@ -384,7 +356,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
LoadPersistedSyncshellHistory();
|
|
||||||
Mediator.Subscribe<DalamudLoginMessage>(this, _ => HandleLogin());
|
Mediator.Subscribe<DalamudLoginMessage>(this, _ => HandleLogin());
|
||||||
Mediator.Subscribe<DalamudLogoutMessage>(this, _ => HandleLogout());
|
Mediator.Subscribe<DalamudLogoutMessage>(this, _ => HandleLogout());
|
||||||
Mediator.Subscribe<ZoneSwitchEndMessage>(this, _ => ScheduleZonePresenceUpdate());
|
Mediator.Subscribe<ZoneSwitchEndMessage>(this, _ => ScheduleZonePresenceUpdate());
|
||||||
@@ -585,7 +556,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var location = _dalamudUtilService.GetMapData();
|
var location = await _dalamudUtilService.GetMapDataAsync().ConfigureAwait(false);
|
||||||
var territoryId = (ushort)location.TerritoryId;
|
var territoryId = (ushort)location.TerritoryId;
|
||||||
var worldId = (ushort)location.ServerId;
|
var worldId = (ushort)location.ServerId;
|
||||||
|
|
||||||
@@ -711,7 +682,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var worldId = (ushort)_dalamudUtilService.GetWorldId();
|
var worldId = (ushort)await _dalamudUtilService.GetWorldIdAsync().ConfigureAwait(false);
|
||||||
return definition.Descriptor with { WorldId = worldId };
|
return definition.Descriptor with { WorldId = worldId };
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -746,7 +717,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
_zoneDefinitions[key] = new ZoneChannelDefinition(key, info.DisplayName ?? key, descriptor, territories);
|
_zoneDefinitions[key] = new ZoneChannelDefinition(key, info.DisplayName ?? key, descriptor, territories);
|
||||||
}
|
}
|
||||||
|
|
||||||
var territoryData = _dalamudUtilService.TerritoryDataEnglish.Value;
|
var territoryData = _dalamudUtilService.TerritoryData.Value;
|
||||||
foreach (var kvp in territoryData)
|
foreach (var kvp in territoryData)
|
||||||
{
|
{
|
||||||
foreach (var variant in EnumerateTerritoryKeys(kvp.Value))
|
foreach (var variant in EnumerateTerritoryKeys(kvp.Value))
|
||||||
@@ -790,7 +761,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
using (_sync.EnterScope())
|
using (_sync.EnterScope())
|
||||||
{
|
{
|
||||||
var remainingGroups = new HashSet<string>(_groupDefinitions.Keys, StringComparer.OrdinalIgnoreCase);
|
var remainingGroups = new HashSet<string>(_groupDefinitions.Keys, StringComparer.OrdinalIgnoreCase);
|
||||||
var allowRemoval = _isConnected;
|
|
||||||
|
|
||||||
foreach (var info in infoList)
|
foreach (var info in infoList)
|
||||||
{
|
{
|
||||||
@@ -806,19 +776,18 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
var key = BuildChannelKey(descriptor);
|
var key = BuildChannelKey(descriptor);
|
||||||
if (!_channels.TryGetValue(key, out var state))
|
if (!_channels.TryGetValue(key, out var state))
|
||||||
{
|
{
|
||||||
state = new ChatChannelState(key, ChatChannelType.Group, info.DisplayName ?? groupId, descriptor);
|
state = new ChatChannelState(key, ChatChannelType.Group, info.DisplayName ?? groupId, descriptor);
|
||||||
var restoredCount = RestoreCachedMessagesLocked(state);
|
state.IsConnected = _chatEnabled && _isConnected;
|
||||||
state.IsConnected = _chatEnabled && _isConnected;
|
state.IsAvailable = _chatEnabled && _isConnected;
|
||||||
state.IsAvailable = _chatEnabled && _isConnected;
|
state.StatusText = !_chatEnabled
|
||||||
state.StatusText = !_chatEnabled
|
? "Chat services disabled"
|
||||||
? "Chat services disabled"
|
: (_isConnected ? null : "Disconnected from chat server");
|
||||||
: (_isConnected ? null : "Disconnected from chat server");
|
_channels[key] = state;
|
||||||
_channels[key] = state;
|
_lastReadCounts[key] = 0;
|
||||||
_lastReadCounts[key] = restoredCount > 0 ? state.Messages.Count : 0;
|
if (_chatEnabled)
|
||||||
if (_chatEnabled)
|
{
|
||||||
{
|
descriptorsToJoin.Add(descriptor);
|
||||||
descriptorsToJoin.Add(descriptor);
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -832,30 +801,26 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowRemoval)
|
foreach (var removedGroupId in remainingGroups)
|
||||||
{
|
{
|
||||||
foreach (var removedGroupId in remainingGroups)
|
if (_groupDefinitions.TryGetValue(removedGroupId, out var definition))
|
||||||
{
|
{
|
||||||
if (_groupDefinitions.TryGetValue(removedGroupId, out var definition))
|
var key = BuildChannelKey(definition.Descriptor);
|
||||||
|
if (_channels.TryGetValue(key, out var state))
|
||||||
{
|
{
|
||||||
var key = BuildChannelKey(definition.Descriptor);
|
descriptorsToLeave.Add(state.Descriptor);
|
||||||
if (_channels.TryGetValue(key, out var state))
|
_channels.Remove(key);
|
||||||
|
_lastReadCounts.Remove(key);
|
||||||
|
_lastPresenceStates.Remove(BuildPresenceKey(state.Descriptor));
|
||||||
|
_selfTokens.Remove(key);
|
||||||
|
_pendingSelfMessages.RemoveAll(p => string.Equals(p.ChannelKey, key, StringComparison.Ordinal));
|
||||||
|
if (string.Equals(_activeChannelKey, key, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
CacheMessagesLocked(state);
|
_activeChannelKey = null;
|
||||||
descriptorsToLeave.Add(state.Descriptor);
|
|
||||||
_channels.Remove(key);
|
|
||||||
_lastReadCounts.Remove(key);
|
|
||||||
_lastPresenceStates.Remove(BuildPresenceKey(state.Descriptor));
|
|
||||||
_selfTokens.Remove(key);
|
|
||||||
_pendingSelfMessages.RemoveAll(p => string.Equals(p.ChannelKey, key, StringComparison.Ordinal));
|
|
||||||
if (string.Equals(_activeChannelKey, key, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
_activeChannelKey = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_groupDefinitions.Remove(removedGroupId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_groupDefinitions.Remove(removedGroupId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -888,12 +853,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
var infos = new List<GroupChatChannelInfoDto>(groups.Count);
|
var infos = new List<GroupChatChannelInfoDto>(groups.Count);
|
||||||
foreach (var group in groups)
|
foreach (var group in groups)
|
||||||
{
|
{
|
||||||
// basically prune the channel if it's disabled
|
|
||||||
if (group.GroupPermissions.IsDisableChat())
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var descriptor = new ChatChannelDescriptor
|
var descriptor = new ChatChannelDescriptor
|
||||||
{
|
{
|
||||||
Type = ChatChannelType.Group,
|
Type = ChatChannelType.Group,
|
||||||
@@ -1009,22 +968,11 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
|
|
||||||
private void OnChatMessageReceived(ChatMessageDto dto)
|
private void OnChatMessageReceived(ChatMessageDto dto)
|
||||||
{
|
{
|
||||||
ChatChannelDescriptor descriptor = dto.Channel.WithNormalizedCustomKey();
|
var descriptor = dto.Channel.WithNormalizedCustomKey();
|
||||||
string key = descriptor.Type == ChatChannelType.Zone ? ZoneChannelKey : BuildChannelKey(descriptor);
|
var key = descriptor.Type == ChatChannelType.Zone ? ZoneChannelKey : BuildChannelKey(descriptor);
|
||||||
bool fromSelf = IsMessageFromSelf(dto, key);
|
var fromSelf = IsMessageFromSelf(dto, key);
|
||||||
ChatMessageEntry message = BuildMessage(dto, fromSelf);
|
var message = BuildMessage(dto, fromSelf);
|
||||||
bool mentionNotificationsEnabled = _chatConfigService.Current.EnableMentionNotifications;
|
|
||||||
bool notifyMention = mentionNotificationsEnabled
|
|
||||||
&& !fromSelf
|
|
||||||
&& descriptor.Type == ChatChannelType.Group
|
|
||||||
&& TryGetSelfMentionToken(dto.Message, out _);
|
|
||||||
|
|
||||||
string? mentionChannelName = null;
|
|
||||||
string? mentionSenderName = null;
|
|
||||||
bool publishChannelList = false;
|
bool publishChannelList = false;
|
||||||
bool shouldPersistHistory = _chatConfigService.Current.PersistSyncshellHistory;
|
|
||||||
List<PersistedChatMessage>? persistedMessages = null;
|
|
||||||
string? persistedChannelKey = null;
|
|
||||||
|
|
||||||
using (_sync.EnterScope())
|
using (_sync.EnterScope())
|
||||||
{
|
{
|
||||||
@@ -1044,14 +992,13 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
descriptor.Type,
|
descriptor.Type,
|
||||||
displayName,
|
displayName,
|
||||||
descriptor.Type == ChatChannelType.Zone ? (_lastZoneDescriptor ?? descriptor) : descriptor);
|
descriptor.Type == ChatChannelType.Zone ? (_lastZoneDescriptor ?? descriptor) : descriptor);
|
||||||
var restoredCount = RestoreCachedMessagesLocked(state);
|
|
||||||
|
|
||||||
state.IsConnected = _isConnected;
|
state.IsConnected = _isConnected;
|
||||||
state.IsAvailable = descriptor.Type == ChatChannelType.Group && _isConnected;
|
state.IsAvailable = descriptor.Type == ChatChannelType.Group && _isConnected;
|
||||||
state.StatusText = descriptor.Type == ChatChannelType.Zone ? ZoneUnavailableMessage : (_isConnected ? null : "Disconnected from chat server");
|
state.StatusText = descriptor.Type == ChatChannelType.Zone ? ZoneUnavailableMessage : (_isConnected ? null : "Disconnected from chat server");
|
||||||
|
|
||||||
_channels[key] = state;
|
_channels[key] = state;
|
||||||
_lastReadCounts[key] = restoredCount > 0 ? state.Messages.Count : 0;
|
_lastReadCounts[key] = 0;
|
||||||
publishChannelList = true;
|
publishChannelList = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1062,12 +1009,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
state.Messages.RemoveAt(0);
|
state.Messages.RemoveAt(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notifyMention)
|
|
||||||
{
|
|
||||||
mentionChannelName = state.DisplayName;
|
|
||||||
mentionSenderName = message.DisplayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(_activeChannelKey, key, StringComparison.Ordinal))
|
if (string.Equals(_activeChannelKey, key, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
state.HasUnread = false;
|
state.HasUnread = false;
|
||||||
@@ -1082,31 +1023,10 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
state.UnreadCount = Math.Min(Math.Max(unreadFromHistory, incrementalUnread), MaxUnreadCount);
|
state.UnreadCount = Math.Min(Math.Max(unreadFromHistory, incrementalUnread), MaxUnreadCount);
|
||||||
state.HasUnread = state.UnreadCount > 0;
|
state.HasUnread = state.UnreadCount > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
MarkChannelsSnapshotDirtyLocked();
|
|
||||||
|
|
||||||
if (shouldPersistHistory && state.Type == ChatChannelType.Group)
|
|
||||||
{
|
|
||||||
persistedChannelKey = state.Key;
|
|
||||||
persistedMessages = BuildPersistedHistoryLocked(state);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Mediator.Publish(new ChatChannelMessageAdded(key, message));
|
Mediator.Publish(new ChatChannelMessageAdded(key, message));
|
||||||
|
|
||||||
if (persistedMessages is not null && persistedChannelKey is not null)
|
|
||||||
{
|
|
||||||
PersistSyncshellHistory(persistedChannelKey, persistedMessages);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (notifyMention)
|
|
||||||
{
|
|
||||||
string channelName = mentionChannelName ?? "Syncshell";
|
|
||||||
string senderName = mentionSenderName ?? "Someone";
|
|
||||||
string notificationText = $"You were mentioned by {senderName} in {channelName}.";
|
|
||||||
Mediator.Publish(new NotificationMessage("Syncshell mention", notificationText, NotificationType.Info));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (publishChannelList)
|
if (publishChannelList)
|
||||||
{
|
{
|
||||||
using (_sync.EnterScope())
|
using (_sync.EnterScope())
|
||||||
@@ -1153,113 +1073,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryGetSelfMentionToken(string message, out string matchedToken)
|
|
||||||
{
|
|
||||||
matchedToken = string.Empty;
|
|
||||||
if (string.IsNullOrWhiteSpace(message))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
HashSet<string> tokens = BuildSelfMentionTokens();
|
|
||||||
if (tokens.Count == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return TryFindMentionToken(message, tokens, out matchedToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private HashSet<string> BuildSelfMentionTokens()
|
|
||||||
{
|
|
||||||
HashSet<string> tokens = new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
string uid = _apiController.UID;
|
|
||||||
if (IsValidMentionToken(uid))
|
|
||||||
{
|
|
||||||
tokens.Add(uid);
|
|
||||||
}
|
|
||||||
|
|
||||||
string displayName = _apiController.DisplayName;
|
|
||||||
if (IsValidMentionToken(displayName))
|
|
||||||
{
|
|
||||||
tokens.Add(displayName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokens;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsValidMentionToken(string value)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < value.Length; i++)
|
|
||||||
{
|
|
||||||
if (!IsMentionChar(value[i]))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryFindMentionToken(string message, IReadOnlyCollection<string> tokens, out string matchedToken)
|
|
||||||
{
|
|
||||||
matchedToken = string.Empty;
|
|
||||||
if (tokens.Count == 0 || string.IsNullOrEmpty(message))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int index = 0;
|
|
||||||
while (index < message.Length)
|
|
||||||
{
|
|
||||||
if (message[index] != '@')
|
|
||||||
{
|
|
||||||
index++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index > 0 && IsMentionChar(message[index - 1]))
|
|
||||||
{
|
|
||||||
index++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
int start = index + 1;
|
|
||||||
int end = start;
|
|
||||||
while (end < message.Length && IsMentionChar(message[end]))
|
|
||||||
{
|
|
||||||
end++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (end == start)
|
|
||||||
{
|
|
||||||
index++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
string token = message.Substring(start, end - start);
|
|
||||||
if (tokens.Contains(token))
|
|
||||||
{
|
|
||||||
matchedToken = token;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
index = end;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsMentionChar(char value)
|
|
||||||
{
|
|
||||||
return char.IsLetterOrDigit(value) || value == '_' || value == '-' || value == '\'';
|
|
||||||
}
|
|
||||||
|
|
||||||
private ChatMessageEntry BuildMessage(ChatMessageDto dto, bool fromSelf)
|
private ChatMessageEntry BuildMessage(ChatMessageDto dto, bool fromSelf)
|
||||||
{
|
{
|
||||||
var displayName = ResolveDisplayName(dto, fromSelf);
|
var displayName = ResolveDisplayName(dto, fromSelf);
|
||||||
@@ -1313,7 +1126,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return _dalamudUtilService.GetPlayerName();
|
return _dalamudUtilService.GetPlayerNameAsync().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -1323,15 +1136,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
|
|
||||||
if (dto.Sender.Kind == ChatSenderKind.IdentifiedUser && dto.Sender.User is not null)
|
if (dto.Sender.Kind == ChatSenderKind.IdentifiedUser && dto.Sender.User is not null)
|
||||||
{
|
{
|
||||||
if (dto.Channel.Type != ChatChannelType.Group || _chatConfigService.Current.ShowNotesInSyncshellChat)
|
|
||||||
{
|
|
||||||
var note = _serverConfigurationManager.GetNoteForUid(dto.Sender.User.UID);
|
|
||||||
if (!string.IsNullOrWhiteSpace(note))
|
|
||||||
{
|
|
||||||
return note;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dto.Sender.User.AliasOrUID;
|
return dto.Sender.User.AliasOrUID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1400,25 +1204,9 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
{
|
{
|
||||||
_activeChannelKey = _channelOrder.Count > 0 ? _channelOrder[0] : null;
|
_activeChannelKey = _channelOrder.Count > 0 ? _channelOrder[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
MarkChannelsSnapshotDirtyLocked();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MarkChannelsSnapshotDirty()
|
private void PublishChannelListChanged() => Mediator.Publish(new ChatChannelsUpdated());
|
||||||
{
|
|
||||||
using (_sync.EnterScope())
|
|
||||||
{
|
|
||||||
_channelsSnapshotDirty = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void MarkChannelsSnapshotDirtyLocked() => _channelsSnapshotDirty = true;
|
|
||||||
|
|
||||||
private void PublishChannelListChanged()
|
|
||||||
{
|
|
||||||
MarkChannelsSnapshotDirty();
|
|
||||||
Mediator.Publish(new ChatChannelsUpdated());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<string> EnumerateTerritoryKeys(string? value)
|
private static IEnumerable<string> EnumerateTerritoryKeys(string? value)
|
||||||
{
|
{
|
||||||
@@ -1461,12 +1249,11 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
if (!_channels.TryGetValue(ZoneChannelKey, out var state))
|
if (!_channels.TryGetValue(ZoneChannelKey, out var state))
|
||||||
{
|
{
|
||||||
state = new ChatChannelState(ZoneChannelKey, ChatChannelType.Zone, "Zone Chat", new ChatChannelDescriptor { Type = ChatChannelType.Zone });
|
state = new ChatChannelState(ZoneChannelKey, ChatChannelType.Zone, "Zone Chat", new ChatChannelDescriptor { Type = ChatChannelType.Zone });
|
||||||
var restoredCount = RestoreCachedMessagesLocked(state);
|
|
||||||
state.IsConnected = _chatEnabled && _isConnected;
|
state.IsConnected = _chatEnabled && _isConnected;
|
||||||
state.IsAvailable = false;
|
state.IsAvailable = false;
|
||||||
state.StatusText = _chatEnabled ? ZoneUnavailableMessage : "Chat services disabled";
|
state.StatusText = _chatEnabled ? ZoneUnavailableMessage : "Chat services disabled";
|
||||||
_channels[ZoneChannelKey] = state;
|
_channels[ZoneChannelKey] = state;
|
||||||
_lastReadCounts[ZoneChannelKey] = restoredCount > 0 ? state.Messages.Count : 0;
|
_lastReadCounts[ZoneChannelKey] = 0;
|
||||||
UpdateChannelOrderLocked();
|
UpdateChannelOrderLocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1475,11 +1262,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
|
|
||||||
private void RemoveZoneStateLocked()
|
private void RemoveZoneStateLocked()
|
||||||
{
|
{
|
||||||
if (_channels.TryGetValue(ZoneChannelKey, out var existing))
|
|
||||||
{
|
|
||||||
CacheMessagesLocked(existing);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_channels.Remove(ZoneChannelKey))
|
if (_channels.Remove(ZoneChannelKey))
|
||||||
{
|
{
|
||||||
_lastReadCounts.Remove(ZoneChannelKey);
|
_lastReadCounts.Remove(ZoneChannelKey);
|
||||||
@@ -1494,335 +1276,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CacheMessagesLocked(ChatChannelState state)
|
|
||||||
{
|
|
||||||
if (state.Messages.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_messageHistoryCache[state.Key] = new List<ChatMessageEntry>(state.Messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int RestoreCachedMessagesLocked(ChatChannelState state)
|
|
||||||
{
|
|
||||||
if (_messageHistoryCache.TryGetValue(state.Key, out var cached) && cached.Count > 0)
|
|
||||||
{
|
|
||||||
state.Messages.AddRange(cached);
|
|
||||||
_messageHistoryCache.Remove(state.Key);
|
|
||||||
return cached.Count;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LoadPersistedSyncshellHistory()
|
|
||||||
{
|
|
||||||
if (!_chatConfigService.Current.PersistSyncshellHistory)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Dictionary<string, string> persisted = _chatConfigService.Current.SyncshellChannelHistory;
|
|
||||||
if (persisted.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<string> invalidKeys = new();
|
|
||||||
foreach (KeyValuePair<string, string> entry in persisted)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(entry.Key) || string.IsNullOrWhiteSpace(entry.Value))
|
|
||||||
{
|
|
||||||
invalidKeys.Add(entry.Key);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryDecodePersistedHistory(entry.Value, out List<PersistedChatMessage> persistedMessages))
|
|
||||||
{
|
|
||||||
invalidKeys.Add(entry.Key);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (persistedMessages.Count == 0)
|
|
||||||
{
|
|
||||||
invalidKeys.Add(entry.Key);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (persistedMessages.Count > MaxMessageHistory)
|
|
||||||
{
|
|
||||||
int startIndex = Math.Max(0, persistedMessages.Count - MaxMessageHistory);
|
|
||||||
persistedMessages = persistedMessages.GetRange(startIndex, persistedMessages.Count - startIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<ChatMessageEntry> restoredMessages = new(persistedMessages.Count);
|
|
||||||
foreach (PersistedChatMessage persistedMessage in persistedMessages)
|
|
||||||
{
|
|
||||||
if (!TryBuildRestoredMessage(entry.Key, persistedMessage, out ChatMessageEntry restoredMessage))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
restoredMessages.Add(restoredMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (restoredMessages.Count == 0)
|
|
||||||
{
|
|
||||||
invalidKeys.Add(entry.Key);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
using (_sync.EnterScope())
|
|
||||||
{
|
|
||||||
_messageHistoryCache[entry.Key] = restoredMessages;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (invalidKeys.Count > 0)
|
|
||||||
{
|
|
||||||
foreach (string key in invalidKeys)
|
|
||||||
{
|
|
||||||
persisted.Remove(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
_chatConfigService.Save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<PersistedChatMessage> BuildPersistedHistoryLocked(ChatChannelState state)
|
|
||||||
{
|
|
||||||
int startIndex = Math.Max(0, state.Messages.Count - MaxMessageHistory);
|
|
||||||
List<PersistedChatMessage> persistedMessages = new(state.Messages.Count - startIndex);
|
|
||||||
for (int i = startIndex; i < state.Messages.Count; i++)
|
|
||||||
{
|
|
||||||
ChatMessageEntry entry = state.Messages[i];
|
|
||||||
if (entry.Payload is not { } payload)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
persistedMessages.Add(new PersistedChatMessage(
|
|
||||||
payload.Message,
|
|
||||||
entry.DisplayName,
|
|
||||||
entry.FromSelf,
|
|
||||||
entry.ReceivedAtUtc,
|
|
||||||
payload.SentAtUtc));
|
|
||||||
}
|
|
||||||
|
|
||||||
return persistedMessages;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PersistSyncshellHistory(string channelKey, List<PersistedChatMessage> persistedMessages)
|
|
||||||
{
|
|
||||||
if (!_chatConfigService.Current.PersistSyncshellHistory)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Dictionary<string, string> persisted = _chatConfigService.Current.SyncshellChannelHistory;
|
|
||||||
if (persistedMessages.Count == 0)
|
|
||||||
{
|
|
||||||
if (persisted.Remove(channelKey))
|
|
||||||
{
|
|
||||||
_chatConfigService.Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string? base64 = EncodePersistedMessages(persistedMessages);
|
|
||||||
if (string.IsNullOrWhiteSpace(base64))
|
|
||||||
{
|
|
||||||
if (persisted.Remove(channelKey))
|
|
||||||
{
|
|
||||||
_chatConfigService.Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
persisted[channelKey] = base64;
|
|
||||||
_chatConfigService.Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? EncodePersistedMessages(List<PersistedChatMessage> persistedMessages)
|
|
||||||
{
|
|
||||||
if (persistedMessages.Count == 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] jsonBytes = JsonSerializer.SerializeToUtf8Bytes(persistedMessages, PersistedHistorySerializerOptions);
|
|
||||||
return Convert.ToBase64String(jsonBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryDecodePersistedHistory(string base64, out List<PersistedChatMessage> persistedMessages)
|
|
||||||
{
|
|
||||||
persistedMessages = new List<PersistedChatMessage>();
|
|
||||||
if (string.IsNullOrWhiteSpace(base64))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
byte[] jsonBytes = Convert.FromBase64String(base64);
|
|
||||||
List<PersistedChatMessage>? decoded = JsonSerializer.Deserialize<List<PersistedChatMessage>>(jsonBytes, PersistedHistorySerializerOptions);
|
|
||||||
if (decoded is null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
persistedMessages = decoded;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryBuildRestoredMessage(string channelKey, PersistedChatMessage persistedMessage, out ChatMessageEntry restoredMessage)
|
|
||||||
{
|
|
||||||
restoredMessage = default;
|
|
||||||
string messageText = persistedMessage.Message;
|
|
||||||
DateTime sentAtUtc = persistedMessage.SentAtUtc;
|
|
||||||
if (string.IsNullOrWhiteSpace(messageText) && persistedMessage.LegacyPayload is { } legacy)
|
|
||||||
{
|
|
||||||
messageText = legacy.Message;
|
|
||||||
sentAtUtc = legacy.SentAtUtc;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(messageText))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatChannelDescriptor descriptor = BuildDescriptorFromChannelKey(channelKey);
|
|
||||||
ChatSenderDescriptor sender = new ChatSenderDescriptor(
|
|
||||||
ChatSenderKind.Anonymous,
|
|
||||||
string.Empty,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
false);
|
|
||||||
|
|
||||||
ChatMessageDto payload = new ChatMessageDto(descriptor, sender, messageText, sentAtUtc, string.Empty);
|
|
||||||
restoredMessage = new ChatMessageEntry(payload, persistedMessage.DisplayName, persistedMessage.FromSelf, persistedMessage.ReceivedAtUtc);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ChatChannelDescriptor BuildDescriptorFromChannelKey(string channelKey)
|
|
||||||
{
|
|
||||||
if (string.Equals(channelKey, ZoneChannelKey, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
return new ChatChannelDescriptor { Type = ChatChannelType.Zone };
|
|
||||||
}
|
|
||||||
|
|
||||||
int separatorIndex = channelKey.IndexOf(':', StringComparison.Ordinal);
|
|
||||||
if (separatorIndex <= 0 || separatorIndex >= channelKey.Length - 1)
|
|
||||||
{
|
|
||||||
return new ChatChannelDescriptor { Type = ChatChannelType.Group };
|
|
||||||
}
|
|
||||||
|
|
||||||
string typeValue = channelKey[..separatorIndex];
|
|
||||||
if (!int.TryParse(typeValue, out int parsedType))
|
|
||||||
{
|
|
||||||
return new ChatChannelDescriptor { Type = ChatChannelType.Group };
|
|
||||||
}
|
|
||||||
|
|
||||||
string customKey = channelKey[(separatorIndex + 1)..];
|
|
||||||
ChatChannelType channelType = parsedType switch
|
|
||||||
{
|
|
||||||
(int)ChatChannelType.Zone => ChatChannelType.Zone,
|
|
||||||
(int)ChatChannelType.Group => ChatChannelType.Group,
|
|
||||||
_ => ChatChannelType.Group
|
|
||||||
};
|
|
||||||
|
|
||||||
return new ChatChannelDescriptor
|
|
||||||
{
|
|
||||||
Type = channelType,
|
|
||||||
CustomKey = customKey
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ClearPersistedSyncshellHistory(bool clearLoadedMessages)
|
|
||||||
{
|
|
||||||
bool shouldPublish = false;
|
|
||||||
bool saveConfig = false;
|
|
||||||
|
|
||||||
using (_sync.EnterScope())
|
|
||||||
{
|
|
||||||
Dictionary<string, List<ChatMessageEntry>> cache = _messageHistoryCache;
|
|
||||||
if (cache.Count > 0)
|
|
||||||
{
|
|
||||||
List<string> keysToRemove = new();
|
|
||||||
foreach (string key in cache.Keys)
|
|
||||||
{
|
|
||||||
if (!string.Equals(key, ZoneChannelKey, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
keysToRemove.Add(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (string key in keysToRemove)
|
|
||||||
{
|
|
||||||
cache.Remove(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keysToRemove.Count > 0)
|
|
||||||
{
|
|
||||||
shouldPublish = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clearLoadedMessages)
|
|
||||||
{
|
|
||||||
foreach (ChatChannelState state in _channels.Values)
|
|
||||||
{
|
|
||||||
if (state.Type != ChatChannelType.Group)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.Messages.Count == 0 && state.UnreadCount == 0 && !state.HasUnread)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.Messages.Clear();
|
|
||||||
state.HasUnread = false;
|
|
||||||
state.UnreadCount = 0;
|
|
||||||
_lastReadCounts[state.Key] = 0;
|
|
||||||
shouldPublish = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Dictionary<string, string> persisted = _chatConfigService.Current.SyncshellChannelHistory;
|
|
||||||
if (persisted.Count > 0)
|
|
||||||
{
|
|
||||||
persisted.Clear();
|
|
||||||
saveConfig = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldPublish)
|
|
||||||
{
|
|
||||||
MarkChannelsSnapshotDirtyLocked();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (saveConfig)
|
|
||||||
{
|
|
||||||
_chatConfigService.Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldPublish)
|
|
||||||
{
|
|
||||||
PublishChannelListChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class ChatChannelState
|
private sealed class ChatChannelState
|
||||||
{
|
{
|
||||||
public ChatChannelState(string key, ChatChannelType type, string displayName, ChatChannelDescriptor descriptor)
|
public ChatChannelState(string key, ChatChannelType type, string displayName, ChatChannelDescriptor descriptor)
|
||||||
@@ -1859,12 +1312,4 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
bool IsOwner);
|
bool IsOwner);
|
||||||
|
|
||||||
private readonly record struct PendingSelfMessage(string ChannelKey, string Message);
|
private readonly record struct PendingSelfMessage(string ChannelKey, string Message);
|
||||||
|
|
||||||
public sealed record PersistedChatMessage(
|
|
||||||
string Message = "",
|
|
||||||
string DisplayName = "",
|
|
||||||
bool FromSelf = false,
|
|
||||||
DateTime ReceivedAtUtc = default,
|
|
||||||
DateTime SentAtUtc = default,
|
|
||||||
[property: JsonPropertyName("Payload")] ChatMessageDto? LegacyPayload = null);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,21 +4,21 @@ using Dalamud.Plugin;
|
|||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.LightlessConfiguration.Models;
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
using LightlessSync.Services.LightFinder;
|
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.UI;
|
|
||||||
using LightlessSync.UI.Services;
|
|
||||||
using LightlessSync.Utils;
|
using LightlessSync.Utils;
|
||||||
using LightlessSync.WebAPI;
|
using LightlessSync.WebAPI;
|
||||||
|
using Lumina.Excel.Sheets;
|
||||||
|
using LightlessSync.UI.Services;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using LightlessSync.UI;
|
||||||
|
using LightlessSync.Services.LightFinder;
|
||||||
|
|
||||||
namespace LightlessSync.Services;
|
namespace LightlessSync.Services;
|
||||||
|
|
||||||
internal class ContextMenuService : IHostedService
|
internal class ContextMenuService : IHostedService
|
||||||
{
|
{
|
||||||
private readonly IContextMenu _contextMenu;
|
private readonly IContextMenu _contextMenu;
|
||||||
private readonly IChatGui _chatGui;
|
|
||||||
private readonly IDalamudPluginInterface _pluginInterface;
|
private readonly IDalamudPluginInterface _pluginInterface;
|
||||||
private readonly IDataManager _gameData;
|
private readonly IDataManager _gameData;
|
||||||
private readonly ILogger<ContextMenuService> _logger;
|
private readonly ILogger<ContextMenuService> _logger;
|
||||||
@@ -29,7 +29,6 @@ internal class ContextMenuService : IHostedService
|
|||||||
private readonly ApiController _apiController;
|
private readonly ApiController _apiController;
|
||||||
private readonly IObjectTable _objectTable;
|
private readonly IObjectTable _objectTable;
|
||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly NotificationService _lightlessNotification;
|
|
||||||
private readonly LightFinderScannerService _broadcastScannerService;
|
private readonly LightFinderScannerService _broadcastScannerService;
|
||||||
private readonly LightFinderService _broadcastService;
|
private readonly LightFinderService _broadcastService;
|
||||||
private readonly LightlessProfileManager _lightlessProfileManager;
|
private readonly LightlessProfileManager _lightlessProfileManager;
|
||||||
@@ -44,7 +43,7 @@ internal class ContextMenuService : IHostedService
|
|||||||
ILogger<ContextMenuService> logger,
|
ILogger<ContextMenuService> logger,
|
||||||
DalamudUtilService dalamudUtil,
|
DalamudUtilService dalamudUtil,
|
||||||
ApiController apiController,
|
ApiController apiController,
|
||||||
IObjectTable objectTable,
|
IObjectTable objectTable,
|
||||||
LightlessConfigService configService,
|
LightlessConfigService configService,
|
||||||
PairRequestService pairRequestService,
|
PairRequestService pairRequestService,
|
||||||
PairUiService pairUiService,
|
PairUiService pairUiService,
|
||||||
@@ -52,9 +51,7 @@ internal class ContextMenuService : IHostedService
|
|||||||
LightFinderScannerService broadcastScannerService,
|
LightFinderScannerService broadcastScannerService,
|
||||||
LightFinderService broadcastService,
|
LightFinderService broadcastService,
|
||||||
LightlessProfileManager lightlessProfileManager,
|
LightlessProfileManager lightlessProfileManager,
|
||||||
LightlessMediator mediator,
|
LightlessMediator mediator)
|
||||||
IChatGui chatGui,
|
|
||||||
NotificationService lightlessNotification)
|
|
||||||
{
|
{
|
||||||
_contextMenu = contextMenu;
|
_contextMenu = contextMenu;
|
||||||
_pluginInterface = pluginInterface;
|
_pluginInterface = pluginInterface;
|
||||||
@@ -71,8 +68,6 @@ internal class ContextMenuService : IHostedService
|
|||||||
_broadcastService = broadcastService;
|
_broadcastService = broadcastService;
|
||||||
_lightlessProfileManager = lightlessProfileManager;
|
_lightlessProfileManager = lightlessProfileManager;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
_chatGui = chatGui;
|
|
||||||
_lightlessNotification = lightlessNotification;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
@@ -104,12 +99,6 @@ internal class ContextMenuService : IHostedService
|
|||||||
if (!_pluginInterface.UiBuilder.ShouldModifyUi)
|
if (!_pluginInterface.UiBuilder.ShouldModifyUi)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!_configService.Current.EnableRightClickMenus)
|
|
||||||
{
|
|
||||||
_logger.LogTrace("Right-click menus are disabled in configuration.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.AddonName != null)
|
if (args.AddonName != null)
|
||||||
{
|
{
|
||||||
var addonName = args.AddonName;
|
var addonName = args.AddonName;
|
||||||
@@ -140,6 +129,7 @@ internal class ContextMenuService : IHostedService
|
|||||||
|
|
||||||
var snapshot = _pairUiService.GetSnapshot();
|
var snapshot = _pairUiService.GetSnapshot();
|
||||||
var pair = snapshot.PairsByUid.Values.FirstOrDefault(p =>
|
var pair = snapshot.PairsByUid.Values.FirstOrDefault(p =>
|
||||||
|
p.IsVisible &&
|
||||||
p.PlayerCharacterId != uint.MaxValue &&
|
p.PlayerCharacterId != uint.MaxValue &&
|
||||||
p.PlayerCharacterId == target.TargetObjectId);
|
p.PlayerCharacterId == target.TargetObjectId);
|
||||||
|
|
||||||
@@ -171,8 +161,9 @@ internal class ContextMenuService : IHostedService
|
|||||||
_logger.LogTrace("Cannot send pair request to {TargetName}@{World} while in PvP or GPose.", target.TargetName, target.TargetHomeWorld.RowId);
|
_logger.LogTrace("Cannot send pair request to {TargetName}@{World} while in PvP or GPose.", target.TargetName, target.TargetHomeWorld.RowId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsWorldValid(target.TargetHomeWorld.RowId))
|
var world = GetWorld(target.TargetHomeWorld.RowId);
|
||||||
|
if (!IsWorldValid(world))
|
||||||
{
|
{
|
||||||
_logger.LogTrace("Target player {TargetName}@{World} is on an invalid world.", target.TargetName, target.TargetHomeWorld.RowId);
|
_logger.LogTrace("Target player {TargetName}@{World} is on an invalid world.", target.TargetName, target.TargetHomeWorld.RowId);
|
||||||
return;
|
return;
|
||||||
@@ -208,24 +199,13 @@ internal class ContextMenuService : IHostedService
|
|||||||
.Where(p => p.IsVisible && p.PlayerCharacterId != uint.MaxValue)
|
.Where(p => p.IsVisible && p.PlayerCharacterId != uint.MaxValue)
|
||||||
.Select(p => (ulong)p.PlayerCharacterId)];
|
.Select(p => (ulong)p.PlayerCharacterId)];
|
||||||
|
|
||||||
private void NotifyInChat(string message, NotificationType type = NotificationType.Info)
|
|
||||||
{
|
|
||||||
if (!_configService.Current.UseLightlessNotifications || (_configService.Current.LightlessPairRequestNotification == NotificationLocation.Chat || _configService.Current.LightlessPairRequestNotification == NotificationLocation.ChatAndLightlessUi))
|
|
||||||
{
|
|
||||||
var chatMsg = $"[Lightless] {message}";
|
|
||||||
if (type == NotificationType.Error)
|
|
||||||
_chatGui.PrintError(chatMsg);
|
|
||||||
else
|
|
||||||
_chatGui.Print(chatMsg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleSelection(IMenuArgs args)
|
private async Task HandleSelection(IMenuArgs args)
|
||||||
{
|
{
|
||||||
if (args.Target is not MenuTargetDefault target)
|
if (args.Target is not MenuTargetDefault target)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!target.TargetHomeWorld.IsValid || !IsWorldValid(target.TargetHomeWorld.RowId))
|
var world = GetWorld(target.TargetHomeWorld.RowId);
|
||||||
|
if (!IsWorldValid(world))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -234,11 +214,11 @@ internal class ContextMenuService : IHostedService
|
|||||||
|
|
||||||
if (targetData == null || targetData.Address == nint.Zero)
|
if (targetData == null || targetData.Address == nint.Zero)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Target player {TargetName}@{World} not found in object table.", target.TargetName, target.TargetHomeWorld.Value.Name);
|
_logger.LogWarning("Target player {TargetName}@{World} not found in object table.", target.TargetName, world.Name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var senderCid = _dalamudUtil.GetCID().ToString().GetHash256();
|
var senderCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
|
||||||
var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address);
|
var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address);
|
||||||
|
|
||||||
_logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid);
|
_logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid);
|
||||||
@@ -247,9 +227,6 @@ internal class ContextMenuService : IHostedService
|
|||||||
{
|
{
|
||||||
_pairRequestService.RemoveRequest(receiverCid);
|
_pairRequestService.RemoveRequest(receiverCid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify in chat when NotificationService is disabled
|
|
||||||
NotifyInChat($"Pair request sent to {target.TargetName}@{target.TargetHomeWorld.Value.Name}.", NotificationType.Info);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -309,8 +286,37 @@ internal class ContextMenuService : IHostedService
|
|||||||
p.HomeWorld.RowId == target.TargetHomeWorld.RowId);
|
p.HomeWorld.RowId == target.TargetHomeWorld.RowId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsWorldValid(uint worldId)
|
private World GetWorld(uint worldId)
|
||||||
{
|
{
|
||||||
return _dalamudUtil.WorldData.Value.ContainsKey((ushort)worldId);
|
var sheet = _gameData.GetExcelSheet<World>()!;
|
||||||
|
var luminaWorlds = sheet.Where(x =>
|
||||||
|
{
|
||||||
|
var dc = x.DataCenter.ValueNullable;
|
||||||
|
var name = x.Name.ExtractText();
|
||||||
|
var internalName = x.InternalName.ExtractText();
|
||||||
|
|
||||||
|
if (dc == null || dc.Value.Region == 0 || string.IsNullOrWhiteSpace(dc.Value.Name.ExtractText()))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(internalName))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (name.Contains('-', StringComparison.Ordinal) || name.Contains('_', StringComparison.Ordinal))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return x.DataCenter.Value.Region != 5 || x.RowId > 3001 && x.RowId != 1200 && IsChineseJapaneseKoreanString(name);
|
||||||
|
});
|
||||||
|
|
||||||
|
return luminaWorlds.FirstOrDefault(x => x.RowId == worldId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsChineseJapaneseKoreanString(string text) => text.All(IsChineseJapaneseKoreanCharacter);
|
||||||
|
|
||||||
|
private static bool IsChineseJapaneseKoreanCharacter(char c) => c >= 0x4E00 && c <= 0x9FFF;
|
||||||
|
|
||||||
|
public static bool IsWorldValid(World world)
|
||||||
|
{
|
||||||
|
var name = world.Name.ToString();
|
||||||
|
return !string.IsNullOrWhiteSpace(name) && char.IsUpper(name[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
using Dalamud.Game.ClientState.Conditions;
|
using Dalamud.Game.ClientState.Conditions;
|
||||||
|
using Dalamud.Game.ClientState.Objects;
|
||||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
using Dalamud.Game.ClientState.Objects.Types;
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
using Dalamud.Game.Text;
|
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Control;
|
using FFXIVClientStructs.FFXIV.Client.Game.Control;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||||
using LightlessSync.API.Dto.CharaData;
|
using LightlessSync.API.Dto.CharaData;
|
||||||
@@ -25,10 +24,8 @@ using Microsoft.Extensions.Logging;
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using BattleNpcSubKind = FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSubKind;
|
|
||||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||||
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
||||||
using Map = Lumina.Excel.Sheets.Map;
|
|
||||||
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
|
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
|
||||||
|
|
||||||
namespace LightlessSync.Services;
|
namespace LightlessSync.Services;
|
||||||
@@ -40,7 +37,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
private readonly ICondition _condition;
|
private readonly ICondition _condition;
|
||||||
private readonly IDataManager _gameData;
|
private readonly IDataManager _gameData;
|
||||||
private readonly IGameConfig _gameConfig;
|
private readonly IGameConfig _gameConfig;
|
||||||
private readonly IPlayerState _playerState;
|
|
||||||
private readonly BlockedCharacterHandler _blockedCharacterHandler;
|
private readonly BlockedCharacterHandler _blockedCharacterHandler;
|
||||||
private readonly IFramework _framework;
|
private readonly IFramework _framework;
|
||||||
private readonly IGameGui _gameGui;
|
private readonly IGameGui _gameGui;
|
||||||
@@ -60,12 +56,11 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
private string _lastGlobalBlockReason = string.Empty;
|
private string _lastGlobalBlockReason = string.Empty;
|
||||||
private ushort _lastZone = 0;
|
private ushort _lastZone = 0;
|
||||||
private ushort _lastWorldId = 0;
|
private ushort _lastWorldId = 0;
|
||||||
private uint _lastMapId = 0;
|
|
||||||
private bool _sentBetweenAreas = false;
|
private bool _sentBetweenAreas = false;
|
||||||
private Lazy<ulong> _cid;
|
private Lazy<ulong> _cid;
|
||||||
|
|
||||||
public DalamudUtilService(ILogger<DalamudUtilService> logger, IClientState clientState, IObjectTable objectTable, IFramework framework,
|
public DalamudUtilService(ILogger<DalamudUtilService> logger, IClientState clientState, IObjectTable objectTable, IFramework framework,
|
||||||
IGameGui gameGui, ICondition condition, IDataManager gameData, ITargetManager targetManager, IGameConfig gameConfig, IPlayerState playerState,
|
IGameGui gameGui, ICondition condition, IDataManager gameData, ITargetManager targetManager, IGameConfig gameConfig,
|
||||||
ActorObjectService actorObjectService, BlockedCharacterHandler blockedCharacterHandler, LightlessMediator mediator, PerformanceCollectorService performanceCollector,
|
ActorObjectService actorObjectService, BlockedCharacterHandler blockedCharacterHandler, LightlessMediator mediator, PerformanceCollectorService performanceCollector,
|
||||||
LightlessConfigService configService, PlayerPerformanceConfigService playerPerformanceConfigService, Lazy<PairFactory> pairFactory)
|
LightlessConfigService configService, PlayerPerformanceConfigService playerPerformanceConfigService, Lazy<PairFactory> pairFactory)
|
||||||
{
|
{
|
||||||
@@ -77,7 +72,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
_condition = condition;
|
_condition = condition;
|
||||||
_gameData = gameData;
|
_gameData = gameData;
|
||||||
_gameConfig = gameConfig;
|
_gameConfig = gameConfig;
|
||||||
_playerState = playerState;
|
|
||||||
_actorObjectService = actorObjectService;
|
_actorObjectService = actorObjectService;
|
||||||
_targetManager = targetManager;
|
_targetManager = targetManager;
|
||||||
_blockedCharacterHandler = blockedCharacterHandler;
|
_blockedCharacterHandler = blockedCharacterHandler;
|
||||||
@@ -86,27 +80,53 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
_configService = configService;
|
_configService = configService;
|
||||||
_playerPerformanceConfigService = playerPerformanceConfigService;
|
_playerPerformanceConfigService = playerPerformanceConfigService;
|
||||||
_pairFactory = pairFactory;
|
_pairFactory = pairFactory;
|
||||||
var clientLanguage = _clientState.ClientLanguage;
|
|
||||||
WorldData = new(() =>
|
WorldData = new(() =>
|
||||||
{
|
{
|
||||||
return gameData.GetExcelSheet<Lumina.Excel.Sheets.World>(clientLanguage)!
|
return gameData.GetExcelSheet<Lumina.Excel.Sheets.World>(Dalamud.Game.ClientLanguage.English)!
|
||||||
.Where(w => !w.Name.IsEmpty && w.DataCenter.RowId != 0 && (w.IsPublic || char.IsUpper(w.Name.ToString()[0])
|
.Where(w => !w.Name.IsEmpty && w.DataCenter.RowId != 0 && (w.IsPublic || char.IsUpper(w.Name.ToString()[0])))
|
||||||
|| w is { RowId: > 1000, UserType: 101 or 201 }))
|
|
||||||
.ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString());
|
.ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString());
|
||||||
});
|
});
|
||||||
JobData = new(() =>
|
JobData = new(() =>
|
||||||
{
|
{
|
||||||
return gameData.GetExcelSheet<ClassJob>(clientLanguage)!
|
return gameData.GetExcelSheet<ClassJob>(Dalamud.Game.ClientLanguage.English)!
|
||||||
.ToDictionary(k => k.RowId, k => k.Name.ToString());
|
.ToDictionary(k => k.RowId, k => k.NameEnglish.ToString());
|
||||||
});
|
});
|
||||||
TerritoryData = new(() => BuildTerritoryData(clientLanguage));
|
TerritoryData = new(() =>
|
||||||
TerritoryDataEnglish = new(() => BuildTerritoryData(Dalamud.Game.ClientLanguage.English));
|
|
||||||
MapData = new(() => BuildMapData(clientLanguage));
|
|
||||||
ContentFinderData = new Lazy<Dictionary<uint, string>>(() =>
|
|
||||||
{
|
{
|
||||||
return _gameData.GetExcelSheet<TerritoryType>()!
|
return gameData.GetExcelSheet<TerritoryType>(Dalamud.Game.ClientLanguage.English)!
|
||||||
.Where(w => w.RowId != 0 && !string.IsNullOrEmpty(w.ContentFinderCondition.ValueNullable?.Name.ToString()))
|
.Where(w => w.RowId != 0)
|
||||||
.ToDictionary(w => w.RowId, w => w.ContentFinderCondition.Value.Name.ToString());
|
.ToDictionary(w => w.RowId, w =>
|
||||||
|
{
|
||||||
|
StringBuilder sb = new();
|
||||||
|
sb.Append(w.PlaceNameRegion.Value.Name);
|
||||||
|
if (w.PlaceName.ValueNullable != null)
|
||||||
|
{
|
||||||
|
sb.Append(" - ");
|
||||||
|
sb.Append(w.PlaceName.Value.Name);
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
MapData = new(() =>
|
||||||
|
{
|
||||||
|
return gameData.GetExcelSheet<Map>(Dalamud.Game.ClientLanguage.English)!
|
||||||
|
.Where(w => w.RowId != 0)
|
||||||
|
.ToDictionary(w => w.RowId, w =>
|
||||||
|
{
|
||||||
|
StringBuilder sb = new();
|
||||||
|
sb.Append(w.PlaceNameRegion.Value.Name);
|
||||||
|
if (w.PlaceName.ValueNullable != null)
|
||||||
|
{
|
||||||
|
sb.Append(" - ");
|
||||||
|
sb.Append(w.PlaceName.Value.Name);
|
||||||
|
}
|
||||||
|
if (w.PlaceNameSub.ValueNullable != null && !string.IsNullOrEmpty(w.PlaceNameSub.Value.Name.ToString()))
|
||||||
|
{
|
||||||
|
sb.Append(" - ");
|
||||||
|
sb.Append(w.PlaceNameSub.Value.Name);
|
||||||
|
}
|
||||||
|
return (w, sb.ToString());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
mediator.Subscribe<TargetPairMessage>(this, (msg) =>
|
mediator.Subscribe<TargetPairMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
@@ -138,71 +158,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
private Lazy<ulong> RebuildCID() => new(GetCID);
|
private Lazy<ulong> RebuildCID() => new(GetCID);
|
||||||
|
|
||||||
public bool IsWine { get; init; }
|
public bool IsWine { get; init; }
|
||||||
private Dictionary<uint, string> BuildTerritoryData(Dalamud.Game.ClientLanguage language)
|
|
||||||
{
|
|
||||||
var placeNames = _gameData.GetExcelSheet<PlaceName>(language)!;
|
|
||||||
return _gameData.GetExcelSheet<TerritoryType>(language)!
|
|
||||||
.Where(w => w.RowId != 0)
|
|
||||||
.ToDictionary(w => w.RowId, w =>
|
|
||||||
{
|
|
||||||
var regionName = GetPlaceName(placeNames, w.PlaceNameRegion.RowId);
|
|
||||||
var placeName = GetPlaceName(placeNames, w.PlaceName.RowId);
|
|
||||||
return BuildPlaceName(regionName, placeName, string.Empty);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private Dictionary<uint, (Map Map, string MapName)> BuildMapData(Dalamud.Game.ClientLanguage language)
|
|
||||||
{
|
|
||||||
var placeNames = _gameData.GetExcelSheet<PlaceName>(language)!;
|
|
||||||
return _gameData.GetExcelSheet<Map>(language)!
|
|
||||||
.Where(w => w.RowId != 0)
|
|
||||||
.ToDictionary(w => w.RowId, w =>
|
|
||||||
{
|
|
||||||
var regionName = GetPlaceName(placeNames, w.PlaceNameRegion.RowId);
|
|
||||||
var placeName = GetPlaceName(placeNames, w.PlaceName.RowId);
|
|
||||||
var subPlaceName = GetPlaceName(placeNames, w.PlaceNameSub.RowId);
|
|
||||||
var displayName = BuildPlaceName(regionName, placeName, subPlaceName);
|
|
||||||
return (w, displayName);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
private static string GetPlaceName(Lumina.Excel.ExcelSheet<PlaceName> placeNames, uint rowId)
|
|
||||||
{
|
|
||||||
if (rowId == 0)
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
return placeNames.GetRow(rowId).Name.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildPlaceName(string regionName, string placeName, string subPlaceName)
|
|
||||||
{
|
|
||||||
StringBuilder sb = new();
|
|
||||||
if (!string.IsNullOrWhiteSpace(regionName))
|
|
||||||
{
|
|
||||||
sb.Append(regionName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(placeName))
|
|
||||||
{
|
|
||||||
if (sb.Length > 0)
|
|
||||||
{
|
|
||||||
sb.Append(" - ");
|
|
||||||
}
|
|
||||||
sb.Append(placeName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(subPlaceName))
|
|
||||||
{
|
|
||||||
if (sb.Length > 0)
|
|
||||||
{
|
|
||||||
sb.Append(" - ");
|
|
||||||
}
|
|
||||||
sb.Append(subPlaceName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
private bool ResolvePairAddress(Pair pair, out Pair resolvedPair, out nint address)
|
private bool ResolvePairAddress(Pair pair, out Pair resolvedPair, out nint address)
|
||||||
{
|
{
|
||||||
resolvedPair = _pairFactory.Value.Create(pair.UniqueIdent) ?? pair;
|
resolvedPair = _pairFactory.Value.Create(pair.UniqueIdent) ?? pair;
|
||||||
@@ -227,28 +182,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
_ = RunOnFrameworkThread(ReleaseFocusUnsafe);
|
_ = RunOnFrameworkThread(ReleaseFocusUnsafe);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void TargetPlayerByAddress(nint address)
|
|
||||||
{
|
|
||||||
if (address == nint.Zero) return;
|
|
||||||
if (_clientState.IsPvP) return;
|
|
||||||
|
|
||||||
_ = RunOnFrameworkThread(() =>
|
|
||||||
{
|
|
||||||
var gameObject = CreateGameObject(address);
|
|
||||||
if (gameObject is null) return;
|
|
||||||
|
|
||||||
var useFocusTarget = _configService.Current.UseFocusTarget;
|
|
||||||
if (useFocusTarget)
|
|
||||||
{
|
|
||||||
_targetManager.FocusTarget = gameObject;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_targetManager.Target = gameObject;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FocusPairUnsafe(nint address, PairUniqueIdentifier pairIdent)
|
private void FocusPairUnsafe(nint address, PairUniqueIdentifier pairIdent)
|
||||||
{
|
{
|
||||||
var target = CreateGameObject(address);
|
var target = CreateGameObject(address);
|
||||||
@@ -300,7 +233,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
public bool IsAnythingDrawing { get; private set; } = false;
|
public bool IsAnythingDrawing { get; private set; } = false;
|
||||||
public bool IsInCutscene { get; private set; } = false;
|
public bool IsInCutscene { get; private set; } = false;
|
||||||
public bool IsInGpose { get; private set; } = false;
|
public bool IsInGpose { get; private set; } = false;
|
||||||
public bool IsGameUiHidden => _gameGui.GameUiHidden;
|
|
||||||
public bool IsLoggedIn { get; private set; }
|
public bool IsLoggedIn { get; private set; }
|
||||||
public bool IsOnFrameworkThread => _framework.IsInFrameworkUpdateThread;
|
public bool IsOnFrameworkThread => _framework.IsInFrameworkUpdateThread;
|
||||||
public bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
|
public bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
|
||||||
@@ -313,9 +245,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
public Lazy<Dictionary<uint, string>> JobData { get; private set; }
|
public Lazy<Dictionary<uint, string>> JobData { get; private set; }
|
||||||
public Lazy<Dictionary<ushort, string>> WorldData { get; private set; }
|
public Lazy<Dictionary<ushort, string>> WorldData { get; private set; }
|
||||||
public Lazy<Dictionary<uint, string>> TerritoryData { get; private set; }
|
public Lazy<Dictionary<uint, string>> TerritoryData { get; private set; }
|
||||||
public Lazy<Dictionary<uint, string>> TerritoryDataEnglish { get; private set; }
|
|
||||||
public Lazy<Dictionary<uint, (Map Map, string MapName)>> MapData { get; private set; }
|
public Lazy<Dictionary<uint, (Map Map, string MapName)>> MapData { get; private set; }
|
||||||
public Lazy<Dictionary<uint, string>> ContentFinderData { get; private set; }
|
|
||||||
public bool IsLodEnabled { get; private set; }
|
public bool IsLodEnabled { get; private set; }
|
||||||
public LightlessMediator Mediator { get; }
|
public LightlessMediator Mediator { get; }
|
||||||
|
|
||||||
@@ -334,7 +264,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TerritoryDataEnglish.Value.TryGetValue(territoryId, out var name) || string.IsNullOrWhiteSpace(name))
|
if (!TerritoryData.Value.TryGetValue(territoryId, out var name) || string.IsNullOrWhiteSpace(name))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -397,8 +327,8 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
public IEnumerable<ICharacter?> GetGposeCharactersFromObjectTable()
|
public IEnumerable<ICharacter?> GetGposeCharactersFromObjectTable()
|
||||||
{
|
{
|
||||||
foreach (var actor in _objectTable
|
foreach (var actor in _actorObjectService.PlayerDescriptors
|
||||||
.Where(a => a.ObjectIndex > 200 && a.ObjectKind == DalamudObjectKind.Player))
|
.Where(a => a.ObjectKind == DalamudObjectKind.Player && a.ObjectIndex > 200))
|
||||||
{
|
{
|
||||||
var character = _objectTable.CreateObjectReference(actor.Address) as ICharacter;
|
var character = _objectTable.CreateObjectReference(actor.Address) as ICharacter;
|
||||||
if (character != null)
|
if (character != null)
|
||||||
@@ -409,7 +339,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
public bool GetIsPlayerPresent()
|
public bool GetIsPlayerPresent()
|
||||||
{
|
{
|
||||||
EnsureIsOnFramework();
|
EnsureIsOnFramework();
|
||||||
return _objectTable.LocalPlayer != null && _objectTable.LocalPlayer.IsValid() && _playerState.IsLoaded;
|
return _objectTable.LocalPlayer != null && _objectTable.LocalPlayer.IsValid();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> GetIsPlayerPresentAsync()
|
public async Task<bool> GetIsPlayerPresentAsync()
|
||||||
@@ -424,6 +354,25 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
if (playerPointer == IntPtr.Zero) return IntPtr.Zero;
|
if (playerPointer == IntPtr.Zero) return IntPtr.Zero;
|
||||||
|
|
||||||
var playerAddress = playerPointer.Value;
|
var playerAddress = playerPointer.Value;
|
||||||
|
var ownerEntityId = ((Character*)playerAddress)->EntityId;
|
||||||
|
if (ownerEntityId == 0) return IntPtr.Zero;
|
||||||
|
|
||||||
|
if (playerAddress == _actorObjectService.LocalPlayerAddress)
|
||||||
|
{
|
||||||
|
var localOwned = _actorObjectService.LocalMinionOrMountAddress;
|
||||||
|
if (localOwned != nint.Zero)
|
||||||
|
{
|
||||||
|
return localOwned;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ownedObject = FindOwnedObject(ownerEntityId, playerAddress, static kind =>
|
||||||
|
kind == DalamudObjectKind.MountType || kind == DalamudObjectKind.Companion);
|
||||||
|
if (ownedObject != nint.Zero)
|
||||||
|
{
|
||||||
|
return ownedObject;
|
||||||
|
}
|
||||||
|
|
||||||
return _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1);
|
return _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,22 +388,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
var mgr = CharacterManager.Instance();
|
var mgr = CharacterManager.Instance();
|
||||||
playerPointer ??= GetPlayerPtr();
|
playerPointer ??= GetPlayerPtr();
|
||||||
if (playerPointer == IntPtr.Zero || (IntPtr)mgr == IntPtr.Zero) return IntPtr.Zero;
|
if (playerPointer == IntPtr.Zero || (IntPtr)mgr == IntPtr.Zero) return IntPtr.Zero;
|
||||||
|
return (IntPtr)mgr->LookupPetByOwnerObject((BattleChara*)playerPointer);
|
||||||
var ownerAddress = playerPointer.Value;
|
|
||||||
var ownerEntityId = ((Character*)ownerAddress)->EntityId;
|
|
||||||
if (ownerEntityId == 0) return IntPtr.Zero;
|
|
||||||
|
|
||||||
var candidate = (IntPtr)mgr->LookupPetByOwnerObject((BattleChara*)ownerAddress);
|
|
||||||
if (candidate != IntPtr.Zero)
|
|
||||||
{
|
|
||||||
var candidateObj = (GameObject*)candidate;
|
|
||||||
if (IsPetMatch(candidateObj, ownerEntityId))
|
|
||||||
{
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return IntPtr.Zero;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IntPtr> GetPetAsync(IntPtr? playerPointer = null)
|
public async Task<IntPtr> GetPetAsync(IntPtr? playerPointer = null)
|
||||||
@@ -462,24 +396,33 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
return await RunOnFrameworkThread(() => GetPetPtr(playerPointer)).ConfigureAwait(false);
|
return await RunOnFrameworkThread(() => GetPetPtr(playerPointer)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static unsafe bool IsPetMatch(GameObject* candidate, uint ownerEntityId)
|
private unsafe nint FindOwnedObject(uint ownerEntityId, nint ownerAddress, Func<DalamudObjectKind, bool> matchesKind)
|
||||||
{
|
{
|
||||||
if (candidate == null)
|
if (ownerEntityId == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return nint.Zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((DalamudObjectKind)candidate->ObjectKind != DalamudObjectKind.BattleNpc)
|
foreach (var obj in _objectTable)
|
||||||
{
|
{
|
||||||
return false;
|
if (obj is null || obj.Address == nint.Zero || obj.Address == ownerAddress)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matchesKind(obj.ObjectKind))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidate = (GameObject*)obj.Address;
|
||||||
|
if (ResolveOwnerId(candidate) == ownerEntityId)
|
||||||
|
{
|
||||||
|
return obj.Address;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (candidate->BattleNpcSubKind != BattleNpcSubKind.Pet)
|
return nint.Zero;
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResolveOwnerId(candidate) == ownerEntityId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static unsafe uint ResolveOwnerId(GameObject* gameObject)
|
private static unsafe uint ResolveOwnerId(GameObject* gameObject)
|
||||||
@@ -529,17 +472,30 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
public string GetPlayerName()
|
public string GetPlayerName()
|
||||||
{
|
{
|
||||||
return _playerState.CharacterName;
|
EnsureIsOnFramework();
|
||||||
|
return _objectTable.LocalPlayer?.Name.ToString() ?? "--";
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetPlayerNameAsync()
|
||||||
|
{
|
||||||
|
return await RunOnFrameworkThread(GetPlayerName).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ulong> GetCIDAsync()
|
||||||
|
{
|
||||||
|
return await RunOnFrameworkThread(GetCID).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe ulong GetCID()
|
public unsafe ulong GetCID()
|
||||||
{
|
{
|
||||||
return _playerState.ContentId;
|
EnsureIsOnFramework();
|
||||||
|
var playerChar = GetPlayerCharacter();
|
||||||
|
return ((BattleChara*)playerChar.Address)->Character.ContentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetPlayerNameHashed()
|
public async Task<string> GetPlayerNameHashedAsync()
|
||||||
{
|
{
|
||||||
return _cid.Value.ToString().GetHash256();
|
return await RunOnFrameworkThread(() => _cid.Value.ToString().GetHash256()).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static unsafe bool TryGetHashedCID(IPlayerCharacter? playerCharacter, out string hashedCid)
|
public static unsafe bool TryGetHashedCID(IPlayerCharacter? playerCharacter, out string hashedCid)
|
||||||
@@ -560,37 +516,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGetHashedCIDFromAddress(nint address, out string hashedCid)
|
|
||||||
{
|
|
||||||
hashedCid = string.Empty;
|
|
||||||
if (address == nint.Zero)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (_framework.IsInFrameworkUpdateThread)
|
|
||||||
{
|
|
||||||
return TryGetHashedCIDFromAddressInternal(address, out hashedCid);
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = _framework.RunOnFrameworkThread(() =>
|
|
||||||
{
|
|
||||||
var success = TryGetHashedCIDFromAddressInternal(address, out var resolved);
|
|
||||||
return (success, resolved);
|
|
||||||
}).GetAwaiter().GetResult();
|
|
||||||
|
|
||||||
hashedCid = result.resolved;
|
|
||||||
return result.success;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryGetHashedCIDFromAddressInternal(nint address, out string hashedCid)
|
|
||||||
{
|
|
||||||
hashedCid = string.Empty;
|
|
||||||
var player = _objectTable.CreateObjectReference(address) as IPlayerCharacter;
|
|
||||||
if (player == null || player.Address != address)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return TryGetHashedCID(player, out hashedCid);
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe static string GetHashedCIDFromPlayerPointer(nint ptr)
|
public unsafe static string GetHashedCIDFromPlayerPointer(nint ptr)
|
||||||
{
|
{
|
||||||
return ((BattleChara*)ptr)->Character.ContentId.ToString().GetHash256();
|
return ((BattleChara*)ptr)->Character.ContentId.ToString().GetHash256();
|
||||||
@@ -609,116 +534,54 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
public uint GetHomeWorldId()
|
public uint GetHomeWorldId()
|
||||||
{
|
{
|
||||||
return _playerState.HomeWorld.RowId;
|
EnsureIsOnFramework();
|
||||||
|
return _objectTable.LocalPlayer?.HomeWorld.RowId ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public uint GetWorldId()
|
public uint GetWorldId()
|
||||||
{
|
{
|
||||||
return _playerState.CurrentWorld.RowId;
|
EnsureIsOnFramework();
|
||||||
|
return _objectTable.LocalPlayer!.CurrentWorld.RowId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe LocationInfo GetMapData()
|
public unsafe LocationInfo GetMapData()
|
||||||
{
|
{
|
||||||
|
EnsureIsOnFramework();
|
||||||
|
var agentMap = AgentMap.Instance();
|
||||||
var houseMan = HousingManager.Instance();
|
var houseMan = HousingManager.Instance();
|
||||||
|
uint serverId = 0;
|
||||||
var location = new LocationInfo();
|
if (_objectTable.LocalPlayer == null) serverId = 0;
|
||||||
location.ServerId = _playerState.CurrentWorld.RowId;
|
else serverId = _objectTable.LocalPlayer.CurrentWorld.RowId;
|
||||||
location.InstanceId = UIState.Instance()->PublicInstance.InstanceId;
|
uint mapId = agentMap == null ? 0 : agentMap->CurrentMapId;
|
||||||
location.TerritoryId = _clientState.TerritoryType;
|
uint territoryId = agentMap == null ? 0 : agentMap->CurrentTerritoryId;
|
||||||
location.MapId = _clientState.MapId;
|
uint divisionId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentDivision());
|
||||||
if (houseMan != null)
|
uint wardId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentWard() + 1);
|
||||||
|
uint houseId = 0;
|
||||||
|
var tempHouseId = houseMan == null ? 0 : (houseMan->GetCurrentPlot());
|
||||||
|
if (!houseMan->IsInside()) tempHouseId = 0;
|
||||||
|
if (tempHouseId < -1)
|
||||||
{
|
{
|
||||||
if (houseMan->IsInside())
|
divisionId = tempHouseId == -127 ? 2 : (uint)1;
|
||||||
{
|
tempHouseId = 100;
|
||||||
location.TerritoryId = HousingManager.GetOriginalHouseTerritoryTypeId();
|
|
||||||
var house = houseMan->GetCurrentIndoorHouseId();
|
|
||||||
location.WardId = house.WardIndex + 1u;
|
|
||||||
location.HouseId = house.IsApartment ? 100 : house.PlotIndex + 1u;
|
|
||||||
location.RoomId = (uint)house.RoomNumber;
|
|
||||||
location.DivisionId = house.IsApartment ? house.ApartmentDivision + 1u : houseMan->GetCurrentDivision();
|
|
||||||
}
|
|
||||||
else if (houseMan->IsInWorkshop())
|
|
||||||
{
|
|
||||||
var workShop = houseMan->WorkshopTerritory;
|
|
||||||
var house = workShop->HouseId;
|
|
||||||
location.WardId = house.WardIndex + 1u;
|
|
||||||
location.HouseId = house.PlotIndex + 1u;
|
|
||||||
}
|
|
||||||
else if (houseMan->IsOutside())
|
|
||||||
{
|
|
||||||
var outside = houseMan->OutdoorTerritory;
|
|
||||||
var house = outside->HouseId;
|
|
||||||
location.WardId = house.WardIndex + 1u;
|
|
||||||
//location.HouseId = (uint)houseMan->GetCurrentPlot() + 1;
|
|
||||||
location.DivisionId = houseMan->GetCurrentDivision();
|
|
||||||
}
|
|
||||||
//_logger.LogWarning(LocationToString(location));
|
|
||||||
}
|
}
|
||||||
return location;
|
if (tempHouseId == -1) tempHouseId = 0;
|
||||||
}
|
houseId = (uint)tempHouseId;
|
||||||
|
if (houseId != 0)
|
||||||
public string LocationToString(LocationInfo location)
|
|
||||||
{
|
|
||||||
if (location.ServerId is 0 || location.TerritoryId is 0) return String.Empty;
|
|
||||||
var str = WorldData.Value[(ushort)location.ServerId];
|
|
||||||
|
|
||||||
if (ContentFinderData.Value.TryGetValue(location.TerritoryId, out var dutyName))
|
|
||||||
{
|
{
|
||||||
str += $" - [In Duty]{dutyName}";
|
territoryId = HousingManager.GetOriginalHouseTerritoryTypeId();
|
||||||
}
|
}
|
||||||
else
|
uint roomId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentRoom());
|
||||||
|
|
||||||
|
return new LocationInfo()
|
||||||
{
|
{
|
||||||
if (location.HouseId is not 0 || location.MapId is 0) // Dont show mapName when in house/no map available
|
ServerId = serverId,
|
||||||
{
|
MapId = mapId,
|
||||||
str += $" - {TerritoryData.Value[(ushort)location.TerritoryId]}";
|
TerritoryId = territoryId,
|
||||||
}
|
DivisionId = divisionId,
|
||||||
else
|
WardId = wardId,
|
||||||
{
|
HouseId = houseId,
|
||||||
str += $" - {MapData.Value[(ushort)location.MapId].MapName}";
|
RoomId = roomId
|
||||||
}
|
};
|
||||||
|
|
||||||
if (location.InstanceId is not 0)
|
|
||||||
{
|
|
||||||
str += ((SeIconChar)(57520 + location.InstanceId)).ToIconString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (location.WardId is not 0)
|
|
||||||
{
|
|
||||||
str += $" Ward #{location.WardId}";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (location.HouseId is not 0 and not 100)
|
|
||||||
{
|
|
||||||
str += $" House #{location.HouseId}";
|
|
||||||
}
|
|
||||||
else if (location.HouseId is 100)
|
|
||||||
{
|
|
||||||
str += $" {(location.DivisionId == 2 ? "[Subdivision]" : "")} Apartment";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (location.RoomId is not 0)
|
|
||||||
{
|
|
||||||
str += $" Room #{location.RoomId}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string LocationToLifestream(LocationInfo location)
|
|
||||||
{
|
|
||||||
if (location.ServerId is 0 || location.TerritoryId is 0 || ContentFinderData.Value.ContainsKey(location.TerritoryId)) return String.Empty;
|
|
||||||
var str = WorldData.Value[(ushort)location.ServerId];
|
|
||||||
if (location.HouseId is 0 && location.MapId is not 0)
|
|
||||||
{
|
|
||||||
var mapName = MapData.Value[(ushort)location.MapId].MapName;
|
|
||||||
var parts = mapName.Split(" - ", StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
var locationName = parts.Length > 0 ? parts[^1] : mapName;
|
|
||||||
str += $", tp {locationName}";
|
|
||||||
string message = $"LocationToLifestream: {str}";
|
|
||||||
_logger.LogInformation(message);
|
|
||||||
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe void SetMarkerAndOpenMap(Vector3 position, Map map)
|
public unsafe void SetMarkerAndOpenMap(Vector3 position, Map map)
|
||||||
@@ -730,6 +593,21 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
agentMap->SetFlagMapMarker(map.TerritoryType.RowId, map.RowId, position);
|
agentMap->SetFlagMapMarker(map.TerritoryType.RowId, map.RowId, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<LocationInfo> GetMapDataAsync()
|
||||||
|
{
|
||||||
|
return await RunOnFrameworkThread(GetMapData).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<uint> GetWorldIdAsync()
|
||||||
|
{
|
||||||
|
return await RunOnFrameworkThread(GetWorldId).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<uint> GetHomeWorldIdAsync()
|
||||||
|
{
|
||||||
|
return await RunOnFrameworkThread(GetHomeWorldId).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
public unsafe bool IsGameObjectPresent(IntPtr key)
|
public unsafe bool IsGameObjectPresent(IntPtr key)
|
||||||
{
|
{
|
||||||
return _objectTable.Any(f => f.Address == key);
|
return _objectTable.Any(f => f.Address == key);
|
||||||
@@ -789,12 +667,9 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
{
|
{
|
||||||
_logger.LogInformation("Starting DalamudUtilService");
|
_logger.LogInformation("Starting DalamudUtilService");
|
||||||
_framework.Update += FrameworkOnUpdate;
|
_framework.Update += FrameworkOnUpdate;
|
||||||
_clientState.Login += OnClientLogin;
|
if (IsLoggedIn)
|
||||||
_clientState.Logout += OnClientLogout;
|
|
||||||
|
|
||||||
if (_clientState.IsLoggedIn)
|
|
||||||
{
|
{
|
||||||
OnClientLogin();
|
_classJobId = _objectTable.LocalPlayer!.ClassJob.RowId;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Started DalamudUtilService");
|
_logger.LogInformation("Started DalamudUtilService");
|
||||||
@@ -807,8 +682,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
Mediator.UnsubscribeAll(this);
|
Mediator.UnsubscribeAll(this);
|
||||||
_framework.Update -= FrameworkOnUpdate;
|
_framework.Update -= FrameworkOnUpdate;
|
||||||
_clientState.Login -= OnClientLogin;
|
|
||||||
_clientState.Logout -= OnClientLogout;
|
|
||||||
if (_FocusPairIdent.HasValue)
|
if (_FocusPairIdent.HasValue)
|
||||||
{
|
{
|
||||||
if (_framework.IsInFrameworkUpdateThread)
|
if (_framework.IsInFrameworkUpdateThread)
|
||||||
@@ -823,45 +696,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnClientLogin()
|
|
||||||
{
|
|
||||||
if (IsLoggedIn)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_ = RunOnFrameworkThread(() =>
|
|
||||||
{
|
|
||||||
if (IsLoggedIn)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var localPlayer = _objectTable.LocalPlayer;
|
|
||||||
IsLoggedIn = true;
|
|
||||||
_lastZone = _clientState.TerritoryType;
|
|
||||||
if (localPlayer != null)
|
|
||||||
{
|
|
||||||
_lastWorldId = (ushort)localPlayer.CurrentWorld.RowId;
|
|
||||||
_classJobId = localPlayer.ClassJob.RowId;
|
|
||||||
}
|
|
||||||
|
|
||||||
_cid = RebuildCID();
|
|
||||||
Mediator.Publish(new DalamudLoginMessage());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnClientLogout(int type, int code)
|
|
||||||
{
|
|
||||||
if (!IsLoggedIn)
|
|
||||||
return;
|
|
||||||
_ = RunOnFrameworkThread(() =>
|
|
||||||
{
|
|
||||||
if (!IsLoggedIn)
|
|
||||||
return;
|
|
||||||
|
|
||||||
IsLoggedIn = false;
|
|
||||||
_lastWorldId = 0;
|
|
||||||
Mediator.Publish(new DalamudLogoutMessage());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task WaitWhileCharacterIsDrawing(ILogger logger, GameObjectHandler handler, Guid redrawId, int timeOut = 5000, CancellationToken? ct = null)
|
public async Task WaitWhileCharacterIsDrawing(ILogger logger, GameObjectHandler handler, Guid redrawId, int timeOut = 5000, CancellationToken? ct = null)
|
||||||
{
|
{
|
||||||
if (!_clientState.IsLoggedIn) return;
|
if (!_clientState.IsLoggedIn) return;
|
||||||
@@ -929,11 +763,11 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
public string? GetWorldNameFromPlayerAddress(nint address)
|
public string? GetWorldNameFromPlayerAddress(nint address)
|
||||||
{
|
{
|
||||||
if (address == nint.Zero) return null;
|
if (address == nint.Zero) return null;
|
||||||
|
|
||||||
EnsureIsOnFramework();
|
EnsureIsOnFramework();
|
||||||
var playerCharacter = _objectTable.OfType<IPlayerCharacter>().FirstOrDefault(p => p.Address == address);
|
var playerCharacter = _objectTable.OfType<IPlayerCharacter>().FirstOrDefault(p => p.Address == address);
|
||||||
if (playerCharacter == null) return null;
|
if (playerCharacter == null) return null;
|
||||||
|
|
||||||
var worldId = (ushort)playerCharacter.HomeWorld.RowId;
|
var worldId = (ushort)playerCharacter.HomeWorld.RowId;
|
||||||
return WorldData.Value.TryGetValue(worldId, out var worldName) ? worldName : null;
|
return WorldData.Value.TryGetValue(worldId, out var worldName) ? worldName : null;
|
||||||
}
|
}
|
||||||
@@ -946,7 +780,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
bool isDrawingChanged = false;
|
bool isDrawingChanged = false;
|
||||||
if ((nint)drawObj != IntPtr.Zero)
|
if ((nint)drawObj != IntPtr.Zero)
|
||||||
{
|
{
|
||||||
isDrawing = gameObj->RenderFlags == (VisibilityFlags)0b100000000000;
|
isDrawing = (gameObj->RenderFlags & VisibilityFlags.Nameplate) != VisibilityFlags.None;
|
||||||
if (!isDrawing)
|
if (!isDrawing)
|
||||||
{
|
{
|
||||||
isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0;
|
isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0;
|
||||||
@@ -999,87 +833,51 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
private unsafe void FrameworkOnUpdateInternal()
|
private unsafe void FrameworkOnUpdateInternal()
|
||||||
{
|
{
|
||||||
var localPlayer = _objectTable.LocalPlayer;
|
if ((_objectTable.LocalPlayer?.IsDead ?? false) && _condition[ConditionFlag.BoundByDuty])
|
||||||
if ((localPlayer?.IsDead ?? false) && _condition[ConditionFlag.BoundByDuty])
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isNormalFrameworkUpdate = DateTime.UtcNow < _delayedFrameworkUpdateCheck.AddSeconds(1);
|
bool isNormalFrameworkUpdate = DateTime.UtcNow < _delayedFrameworkUpdateCheck.AddSeconds(1);
|
||||||
var clientLoggedIn = _clientState.IsLoggedIn;
|
|
||||||
|
|
||||||
_performanceCollector.LogPerformance(this, $"FrameworkOnUpdateInternal+{(isNormalFrameworkUpdate ? "Regular" : "Delayed")}", () =>
|
_performanceCollector.LogPerformance(this, $"FrameworkOnUpdateInternal+{(isNormalFrameworkUpdate ? "Regular" : "Delayed")}", () =>
|
||||||
{
|
{
|
||||||
IsAnythingDrawing = false;
|
IsAnythingDrawing = false;
|
||||||
|
|
||||||
if (!isNormalFrameworkUpdate)
|
|
||||||
{
|
|
||||||
if (_gameConfig != null
|
|
||||||
&& _gameConfig.TryGet(Dalamud.Game.Config.SystemConfigOption.LodType_DX11, out bool lodEnabled))
|
|
||||||
{
|
|
||||||
IsLodEnabled = lodEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsInCombat || IsPerforming || IsInInstance)
|
|
||||||
Mediator.Publish(new FrameworkUpdateMessage());
|
|
||||||
|
|
||||||
Mediator.Publish(new DelayedFrameworkUpdateMessage());
|
|
||||||
|
|
||||||
_delayedFrameworkUpdateCheck = DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!clientLoggedIn)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_performanceCollector.LogPerformance(this, $"TrackedActorsToState",
|
_performanceCollector.LogPerformance(this, $"TrackedActorsToState",
|
||||||
() =>
|
() =>
|
||||||
{
|
{
|
||||||
if (!_actorObjectService.HooksActive || !isNormalFrameworkUpdate || _actorObjectService.HasPendingHashResolutions)
|
_actorObjectService.RefreshTrackedActors();
|
||||||
{
|
|
||||||
_actorObjectService.RefreshTrackedActors();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_clientState.IsLoggedIn && localPlayer != null)
|
var playerDescriptors = _actorObjectService.PlayerCharacterDescriptors;
|
||||||
|
for (var i = 0; i < playerDescriptors.Count; i++)
|
||||||
{
|
{
|
||||||
var playerDescriptors = _actorObjectService.PlayerDescriptors;
|
var actor = playerDescriptors[i];
|
||||||
for (var i = 0; i < playerDescriptors.Count; i++)
|
|
||||||
|
var playerAddress = actor.Address;
|
||||||
|
if (playerAddress == nint.Zero)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (actor.ObjectIndex >= 200)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (_blockedCharacterHandler.IsCharacterBlocked(playerAddress, out bool firstTime) && firstTime)
|
||||||
{
|
{
|
||||||
var actor = playerDescriptors[i];
|
_logger.LogTrace("Skipping character {addr}, blocked/muted", playerAddress.ToString("X"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var playerAddress = actor.Address;
|
if (!IsAnythingDrawing)
|
||||||
if (playerAddress == nint.Zero)
|
{
|
||||||
continue;
|
var gameObj = (GameObject*)playerAddress;
|
||||||
|
var currentName = gameObj != null ? gameObj->NameString ?? string.Empty : string.Empty;
|
||||||
if (actor.ObjectIndex >= 200)
|
var charaName = string.IsNullOrEmpty(currentName) ? actor.Name : currentName;
|
||||||
continue;
|
CheckCharacterForDrawing(playerAddress, charaName);
|
||||||
|
if (IsAnythingDrawing)
|
||||||
var obj = _objectTable[actor.ObjectIndex];
|
|
||||||
if (obj is not IPlayerCharacter player || player.Address != playerAddress)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (_blockedCharacterHandler.IsCharacterBlocked(playerAddress, actor.ObjectIndex, out bool firstTime) && firstTime)
|
|
||||||
{
|
|
||||||
_logger.LogTrace("Skipping character {addr}, blocked/muted", playerAddress.ToString("X"));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!IsAnythingDrawing)
|
|
||||||
{
|
|
||||||
var charaName = player.Name.TextValue;
|
|
||||||
if (string.IsNullOrEmpty(charaName))
|
|
||||||
{
|
|
||||||
charaName = actor.Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckCharacterForDrawing(playerAddress, charaName);
|
|
||||||
if (IsAnythingDrawing)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1149,7 +947,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Cutscene
|
// Cutscene
|
||||||
HandleStateTransition(() => IsInCutscene, v => IsInCutscene = v, shouldBeInCutscene, "Cutscene",
|
HandleStateTransition(() => IsInCutscene,v => IsInCutscene = v, shouldBeInCutscene, "Cutscene",
|
||||||
onEnter: () =>
|
onEnter: () =>
|
||||||
{
|
{
|
||||||
Mediator.Publish(new CutsceneStartMessage());
|
Mediator.Publish(new CutsceneStartMessage());
|
||||||
@@ -1193,18 +991,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
Mediator.Publish(new ResumeScanMessage(nameof(ConditionFlag.BetweenAreas)));
|
Mediator.Publish(new ResumeScanMessage(nameof(ConditionFlag.BetweenAreas)));
|
||||||
}
|
}
|
||||||
|
|
||||||
//Map
|
var localPlayer = _objectTable.LocalPlayer;
|
||||||
if (!_sentBetweenAreas)
|
|
||||||
{
|
|
||||||
var mapid = _clientState.MapId;
|
|
||||||
if (mapid != _lastMapId)
|
|
||||||
{
|
|
||||||
_lastMapId = mapid;
|
|
||||||
Mediator.Publish(new MapChangedMessage(mapid));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (localPlayer != null)
|
if (localPlayer != null)
|
||||||
{
|
{
|
||||||
_classJobId = localPlayer.ClassJob.RowId;
|
_classJobId = localPlayer.ClassJob.RowId;
|
||||||
@@ -1226,6 +1013,39 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
Mediator.Publish(new FrameworkUpdateMessage());
|
Mediator.Publish(new FrameworkUpdateMessage());
|
||||||
|
|
||||||
Mediator.Publish(new PriorityFrameworkUpdateMessage());
|
Mediator.Publish(new PriorityFrameworkUpdateMessage());
|
||||||
|
|
||||||
|
if (isNormalFrameworkUpdate)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (localPlayer != null && !IsLoggedIn)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Logged in");
|
||||||
|
IsLoggedIn = true;
|
||||||
|
_lastZone = _clientState.TerritoryType;
|
||||||
|
_lastWorldId = (ushort)localPlayer.CurrentWorld.RowId;
|
||||||
|
_cid = RebuildCID();
|
||||||
|
Mediator.Publish(new DalamudLoginMessage());
|
||||||
|
}
|
||||||
|
else if (localPlayer == null && IsLoggedIn)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Logged out");
|
||||||
|
IsLoggedIn = false;
|
||||||
|
_lastWorldId = 0;
|
||||||
|
Mediator.Publish(new DalamudLogoutMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_gameConfig != null
|
||||||
|
&& _gameConfig.TryGet(Dalamud.Game.Config.SystemConfigOption.LodType_DX11, out bool lodEnabled))
|
||||||
|
{
|
||||||
|
IsLodEnabled = lodEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsInCombat || IsPerforming || IsInInstance)
|
||||||
|
Mediator.Publish(new FrameworkUpdateMessage());
|
||||||
|
|
||||||
|
Mediator.Publish(new DelayedFrameworkUpdateMessage());
|
||||||
|
|
||||||
|
_delayedFrameworkUpdateCheck = DateTime.UtcNow;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,863 +0,0 @@
|
|||||||
using Dalamud.Game.ClientState.Objects.Enums;
|
|
||||||
using Dalamud.Plugin.Services;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
|
||||||
using LightlessSync.LightlessConfiguration;
|
|
||||||
using LightlessSync.Services.Mediator;
|
|
||||||
using LightlessSync.UI;
|
|
||||||
using LightlessSync.UI.Services;
|
|
||||||
using LightlessSync.Utils;
|
|
||||||
using LightlessSync.UtilsEnum.Enum;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Collections.Immutable;
|
|
||||||
using Task = System.Threading.Tasks.Task;
|
|
||||||
|
|
||||||
namespace LightlessSync.Services.LightFinder;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Native nameplate handler that injects LightFinder labels via the signature hook path.
|
|
||||||
/// </summary>
|
|
||||||
public unsafe class LightFinderNativePlateHandler : DisposableMediatorSubscriberBase, IHostedService
|
|
||||||
{
|
|
||||||
private const uint NameplateNodeIdBase = 0x7D99D500;
|
|
||||||
private const string DefaultLabelText = "LightFinder";
|
|
||||||
|
|
||||||
private readonly ILogger<LightFinderNativePlateHandler> _logger;
|
|
||||||
private readonly IClientState _clientState;
|
|
||||||
private readonly IObjectTable _objectTable;
|
|
||||||
private readonly LightlessConfigService _configService;
|
|
||||||
private readonly PairUiService _pairUiService;
|
|
||||||
private readonly NameplateUpdateHookService _nameplateUpdateHookService;
|
|
||||||
|
|
||||||
private readonly int[] _cachedNameplateTextWidths = new int[AddonNamePlate.NumNamePlateObjects];
|
|
||||||
private readonly int[] _cachedNameplateTextHeights = new int[AddonNamePlate.NumNamePlateObjects];
|
|
||||||
private readonly int[] _cachedNameplateContainerHeights = new int[AddonNamePlate.NumNamePlateObjects];
|
|
||||||
private readonly int[] _cachedNameplateTextOffsets = new int[AddonNamePlate.NumNamePlateObjects];
|
|
||||||
private readonly string?[] _lastLabelByIndex = new string?[AddonNamePlate.NumNamePlateObjects];
|
|
||||||
|
|
||||||
private ImmutableHashSet<string> _activeBroadcastingCids = [];
|
|
||||||
private LightfinderLabelRenderer _lastRenderer;
|
|
||||||
private uint _lastSignatureUpdateFrame;
|
|
||||||
private bool _isUpdating;
|
|
||||||
private string _lastLabelContent = DefaultLabelText;
|
|
||||||
|
|
||||||
public LightFinderNativePlateHandler(
|
|
||||||
ILogger<LightFinderNativePlateHandler> logger,
|
|
||||||
IClientState clientState,
|
|
||||||
LightlessConfigService configService,
|
|
||||||
LightlessMediator mediator,
|
|
||||||
IObjectTable objectTable,
|
|
||||||
PairUiService pairUiService,
|
|
||||||
NameplateUpdateHookService nameplateUpdateHookService) : base(logger, mediator)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_clientState = clientState;
|
|
||||||
_configService = configService;
|
|
||||||
_objectTable = objectTable;
|
|
||||||
_pairUiService = pairUiService;
|
|
||||||
_nameplateUpdateHookService = nameplateUpdateHookService;
|
|
||||||
_lastRenderer = _configService.Current.LightfinderLabelRenderer;
|
|
||||||
|
|
||||||
Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsSignatureMode => _configService.Current.LightfinderLabelRenderer == LightfinderLabelRenderer.SignatureHook;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Starts listening for nameplate updates from the hook service.
|
|
||||||
/// </summary>
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_nameplateUpdateHookService.NameplateUpdated += OnNameplateUpdated;
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stops listening for nameplate updates and tears down any constructed nodes.
|
|
||||||
/// </summary>
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_nameplateUpdateHookService.NameplateUpdated -= OnNameplateUpdated;
|
|
||||||
UnsubscribeAll();
|
|
||||||
TryDestroyNameplateNodes();
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Triggered by the sig hook to refresh native nameplate labels.
|
|
||||||
/// </summary>
|
|
||||||
private void HandleNameplateUpdate(RaptureAtkModule* raptureAtkModule)
|
|
||||||
{
|
|
||||||
if (_isUpdating)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_isUpdating = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
RefreshRendererState();
|
|
||||||
if (!IsSignatureMode)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (raptureAtkModule == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var namePlateAddon = GetNamePlateAddon(raptureAtkModule);
|
|
||||||
if (namePlateAddon == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (_clientState.IsGPosing)
|
|
||||||
{
|
|
||||||
HideAllNameplateNodes(namePlateAddon);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fw = Framework.Instance();
|
|
||||||
if (fw == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var frame = fw->FrameCounter;
|
|
||||||
if (_lastSignatureUpdateFrame == frame)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_lastSignatureUpdateFrame = frame;
|
|
||||||
UpdateNameplateNodes(namePlateAddon);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_isUpdating = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Hook callback from the nameplate update signature.
|
|
||||||
/// </summary>
|
|
||||||
private void OnNameplateUpdated(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo,
|
|
||||||
NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex)
|
|
||||||
{
|
|
||||||
HandleNameplateUpdate(raptureAtkModule);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates the active broadcasting CID set and requests a nameplate redraw.
|
|
||||||
/// </summary>
|
|
||||||
public void UpdateBroadcastingCids(IEnumerable<string> cids)
|
|
||||||
{
|
|
||||||
var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal);
|
|
||||||
if (ReferenceEquals(_activeBroadcastingCids, newSet) || _activeBroadcastingCids.SetEquals(newSet))
|
|
||||||
return;
|
|
||||||
|
|
||||||
_activeBroadcastingCids = newSet;
|
|
||||||
if (_logger.IsEnabled(LogLevel.Trace))
|
|
||||||
_logger.LogTrace("Active broadcast IDs (native): {Cids}", string.Join(',', _activeBroadcastingCids));
|
|
||||||
RequestNameplateRedraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sync renderer state with config and clear/remove native nodes if needed.
|
|
||||||
/// </summary>
|
|
||||||
private void RefreshRendererState()
|
|
||||||
{
|
|
||||||
var renderer = _configService.Current.LightfinderLabelRenderer;
|
|
||||||
if (renderer == _lastRenderer)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_lastRenderer = renderer;
|
|
||||||
|
|
||||||
if (renderer == LightfinderLabelRenderer.SignatureHook)
|
|
||||||
{
|
|
||||||
ClearNameplateCaches();
|
|
||||||
RequestNameplateRedraw();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
TryDestroyNameplateNodes();
|
|
||||||
ClearNameplateCaches();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Requests a full nameplate update through the native addon.
|
|
||||||
/// </summary>
|
|
||||||
private void RequestNameplateRedraw()
|
|
||||||
{
|
|
||||||
if (!IsSignatureMode)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var raptureAtkModule = GetRaptureAtkModule();
|
|
||||||
if (raptureAtkModule == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var namePlateAddon = GetNamePlateAddon(raptureAtkModule);
|
|
||||||
if (namePlateAddon == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
namePlateAddon->DoFullUpdate = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private HashSet<ulong> VisibleUserIds
|
|
||||||
=> [.. _pairUiService.GetSnapshot().PairsByUid.Values
|
|
||||||
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
|
||||||
.Select(u => (ulong)u.PlayerCharacterId)];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates/updates LightFinder label nodes for active broadcasts.
|
|
||||||
/// </summary>
|
|
||||||
private void UpdateNameplateNodes(AddonNamePlate* namePlateAddon)
|
|
||||||
{
|
|
||||||
if (namePlateAddon == null)
|
|
||||||
{
|
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
|
||||||
_logger.LogDebug("NamePlate addon unavailable during update, skipping label refresh.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!IsNameplateAddonVisible(namePlateAddon))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!IsSignatureMode)
|
|
||||||
{
|
|
||||||
HideAllNameplateNodes(namePlateAddon);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_activeBroadcastingCids.Count == 0)
|
|
||||||
{
|
|
||||||
HideAllNameplateNodes(namePlateAddon);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var framework = Framework.Instance();
|
|
||||||
if (framework == null)
|
|
||||||
{
|
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
|
||||||
_logger.LogDebug("Framework instance unavailable during nameplate update, skipping.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var uiModule = framework->GetUIModule();
|
|
||||||
if (uiModule == null)
|
|
||||||
{
|
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
|
||||||
_logger.LogDebug("UI module unavailable during nameplate update, skipping.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ui3DModule = uiModule->GetUI3DModule();
|
|
||||||
if (ui3DModule == null)
|
|
||||||
{
|
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
|
||||||
_logger.LogDebug("UI3D module unavailable during nameplate update, skipping.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var vec = ui3DModule->NamePlateObjectInfoPointers;
|
|
||||||
if (vec.IsEmpty)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var config = _configService.Current;
|
|
||||||
var visibleUserIdsSnapshot = VisibleUserIds;
|
|
||||||
var labelColor = UIColors.Get("Lightfinder");
|
|
||||||
var edgeColor = UIColors.Get("LightfinderEdge");
|
|
||||||
var scaleMultiplier = Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f);
|
|
||||||
var baseScale = config.LightfinderLabelUseIcon ? 1.0f : 0.5f;
|
|
||||||
var effectiveScale = baseScale * scaleMultiplier;
|
|
||||||
var labelContent = config.LightfinderLabelUseIcon
|
|
||||||
? LightFinderPlateHandler.NormalizeIconGlyph(config.LightfinderLabelIconGlyph)
|
|
||||||
: DefaultLabelText;
|
|
||||||
|
|
||||||
if (!config.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal)))
|
|
||||||
labelContent = DefaultLabelText;
|
|
||||||
|
|
||||||
if (!string.Equals(_lastLabelContent, labelContent, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
_lastLabelContent = labelContent;
|
|
||||||
Array.Fill(_lastLabelByIndex, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
var desiredFontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed;
|
|
||||||
var baseFontSize = config.LightfinderLabelUseIcon ? 36f : 24f;
|
|
||||||
var desiredFontSize = (byte)Math.Clamp((int)Math.Round(baseFontSize * scaleMultiplier), 1, 255);
|
|
||||||
var desiredFlags = config.LightfinderLabelUseIcon
|
|
||||||
? TextFlags.Edge | TextFlags.Glare | TextFlags.AutoAdjustNodeSize
|
|
||||||
: TextFlags.Edge | TextFlags.Glare;
|
|
||||||
var desiredLineSpacing = (byte)Math.Clamp((int)Math.Round(24 * scaleMultiplier), 0, byte.MaxValue);
|
|
||||||
var defaultNodeWidth = (int)Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
|
||||||
var defaultNodeHeight = (int)Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
|
|
||||||
|
|
||||||
var safeCount = Math.Min(ui3DModule->NamePlateObjectInfoCount, vec.Length);
|
|
||||||
var visibleIndices = new bool[AddonNamePlate.NumNamePlateObjects];
|
|
||||||
|
|
||||||
for (int i = 0; i < safeCount; ++i)
|
|
||||||
{
|
|
||||||
var objectInfoPtr = vec[i];
|
|
||||||
if (objectInfoPtr == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var objectInfo = objectInfoPtr.Value;
|
|
||||||
if (objectInfo == null || objectInfo->GameObject == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var nameplateIndex = objectInfo->NamePlateIndex;
|
|
||||||
if (nameplateIndex < 0 || nameplateIndex >= AddonNamePlate.NumNamePlateObjects)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var gameObject = objectInfo->GameObject;
|
|
||||||
if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject);
|
|
||||||
if (cid == null || !_activeBroadcastingCids.Contains(cid))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var local = _objectTable.LocalPlayer;
|
|
||||||
if (!config.LightfinderLabelShowOwn && local != null &&
|
|
||||||
objectInfo->GameObject->GetGameObjectId() == local.GameObjectId)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var hidePaired = !config.LightfinderLabelShowPaired;
|
|
||||||
var goId = (ulong)gameObject->GetGameObjectId();
|
|
||||||
if (hidePaired && visibleUserIdsSnapshot.Contains(goId))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var nameplateObject = namePlateAddon->NamePlateObjectArray[nameplateIndex];
|
|
||||||
var root = nameplateObject.RootComponentNode;
|
|
||||||
var nameContainer = nameplateObject.NameContainer;
|
|
||||||
var nameText = nameplateObject.NameText;
|
|
||||||
var marker = nameplateObject.MarkerIcon;
|
|
||||||
|
|
||||||
if (root == null || root->Component == null || nameContainer == null || nameText == null)
|
|
||||||
{
|
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
|
||||||
_logger.LogDebug("Nameplate {Index} missing required nodes during update, skipping.", nameplateIndex);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var nodeId = GetNameplateNodeId(nameplateIndex);
|
|
||||||
var pNode = EnsureNameplateTextNode(nameContainer, root, nodeId, out var nodeCreated);
|
|
||||||
if (pNode == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
bool isVisible =
|
|
||||||
((marker != null) && marker->AtkResNode.IsVisible()) ||
|
|
||||||
(nameContainer->IsVisible() && nameText->AtkResNode.IsVisible()) ||
|
|
||||||
config.LightfinderLabelShowHidden;
|
|
||||||
|
|
||||||
if (!isVisible)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!pNode->AtkResNode.IsVisible())
|
|
||||||
pNode->AtkResNode.ToggleVisibility(enable: true);
|
|
||||||
visibleIndices[nameplateIndex] = true;
|
|
||||||
|
|
||||||
if (nodeCreated)
|
|
||||||
pNode->AtkResNode.SetUseDepthBasedPriority(enable: true);
|
|
||||||
|
|
||||||
var scaleMatches = NearlyEqual(pNode->AtkResNode.ScaleX, effectiveScale) &&
|
|
||||||
NearlyEqual(pNode->AtkResNode.ScaleY, effectiveScale);
|
|
||||||
if (!scaleMatches)
|
|
||||||
pNode->AtkResNode.SetScale(effectiveScale, effectiveScale);
|
|
||||||
|
|
||||||
var fontTypeChanged = pNode->FontType != desiredFontType;
|
|
||||||
if (fontTypeChanged)
|
|
||||||
pNode->FontType = desiredFontType;
|
|
||||||
|
|
||||||
var fontSizeChanged = pNode->FontSize != desiredFontSize;
|
|
||||||
if (fontSizeChanged)
|
|
||||||
pNode->FontSize = desiredFontSize;
|
|
||||||
|
|
||||||
var needsTextUpdate = nodeCreated ||
|
|
||||||
!string.Equals(_lastLabelByIndex[nameplateIndex], labelContent, StringComparison.Ordinal);
|
|
||||||
if (needsTextUpdate)
|
|
||||||
{
|
|
||||||
pNode->SetText(labelContent);
|
|
||||||
_lastLabelByIndex[nameplateIndex] = labelContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
var flagsChanged = pNode->TextFlags != desiredFlags;
|
|
||||||
var nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
|
||||||
if (nodeWidth <= 0)
|
|
||||||
nodeWidth = defaultNodeWidth;
|
|
||||||
var nodeHeight = defaultNodeHeight;
|
|
||||||
AlignmentType alignment;
|
|
||||||
|
|
||||||
var textScaleY = nameText->AtkResNode.ScaleY;
|
|
||||||
if (textScaleY <= 0f)
|
|
||||||
textScaleY = 1f;
|
|
||||||
|
|
||||||
var blockHeight = Math.Abs((int)nameplateObject.TextH);
|
|
||||||
if (blockHeight > 0)
|
|
||||||
{
|
|
||||||
_cachedNameplateTextHeights[nameplateIndex] = blockHeight;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
blockHeight = _cachedNameplateTextHeights[nameplateIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (blockHeight <= 0)
|
|
||||||
{
|
|
||||||
blockHeight = GetScaledTextHeight(nameText);
|
|
||||||
if (blockHeight <= 0)
|
|
||||||
blockHeight = nodeHeight;
|
|
||||||
|
|
||||||
_cachedNameplateTextHeights[nameplateIndex] = blockHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
var containerHeight = (int)nameContainer->Height;
|
|
||||||
if (containerHeight > 0)
|
|
||||||
{
|
|
||||||
_cachedNameplateContainerHeights[nameplateIndex] = containerHeight;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
containerHeight = _cachedNameplateContainerHeights[nameplateIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (containerHeight <= 0)
|
|
||||||
{
|
|
||||||
containerHeight = blockHeight + (int)Math.Round(8 * textScaleY);
|
|
||||||
if (containerHeight <= blockHeight)
|
|
||||||
containerHeight = blockHeight + 1;
|
|
||||||
|
|
||||||
_cachedNameplateContainerHeights[nameplateIndex] = containerHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
var blockTop = containerHeight - blockHeight;
|
|
||||||
if (blockTop < 0)
|
|
||||||
blockTop = 0;
|
|
||||||
var verticalPadding = (int)Math.Round(4 * effectiveScale);
|
|
||||||
|
|
||||||
var positionY = blockTop - verticalPadding - nodeHeight;
|
|
||||||
|
|
||||||
var textWidth = Math.Abs((int)nameplateObject.TextW);
|
|
||||||
if (textWidth <= 0)
|
|
||||||
{
|
|
||||||
textWidth = GetScaledTextWidth(nameText);
|
|
||||||
if (textWidth <= 0)
|
|
||||||
textWidth = nodeWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textWidth > 0)
|
|
||||||
{
|
|
||||||
_cachedNameplateTextWidths[nameplateIndex] = textWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
var textOffset = (int)Math.Round(nameText->AtkResNode.X);
|
|
||||||
var hasValidOffset = false;
|
|
||||||
|
|
||||||
if (Math.Abs((int)nameplateObject.TextW) > 0 || textOffset != 0)
|
|
||||||
{
|
|
||||||
_cachedNameplateTextOffsets[nameplateIndex] = textOffset;
|
|
||||||
hasValidOffset = true;
|
|
||||||
}
|
|
||||||
else if (_cachedNameplateTextOffsets[nameplateIndex] != int.MinValue)
|
|
||||||
{
|
|
||||||
hasValidOffset = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
int positionX;
|
|
||||||
|
|
||||||
if (!config.LightfinderLabelUseIcon)
|
|
||||||
{
|
|
||||||
var needsWidthRefresh = nodeCreated || needsTextUpdate || !scaleMatches || fontTypeChanged || fontSizeChanged || flagsChanged;
|
|
||||||
if (flagsChanged)
|
|
||||||
pNode->TextFlags = desiredFlags;
|
|
||||||
|
|
||||||
if (needsWidthRefresh)
|
|
||||||
{
|
|
||||||
if (pNode->AtkResNode.Width != 0)
|
|
||||||
pNode->AtkResNode.Width = 0;
|
|
||||||
nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
|
||||||
if (nodeWidth <= 0)
|
|
||||||
nodeWidth = defaultNodeWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pNode->AtkResNode.Width != (ushort)nodeWidth)
|
|
||||||
pNode->AtkResNode.Width = (ushort)nodeWidth;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var needsWidthRefresh = nodeCreated || needsTextUpdate || !scaleMatches || fontTypeChanged || fontSizeChanged || flagsChanged;
|
|
||||||
if (flagsChanged)
|
|
||||||
pNode->TextFlags = desiredFlags;
|
|
||||||
|
|
||||||
if (needsWidthRefresh && pNode->AtkResNode.Width != 0)
|
|
||||||
pNode->AtkResNode.Width = 0;
|
|
||||||
nodeWidth = pNode->AtkResNode.GetWidth();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.LightfinderAutoAlign && nameContainer != null && hasValidOffset)
|
|
||||||
{
|
|
||||||
var nameplateWidth = (int)nameContainer->Width;
|
|
||||||
|
|
||||||
int leftPos = nameplateWidth / 8;
|
|
||||||
int rightPos = nameplateWidth - nodeWidth - (nameplateWidth / 8);
|
|
||||||
int centrePos = (nameplateWidth - nodeWidth) / 2;
|
|
||||||
int staticMargin = 24;
|
|
||||||
int calcMargin = (int)(nameplateWidth * 0.08f);
|
|
||||||
|
|
||||||
switch (config.LabelAlignment)
|
|
||||||
{
|
|
||||||
case LabelAlignment.Left:
|
|
||||||
positionX = config.LightfinderLabelUseIcon ? leftPos + staticMargin : leftPos;
|
|
||||||
alignment = AlignmentType.BottomLeft;
|
|
||||||
break;
|
|
||||||
case LabelAlignment.Right:
|
|
||||||
positionX = config.LightfinderLabelUseIcon ? rightPos - staticMargin : nameplateWidth - nodeWidth + calcMargin;
|
|
||||||
alignment = AlignmentType.BottomRight;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
positionX = config.LightfinderLabelUseIcon ? centrePos : centrePos + calcMargin;
|
|
||||||
alignment = AlignmentType.Bottom;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
positionX = 58 + config.LightfinderLabelOffsetX;
|
|
||||||
alignment = AlignmentType.Bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
positionY += config.LightfinderLabelOffsetY;
|
|
||||||
|
|
||||||
alignment = (AlignmentType)Math.Clamp((int)alignment, 0, 8);
|
|
||||||
if (pNode->AtkResNode.Color.A != 255)
|
|
||||||
pNode->AtkResNode.Color.A = 255;
|
|
||||||
|
|
||||||
var textR = (byte)(labelColor.X * 255);
|
|
||||||
var textG = (byte)(labelColor.Y * 255);
|
|
||||||
var textB = (byte)(labelColor.Z * 255);
|
|
||||||
var textA = (byte)(labelColor.W * 255);
|
|
||||||
|
|
||||||
if (pNode->TextColor.R != textR || pNode->TextColor.G != textG ||
|
|
||||||
pNode->TextColor.B != textB || pNode->TextColor.A != textA)
|
|
||||||
{
|
|
||||||
pNode->TextColor.R = textR;
|
|
||||||
pNode->TextColor.G = textG;
|
|
||||||
pNode->TextColor.B = textB;
|
|
||||||
pNode->TextColor.A = textA;
|
|
||||||
}
|
|
||||||
|
|
||||||
var edgeR = (byte)(edgeColor.X * 255);
|
|
||||||
var edgeG = (byte)(edgeColor.Y * 255);
|
|
||||||
var edgeB = (byte)(edgeColor.Z * 255);
|
|
||||||
var edgeA = (byte)(edgeColor.W * 255);
|
|
||||||
|
|
||||||
if (pNode->EdgeColor.R != edgeR || pNode->EdgeColor.G != edgeG ||
|
|
||||||
pNode->EdgeColor.B != edgeB || pNode->EdgeColor.A != edgeA)
|
|
||||||
{
|
|
||||||
pNode->EdgeColor.R = edgeR;
|
|
||||||
pNode->EdgeColor.G = edgeG;
|
|
||||||
pNode->EdgeColor.B = edgeB;
|
|
||||||
pNode->EdgeColor.A = edgeA;
|
|
||||||
}
|
|
||||||
|
|
||||||
var desiredAlignment = config.LightfinderLabelUseIcon ? alignment : AlignmentType.Bottom;
|
|
||||||
if (pNode->AlignmentType != desiredAlignment)
|
|
||||||
pNode->AlignmentType = desiredAlignment;
|
|
||||||
|
|
||||||
var desiredX = (short)Math.Clamp(positionX, short.MinValue, short.MaxValue);
|
|
||||||
var desiredY = (short)Math.Clamp(positionY, short.MinValue, short.MaxValue);
|
|
||||||
if (!NearlyEqual(pNode->AtkResNode.X, desiredX) || !NearlyEqual(pNode->AtkResNode.Y, desiredY))
|
|
||||||
pNode->AtkResNode.SetPositionShort(desiredX, desiredY);
|
|
||||||
|
|
||||||
if (pNode->LineSpacing != desiredLineSpacing)
|
|
||||||
pNode->LineSpacing = desiredLineSpacing;
|
|
||||||
if (pNode->CharSpacing != 1)
|
|
||||||
pNode->CharSpacing = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
HideUnmarkedNodes(namePlateAddon, visibleIndices);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Resolve the current RaptureAtkModule for native UI access.
|
|
||||||
/// </summary>
|
|
||||||
private static RaptureAtkModule* GetRaptureAtkModule()
|
|
||||||
{
|
|
||||||
var framework = Framework.Instance();
|
|
||||||
if (framework == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var uiModule = framework->GetUIModule();
|
|
||||||
if (uiModule == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return uiModule->GetRaptureAtkModule();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Resolve the NamePlate addon from the given RaptureAtkModule.
|
|
||||||
/// </summary>
|
|
||||||
private static AddonNamePlate* GetNamePlateAddon(RaptureAtkModule* raptureAtkModule)
|
|
||||||
{
|
|
||||||
if (raptureAtkModule == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var addon = raptureAtkModule->RaptureAtkUnitManager.GetAddonByName("NamePlate");
|
|
||||||
return addon != null ? (AddonNamePlate*)addon : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static uint GetNameplateNodeId(int index)
|
|
||||||
=> NameplateNodeIdBase + (uint)index;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks if the NamePlate addon is visible and safe to touch.
|
|
||||||
/// </summary>
|
|
||||||
private static bool IsNameplateAddonVisible(AddonNamePlate* namePlateAddon)
|
|
||||||
{
|
|
||||||
if (namePlateAddon == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var root = namePlateAddon->AtkUnitBase.RootNode;
|
|
||||||
return root != null && root->IsVisible();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Finds a LightFinder text node by ID in the name container.
|
|
||||||
/// </summary>
|
|
||||||
private static AtkTextNode* FindNameplateTextNode(AtkResNode* nameContainer, uint nodeId)
|
|
||||||
{
|
|
||||||
if (nameContainer == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var child = nameContainer->ChildNode;
|
|
||||||
while (child != null)
|
|
||||||
{
|
|
||||||
if (child->NodeId == nodeId &&
|
|
||||||
child->Type == NodeType.Text &&
|
|
||||||
child->ParentNode == nameContainer)
|
|
||||||
return (AtkTextNode*)child;
|
|
||||||
|
|
||||||
child = child->PrevSiblingNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ensures a LightFinder text node exists for the given nameplate index.
|
|
||||||
/// </summary>
|
|
||||||
private static AtkTextNode* EnsureNameplateTextNode(AtkResNode* nameContainer, AtkComponentNode* root, uint nodeId, out bool created)
|
|
||||||
{
|
|
||||||
created = false;
|
|
||||||
if (nameContainer == null || root == null || root->Component == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var existing = FindNameplateTextNode(nameContainer, nodeId);
|
|
||||||
if (existing != null)
|
|
||||||
return existing;
|
|
||||||
|
|
||||||
if (nameContainer->ChildNode == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var newNode = AtkNodeHelpers.CreateOrphanTextNode(nodeId, TextFlags.Edge | TextFlags.Glare);
|
|
||||||
if (newNode == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var lastChild = nameContainer->ChildNode;
|
|
||||||
while (lastChild->PrevSiblingNode != null)
|
|
||||||
lastChild = lastChild->PrevSiblingNode;
|
|
||||||
|
|
||||||
newNode->AtkResNode.NextSiblingNode = lastChild;
|
|
||||||
newNode->AtkResNode.ParentNode = nameContainer;
|
|
||||||
lastChild->PrevSiblingNode = (AtkResNode*)newNode;
|
|
||||||
root->Component->UldManager.UpdateDrawNodeList();
|
|
||||||
newNode->AtkResNode.SetUseDepthBasedPriority(true);
|
|
||||||
|
|
||||||
created = true;
|
|
||||||
return newNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Hides all native LightFinder nodes on the nameplate addon.
|
|
||||||
/// </summary>
|
|
||||||
private static void HideAllNameplateNodes(AddonNamePlate* namePlateAddon)
|
|
||||||
{
|
|
||||||
if (namePlateAddon == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!IsNameplateAddonVisible(namePlateAddon))
|
|
||||||
return;
|
|
||||||
|
|
||||||
for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i)
|
|
||||||
{
|
|
||||||
HideNameplateTextNode(namePlateAddon->NamePlateObjectArray[i], GetNameplateNodeId(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Hides all LightFinder nodes not marked as visible this frame.
|
|
||||||
/// </summary>
|
|
||||||
private static void HideUnmarkedNodes(AddonNamePlate* namePlateAddon, bool[] visibleIndices)
|
|
||||||
{
|
|
||||||
if (namePlateAddon == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!IsNameplateAddonVisible(namePlateAddon))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var visibleLength = visibleIndices.Length;
|
|
||||||
for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i)
|
|
||||||
{
|
|
||||||
if (i < visibleLength && visibleIndices[i])
|
|
||||||
continue;
|
|
||||||
|
|
||||||
HideNameplateTextNode(namePlateAddon->NamePlateObjectArray[i], GetNameplateNodeId(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Hides the LightFinder text node for a single nameplate object.
|
|
||||||
/// </summary>
|
|
||||||
private static void HideNameplateTextNode(AddonNamePlate.NamePlateObject nameplateObject, uint nodeId)
|
|
||||||
{
|
|
||||||
var nameContainer = nameplateObject.NameContainer;
|
|
||||||
if (nameContainer == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var node = FindNameplateTextNode(nameContainer, nodeId);
|
|
||||||
if (!IsValidNameplateTextNode(node, nameContainer))
|
|
||||||
return;
|
|
||||||
|
|
||||||
node->AtkResNode.ToggleVisibility(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Attempts to destroy all constructed LightFinder nodes safely.
|
|
||||||
/// </summary>
|
|
||||||
private void TryDestroyNameplateNodes()
|
|
||||||
{
|
|
||||||
var raptureAtkModule = GetRaptureAtkModule();
|
|
||||||
if (raptureAtkModule == null)
|
|
||||||
{
|
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
|
||||||
_logger.LogDebug("Unable to destroy nameplate nodes because the RaptureAtkModule is not available.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var namePlateAddon = GetNamePlateAddon(raptureAtkModule);
|
|
||||||
if (namePlateAddon == null)
|
|
||||||
{
|
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
|
||||||
_logger.LogDebug("Unable to destroy nameplate nodes because the NamePlate addon is not available.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DestroyNameplateNodes(namePlateAddon);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes all constructed LightFinder nodes from the given nameplate addon.
|
|
||||||
/// </summary>
|
|
||||||
private void DestroyNameplateNodes(AddonNamePlate* namePlateAddon)
|
|
||||||
{
|
|
||||||
if (namePlateAddon == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i)
|
|
||||||
{
|
|
||||||
var nameplateObject = namePlateAddon->NamePlateObjectArray[i];
|
|
||||||
var root = nameplateObject.RootComponentNode;
|
|
||||||
var nameContainer = nameplateObject.NameContainer;
|
|
||||||
if (root == null || root->Component == null || nameContainer == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var nodeId = GetNameplateNodeId(i);
|
|
||||||
var textNode = FindNameplateTextNode(nameContainer, nodeId);
|
|
||||||
if (!IsValidNameplateTextNode(textNode, nameContainer))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var resNode = &textNode->AtkResNode;
|
|
||||||
|
|
||||||
if (resNode->PrevSiblingNode != null)
|
|
||||||
resNode->PrevSiblingNode->NextSiblingNode = resNode->NextSiblingNode;
|
|
||||||
if (resNode->NextSiblingNode != null)
|
|
||||||
resNode->NextSiblingNode->PrevSiblingNode = resNode->PrevSiblingNode;
|
|
||||||
|
|
||||||
root->Component->UldManager.UpdateDrawNodeList();
|
|
||||||
resNode->Destroy(true);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_logger.LogError(e, "Unknown error while removing text node 0x{Node:X} for nameplate {Index} on component node 0x{Component:X}", (IntPtr)textNode, i, (IntPtr)root);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ClearNameplateCaches();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates that a node is a LightFinder text node owned by the container.
|
|
||||||
/// </summary>
|
|
||||||
private static bool IsValidNameplateTextNode(AtkTextNode* node, AtkResNode* nameContainer)
|
|
||||||
{
|
|
||||||
if (node == null || nameContainer == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var resNode = &node->AtkResNode;
|
|
||||||
return resNode->Type == NodeType.Text && resNode->ParentNode == nameContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Float comparison helper for UI values.
|
|
||||||
/// </summary>
|
|
||||||
private static bool NearlyEqual(float a, float b, float epsilon = 0.001f)
|
|
||||||
=> Math.Abs(a - b) <= epsilon;
|
|
||||||
|
|
||||||
private static int GetScaledTextHeight(AtkTextNode* node)
|
|
||||||
{
|
|
||||||
if (node == null)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
var resNode = &node->AtkResNode;
|
|
||||||
var rawHeight = (int)resNode->GetHeight();
|
|
||||||
if (rawHeight <= 0 && node->LineSpacing > 0)
|
|
||||||
rawHeight = node->LineSpacing;
|
|
||||||
if (rawHeight <= 0)
|
|
||||||
rawHeight = AtkNodeHelpers.DefaultTextNodeHeight;
|
|
||||||
|
|
||||||
var scale = resNode->ScaleY;
|
|
||||||
if (scale <= 0f)
|
|
||||||
scale = 1f;
|
|
||||||
|
|
||||||
var computed = (int)Math.Round(rawHeight * scale);
|
|
||||||
return Math.Max(1, computed);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int GetScaledTextWidth(AtkTextNode* node)
|
|
||||||
{
|
|
||||||
if (node == null)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
var resNode = &node->AtkResNode;
|
|
||||||
var rawWidth = (int)resNode->GetWidth();
|
|
||||||
if (rawWidth <= 0)
|
|
||||||
rawWidth = AtkNodeHelpers.DefaultTextNodeWidth;
|
|
||||||
|
|
||||||
var scale = resNode->ScaleX;
|
|
||||||
if (scale <= 0f)
|
|
||||||
scale = 1f;
|
|
||||||
|
|
||||||
var computed = (int)Math.Round(rawWidth * scale);
|
|
||||||
return Math.Max(1, computed);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clears cached text sizing and label state for nameplates.
|
|
||||||
/// </summary>
|
|
||||||
public void ClearNameplateCaches()
|
|
||||||
{
|
|
||||||
Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length);
|
|
||||||
Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length);
|
|
||||||
Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length);
|
|
||||||
Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
|
||||||
Array.Fill(_lastLabelByIndex, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using LightlessSync.API.Dto.User;
|
using LightlessSync.API.Dto.User;
|
||||||
using LightlessSync.Services.ActorTracking;
|
using LightlessSync.Services.ActorTracking;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
@@ -15,15 +15,11 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private readonly LightFinderService _broadcastService;
|
private readonly LightFinderService _broadcastService;
|
||||||
private readonly LightFinderPlateHandler _lightFinderPlateHandler;
|
private readonly LightFinderPlateHandler _lightFinderPlateHandler;
|
||||||
private readonly LightFinderNativePlateHandler _lightFinderNativePlateHandler;
|
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, BroadcastEntry> _broadcastCache = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, BroadcastEntry> _broadcastCache = new(StringComparer.Ordinal);
|
||||||
private readonly Queue<string> _lookupQueue = new();
|
private readonly Queue<string> _lookupQueue = new();
|
||||||
private readonly HashSet<string> _lookupQueuedCids = [];
|
private readonly HashSet<string> _lookupQueuedCids = [];
|
||||||
private readonly HashSet<string> _syncshellCids = [];
|
private readonly HashSet<string> _syncshellCids = [];
|
||||||
private volatile bool _pendingLocalBroadcast;
|
|
||||||
private TimeSpan? _pendingLocalTtl;
|
|
||||||
private string? _pendingLocalGid;
|
|
||||||
|
|
||||||
private static readonly TimeSpan _maxAllowedTtl = TimeSpan.FromMinutes(4);
|
private static readonly TimeSpan _maxAllowedTtl = TimeSpan.FromMinutes(4);
|
||||||
private static readonly TimeSpan _retryDelay = TimeSpan.FromMinutes(1);
|
private static readonly TimeSpan _retryDelay = TimeSpan.FromMinutes(1);
|
||||||
@@ -37,7 +33,6 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
private const int _maxQueueSize = 100;
|
private const int _maxQueueSize = 100;
|
||||||
|
|
||||||
private volatile bool _batchRunning = false;
|
private volatile bool _batchRunning = false;
|
||||||
private volatile bool _disposed = false;
|
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, BroadcastEntry> BroadcastCache => _broadcastCache;
|
public IReadOnlyDictionary<string, BroadcastEntry> BroadcastCache => _broadcastCache;
|
||||||
public readonly record struct BroadcastEntry(bool IsBroadcasting, DateTime ExpiryTime, string? GID);
|
public readonly record struct BroadcastEntry(bool IsBroadcasting, DateTime ExpiryTime, string? GID);
|
||||||
@@ -47,14 +42,12 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
LightFinderService broadcastService,
|
LightFinderService broadcastService,
|
||||||
LightlessMediator mediator,
|
LightlessMediator mediator,
|
||||||
LightFinderPlateHandler lightFinderPlateHandler,
|
LightFinderPlateHandler lightFinderPlateHandler,
|
||||||
LightFinderNativePlateHandler lightFinderNativePlateHandler,
|
|
||||||
ActorObjectService actorTracker) : base(logger, mediator)
|
ActorObjectService actorTracker) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_actorTracker = actorTracker;
|
_actorTracker = actorTracker;
|
||||||
_broadcastService = broadcastService;
|
_broadcastService = broadcastService;
|
||||||
_lightFinderPlateHandler = lightFinderPlateHandler;
|
_lightFinderPlateHandler = lightFinderPlateHandler;
|
||||||
_lightFinderNativePlateHandler = lightFinderNativePlateHandler;
|
|
||||||
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_framework = framework;
|
_framework = framework;
|
||||||
@@ -70,25 +63,20 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public void Update()
|
public void Update()
|
||||||
{
|
{
|
||||||
if (_disposed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_frameCounter++;
|
_frameCounter++;
|
||||||
var lookupsThisFrame = 0;
|
var lookupsThisFrame = 0;
|
||||||
|
|
||||||
if (!_broadcastService.IsBroadcasting)
|
if (!_broadcastService.IsBroadcasting)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
TryPrimeLocalBroadcastCache();
|
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
foreach (var descriptor in _actorTracker.PlayerDescriptors)
|
foreach (var address in _actorTracker.PlayerAddresses)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(descriptor.HashedContentId))
|
if (address == nint.Zero)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var cid = descriptor.HashedContentId;
|
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer(address);
|
||||||
var isStale = !_broadcastCache.TryGetValue(cid, out var entry) || entry.ExpiryTime <= now;
|
var isStale = !_broadcastCache.TryGetValue(cid, out var entry) || entry.ExpiryTime <= now;
|
||||||
|
|
||||||
if (isStale && _lookupQueuedCids.Add(cid) && _lookupQueue.Count < _maxQueueSize)
|
if (isStale && _lookupQueuedCids.Add(cid) && _lookupQueue.Count < _maxQueueSize)
|
||||||
@@ -116,14 +104,7 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private async Task BatchUpdateBroadcastCacheAsync(List<string> cids)
|
private async Task BatchUpdateBroadcastCacheAsync(List<string> cids)
|
||||||
{
|
{
|
||||||
if (_disposed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var results = await _broadcastService.AreUsersBroadcastingAsync(cids).ConfigureAwait(false);
|
var results = await _broadcastService.AreUsersBroadcastingAsync(cids).ConfigureAwait(false);
|
||||||
|
|
||||||
if (_disposed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
foreach (var (cid, info) in results)
|
foreach (var (cid, info) in results)
|
||||||
@@ -142,88 +123,35 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
(_, old) => new BroadcastEntry(info.IsBroadcasting, expiry, info.GID));
|
(_, old) => new BroadcastEntry(info.IsBroadcasting, expiry, info.GID));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_disposed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var activeCids = _broadcastCache
|
var activeCids = _broadcastCache
|
||||||
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now)
|
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now)
|
||||||
.Select(e => e.Key)
|
.Select(e => e.Key)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
_lightFinderPlateHandler.UpdateBroadcastingCids(activeCids);
|
_lightFinderPlateHandler.UpdateBroadcastingCids(activeCids);
|
||||||
_lightFinderNativePlateHandler.UpdateBroadcastingCids(activeCids);
|
|
||||||
UpdateSyncshellBroadcasts();
|
UpdateSyncshellBroadcasts();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnBroadcastStatusChanged(BroadcastStatusChangedMessage msg)
|
private void OnBroadcastStatusChanged(BroadcastStatusChangedMessage msg)
|
||||||
{
|
{
|
||||||
if (_disposed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!msg.Enabled)
|
if (!msg.Enabled)
|
||||||
{
|
{
|
||||||
_broadcastCache.Clear();
|
_broadcastCache.Clear();
|
||||||
_lookupQueue.Clear();
|
_lookupQueue.Clear();
|
||||||
_lookupQueuedCids.Clear();
|
_lookupQueuedCids.Clear();
|
||||||
_syncshellCids.Clear();
|
_syncshellCids.Clear();
|
||||||
_pendingLocalBroadcast = false;
|
|
||||||
_pendingLocalTtl = null;
|
|
||||||
|
|
||||||
_lightFinderPlateHandler.UpdateBroadcastingCids([]);
|
_lightFinderPlateHandler.UpdateBroadcastingCids([]);
|
||||||
_lightFinderNativePlateHandler.UpdateBroadcastingCids([]);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_pendingLocalBroadcast = true;
|
|
||||||
_pendingLocalTtl = msg.Ttl;
|
|
||||||
_pendingLocalGid = msg.Gid;
|
|
||||||
TryPrimeLocalBroadcastCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TryPrimeLocalBroadcastCache()
|
|
||||||
{
|
|
||||||
if (!_pendingLocalBroadcast)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!TryGetLocalHashedCid(out var localCid))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var ttl = _pendingLocalTtl ?? _maxAllowedTtl;
|
|
||||||
var expiry = DateTime.UtcNow + ttl;
|
|
||||||
|
|
||||||
_broadcastCache.AddOrUpdate(localCid,
|
|
||||||
new BroadcastEntry(true, expiry, _pendingLocalGid),
|
|
||||||
(_, old) => new BroadcastEntry(true, expiry, _pendingLocalGid ?? old.GID));
|
|
||||||
|
|
||||||
_pendingLocalBroadcast = false;
|
|
||||||
_pendingLocalTtl = null;
|
|
||||||
_pendingLocalGid = null;
|
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
var activeCids = _broadcastCache
|
|
||||||
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now)
|
|
||||||
.Select(e => e.Key)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
_lightFinderPlateHandler.UpdateBroadcastingCids(activeCids);
|
|
||||||
_lightFinderNativePlateHandler.UpdateBroadcastingCids(activeCids);
|
|
||||||
UpdateSyncshellBroadcasts();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateSyncshellBroadcasts()
|
private void UpdateSyncshellBroadcasts()
|
||||||
{
|
{
|
||||||
if (_disposed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var nearbyCids = GetNearbyHashedCids(out _);
|
var newSet = _broadcastCache
|
||||||
var newSet = nearbyCids.Count == 0
|
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
|
||||||
? new HashSet<string>(StringComparer.Ordinal)
|
.Select(e => e.Key)
|
||||||
: _broadcastCache
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
|
|
||||||
.Where(e => nearbyCids.Contains(e.Key))
|
|
||||||
.Select(e => e.Key)
|
|
||||||
.ToHashSet(StringComparer.Ordinal);
|
|
||||||
|
|
||||||
if (!_syncshellCids.SetEquals(newSet))
|
if (!_syncshellCids.SetEquals(newSet))
|
||||||
{
|
{
|
||||||
@@ -235,17 +163,12 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BroadcastStatusInfoDto> GetActiveSyncshellBroadcasts(bool excludeLocal = false)
|
public List<BroadcastStatusInfoDto> GetActiveSyncshellBroadcasts()
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var nearbyCids = GetNearbyHashedCids(out var localCid);
|
|
||||||
if (nearbyCids.Count == 0)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
return [.. _broadcastCache
|
return [.. _broadcastCache
|
||||||
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
|
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
|
||||||
.Where(e => nearbyCids.Contains(e.Key))
|
|
||||||
.Where(e => !excludeLocal || !string.Equals(e.Key, localCid, StringComparison.Ordinal))
|
|
||||||
.Select(e => new BroadcastStatusInfoDto
|
.Select(e => new BroadcastStatusInfoDto
|
||||||
{
|
{
|
||||||
HashedCID = e.Key,
|
HashedCID = e.Key,
|
||||||
@@ -255,47 +178,6 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
})];
|
})];
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGetLocalHashedCid(out string hashedCid)
|
|
||||||
{
|
|
||||||
hashedCid = string.Empty;
|
|
||||||
var descriptors = _actorTracker.PlayerDescriptors;
|
|
||||||
if (descriptors.Count == 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
foreach (var descriptor in descriptors)
|
|
||||||
{
|
|
||||||
if (!descriptor.IsLocalPlayer || string.IsNullOrWhiteSpace(descriptor.HashedContentId))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
hashedCid = descriptor.HashedContentId;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private HashSet<string> GetNearbyHashedCids(out string? localCid)
|
|
||||||
{
|
|
||||||
localCid = null;
|
|
||||||
var descriptors = _actorTracker.PlayerDescriptors;
|
|
||||||
if (descriptors.Count == 0)
|
|
||||||
return new HashSet<string>(StringComparer.Ordinal);
|
|
||||||
|
|
||||||
var set = new HashSet<string>(StringComparer.Ordinal);
|
|
||||||
foreach (var descriptor in descriptors)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(descriptor.HashedContentId))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (descriptor.IsLocalPlayer)
|
|
||||||
localCid = descriptor.HashedContentId;
|
|
||||||
|
|
||||||
set.Add(descriptor.HashedContentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return set;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ExpiredBroadcastCleanupLoop()
|
private async Task ExpiredBroadcastCleanupLoop()
|
||||||
{
|
{
|
||||||
var token = _cleanupCts.Token;
|
var token = _cleanupCts.Token;
|
||||||
@@ -348,35 +230,17 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
_disposed = true;
|
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
_framework.Update -= OnFrameworkUpdate;
|
_framework.Update -= OnFrameworkUpdate;
|
||||||
|
if (_cleanupTask != null)
|
||||||
try
|
|
||||||
{
|
{
|
||||||
_cleanupCts.Cancel();
|
_cleanupTask?.Wait(100, _cleanupCts.Token);
|
||||||
}
|
|
||||||
catch (ObjectDisposedException)
|
|
||||||
{
|
|
||||||
// Already disposed, can be ignored :)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
_cleanupCts.Cancel();
|
||||||
{
|
_cleanupCts.Dispose();
|
||||||
_cleanupTask?.Wait(100);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// Task may have already completed or been cancelled?
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
_cleanupTask?.Wait(100);
|
||||||
{
|
_cleanupCts.Dispose();
|
||||||
_cleanupCts.Dispose();
|
|
||||||
}
|
|
||||||
catch (ObjectDisposedException)
|
|
||||||
{
|
|
||||||
// Already disposed, ignore
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using LightlessSync.API.Dto.Group;
|
using LightlessSync.API.Dto.Group;
|
||||||
using LightlessSync.API.Dto.User;
|
using LightlessSync.API.Dto.User;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
@@ -67,7 +67,7 @@ public class LightFinderService : IHostedService, IMediatorSubscriber
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var cid = _dalamudUtil.GetCID();
|
var cid = await _dalamudUtil.GetCIDAsync().ConfigureAwait(false);
|
||||||
return cid.ToString().GetHash256();
|
return cid.ToString().GetHash256();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -121,10 +121,7 @@ public class LightFinderService : IHostedService, IMediatorSubscriber
|
|||||||
_waitingForTtlFetch = false;
|
_waitingForTtlFetch = false;
|
||||||
|
|
||||||
if (!wasEnabled || previousRemaining != validTtl)
|
if (!wasEnabled || previousRemaining != validTtl)
|
||||||
{
|
_mediator.Publish(new BroadcastStatusChangedMessage(true, validTtl));
|
||||||
var gid = _config.Current.SyncshellFinderEnabled ? _config.Current.SelectedFinderSyncshell : null;
|
|
||||||
_mediator.Publish(new BroadcastStatusChangedMessage(true, validTtl, gid));
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Lightfinder broadcast enabled ({Context}), TTL: {TTL}", context, validTtl);
|
_logger.LogInformation("Lightfinder broadcast enabled ({Context}), TTL: {TTL}", context, validTtl);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,276 +0,0 @@
|
|||||||
using Lifestream.Enums;
|
|
||||||
using LightlessSync.API.Data;
|
|
||||||
using LightlessSync.API.Dto.CharaData;
|
|
||||||
using LightlessSync.API.Dto.User;
|
|
||||||
using LightlessSync.Services.Mediator;
|
|
||||||
using LightlessSync.WebAPI;
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Primitives;
|
|
||||||
|
|
||||||
namespace LightlessSync.Services
|
|
||||||
{
|
|
||||||
public class LocationShareService : DisposableMediatorSubscriberBase
|
|
||||||
{
|
|
||||||
private readonly DalamudUtilService _dalamudUtilService;
|
|
||||||
private readonly ApiController _apiController;
|
|
||||||
private IMemoryCache _locations = new MemoryCache(new MemoryCacheOptions());
|
|
||||||
private IMemoryCache _sharingStatus = new MemoryCache(new MemoryCacheOptions());
|
|
||||||
private CancellationTokenSource _resetToken = new CancellationTokenSource();
|
|
||||||
|
|
||||||
public LocationShareService(ILogger<LocationShareService> logger, LightlessMediator mediator, DalamudUtilService dalamudUtilService, ApiController apiController) : base(logger, mediator)
|
|
||||||
{
|
|
||||||
_dalamudUtilService = dalamudUtilService;
|
|
||||||
_apiController = apiController;
|
|
||||||
|
|
||||||
|
|
||||||
Mediator.Subscribe<DisconnectedMessage>(this, (msg) =>
|
|
||||||
{
|
|
||||||
_resetToken.Cancel();
|
|
||||||
_resetToken.Dispose();
|
|
||||||
_resetToken = new CancellationTokenSource();
|
|
||||||
});
|
|
||||||
Mediator.Subscribe<ConnectedMessage>(this, (msg) =>
|
|
||||||
{
|
|
||||||
_ = _apiController.UpdateLocation(new LocationDto(new UserData(_apiController.UID, apiController.DisplayName), _dalamudUtilService.GetMapData()));
|
|
||||||
_ = RequestAllLocation();
|
|
||||||
} );
|
|
||||||
Mediator.Subscribe<LocationSharingMessage>(this, UpdateLocationList);
|
|
||||||
Mediator.Subscribe<MapChangedMessage>(this,
|
|
||||||
msg => _ = _apiController.UpdateLocation(new LocationDto(new UserData(_apiController.UID, _apiController.DisplayName), _dalamudUtilService.GetMapData())));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateLocationList(LocationSharingMessage msg)
|
|
||||||
{
|
|
||||||
if (_locations.TryGetValue(msg.User.UID, out _) && msg.LocationInfo.ServerId is 0)
|
|
||||||
{
|
|
||||||
_locations.Remove(msg.User.UID);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( msg.LocationInfo.ServerId is not 0 && msg.ExpireAt > DateTime.UtcNow)
|
|
||||||
{
|
|
||||||
AddLocationInfo(msg.User.UID, msg.LocationInfo, msg.ExpireAt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddLocationInfo(string uid, LocationInfo location, DateTimeOffset expireAt)
|
|
||||||
{
|
|
||||||
var options = new MemoryCacheEntryOptions()
|
|
||||||
.SetAbsoluteExpiration(expireAt)
|
|
||||||
.AddExpirationToken(new CancellationChangeToken(_resetToken.Token));
|
|
||||||
_locations.Set(uid, location, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RequestAllLocation()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var (data, status) = await _apiController.RequestAllLocationInfo().ConfigureAwait(false);
|
|
||||||
foreach (var dto in data)
|
|
||||||
{
|
|
||||||
AddLocationInfo(dto.LocationDto.User.UID, dto.LocationDto.Location, dto.ExpireAt);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var dto in status)
|
|
||||||
{
|
|
||||||
AddStatus(dto.User.UID, dto.ExpireAt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logger.LogError(e,"RequestAllLocation error : ");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddStatus(string uid, DateTimeOffset expireAt)
|
|
||||||
{
|
|
||||||
var options = new MemoryCacheEntryOptions()
|
|
||||||
.SetAbsoluteExpiration(expireAt)
|
|
||||||
.AddExpirationToken(new CancellationChangeToken(_resetToken.Token));
|
|
||||||
_sharingStatus.Set(uid, expireAt, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetUserLocation(string uid)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_locations.TryGetValue<LocationInfo>(uid, out var location))
|
|
||||||
{
|
|
||||||
return _dalamudUtilService.LocationToString(location);
|
|
||||||
}
|
|
||||||
return String.Empty;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logger.LogError(e,"GetUserLocation error : ");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocationInfo? GetLocationForLifestreamByUid(string uid)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_locations.TryGetValue<LocationInfo>(uid, out var location))
|
|
||||||
{
|
|
||||||
return location;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logger.LogError(e,"GetLocationInfoByUid error : ");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public AddressBookEntryTuple? GetAddressBookEntryByLocation(LocationInfo location)
|
|
||||||
{
|
|
||||||
if (location.ServerId is 0 || location.TerritoryId is 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var territoryHousing = (TerritoryTypeIdHousing)location.TerritoryId;
|
|
||||||
|
|
||||||
if (territoryHousing == TerritoryTypeIdHousing.None || !Enum.IsDefined(typeof(TerritoryTypeIdHousing), territoryHousing))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var city = GetResidentialAetheryteKind(territoryHousing);
|
|
||||||
|
|
||||||
if (city == ResidentialAetheryteKind.None)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (location.HouseId is not 0 and not 100)
|
|
||||||
{
|
|
||||||
AddressBookEntryTuple addressEntry = (
|
|
||||||
Name: "",
|
|
||||||
World: (int)location.ServerId,
|
|
||||||
City: (int)city,
|
|
||||||
Ward: (int)location.WardId,
|
|
||||||
PropertyType: 0,
|
|
||||||
Plot: (int)location.HouseId,
|
|
||||||
Apartment: 0,
|
|
||||||
ApartmentSubdivision: location.DivisionId == 2,
|
|
||||||
AliasEnabled: false,
|
|
||||||
Alias: ""
|
|
||||||
);
|
|
||||||
return addressEntry;
|
|
||||||
}
|
|
||||||
else if (location.HouseId is 100)
|
|
||||||
{
|
|
||||||
AddressBookEntryTuple addressEntry = (
|
|
||||||
Name: "",
|
|
||||||
World: (int)location.ServerId,
|
|
||||||
City: (int)city,
|
|
||||||
Ward: (int)location.WardId,
|
|
||||||
PropertyType: 1,
|
|
||||||
Plot: 0,
|
|
||||||
Apartment: (int)location.RoomId,
|
|
||||||
ApartmentSubdivision: location.DivisionId == 2,
|
|
||||||
AliasEnabled: false,
|
|
||||||
Alias: ""
|
|
||||||
);
|
|
||||||
return addressEntry;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ResidentialAetheryteKind GetResidentialAetheryteKind(TerritoryTypeIdHousing territoryHousing)
|
|
||||||
{
|
|
||||||
return territoryHousing switch
|
|
||||||
{
|
|
||||||
TerritoryTypeIdHousing.Shirogane or
|
|
||||||
TerritoryTypeIdHousing.ShiroganeApartment or
|
|
||||||
TerritoryTypeIdHousing.ShiroganeSmall or
|
|
||||||
TerritoryTypeIdHousing.ShiroganeMedium or
|
|
||||||
TerritoryTypeIdHousing.ShiroganeLarge or
|
|
||||||
TerritoryTypeIdHousing.ShiroganeFCRoom or
|
|
||||||
TerritoryTypeIdHousing.ShiroganeFCWorkshop
|
|
||||||
=> ResidentialAetheryteKind.Kugane,
|
|
||||||
|
|
||||||
TerritoryTypeIdHousing.Lavender or
|
|
||||||
TerritoryTypeIdHousing.LavenderSmall or
|
|
||||||
TerritoryTypeIdHousing.LavenderMedium or
|
|
||||||
TerritoryTypeIdHousing.LavenderLarge or
|
|
||||||
TerritoryTypeIdHousing.LavenderApartment or
|
|
||||||
TerritoryTypeIdHousing.LavenderFCRoom or
|
|
||||||
TerritoryTypeIdHousing.LavenderFCWorkshop
|
|
||||||
=> ResidentialAetheryteKind.Gridania,
|
|
||||||
|
|
||||||
TerritoryTypeIdHousing.Mist or
|
|
||||||
TerritoryTypeIdHousing.MistSmall or
|
|
||||||
TerritoryTypeIdHousing.MistMedium or
|
|
||||||
TerritoryTypeIdHousing.MistLarge or
|
|
||||||
TerritoryTypeIdHousing.MistApartment or
|
|
||||||
TerritoryTypeIdHousing.MistFCRoom or
|
|
||||||
TerritoryTypeIdHousing.MistFCWorkshop
|
|
||||||
=> ResidentialAetheryteKind.Limsa,
|
|
||||||
|
|
||||||
TerritoryTypeIdHousing.Goblet or
|
|
||||||
TerritoryTypeIdHousing.GobletSmall or
|
|
||||||
TerritoryTypeIdHousing.GobletMedium or
|
|
||||||
TerritoryTypeIdHousing.GobletLarge or
|
|
||||||
TerritoryTypeIdHousing.GobletApartment or
|
|
||||||
TerritoryTypeIdHousing.GobletFCRoom or
|
|
||||||
TerritoryTypeIdHousing.GobletFCWorkshop
|
|
||||||
=> ResidentialAetheryteKind.Uldah,
|
|
||||||
|
|
||||||
TerritoryTypeIdHousing.Empyream or
|
|
||||||
TerritoryTypeIdHousing.EmpyreamSmall or
|
|
||||||
TerritoryTypeIdHousing.EmpyreamMedium or
|
|
||||||
TerritoryTypeIdHousing.EmpyreamLarge or
|
|
||||||
TerritoryTypeIdHousing.EmpyreamApartment or
|
|
||||||
TerritoryTypeIdHousing.EmpyreamFCRoom or
|
|
||||||
TerritoryTypeIdHousing.EmpyreamFCWorkshop
|
|
||||||
=> ResidentialAetheryteKind.Foundation,
|
|
||||||
|
|
||||||
_ => ResidentialAetheryteKind.None
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public string? GetMapAddressByLocation(LocationInfo location)
|
|
||||||
{
|
|
||||||
string? liString = null;
|
|
||||||
var territoryHousing = (TerritoryTypeIdHousing)location.TerritoryId;
|
|
||||||
if (GetResidentialAetheryteKind(territoryHousing) == ResidentialAetheryteKind.None)
|
|
||||||
{
|
|
||||||
liString = _dalamudUtilService.LocationToLifestream(location);
|
|
||||||
}
|
|
||||||
return liString;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DateTimeOffset GetSharingStatus(string uid)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_sharingStatus.TryGetValue<DateTimeOffset>(uid, out var expireAt))
|
|
||||||
{
|
|
||||||
return expireAt;
|
|
||||||
}
|
|
||||||
return DateTimeOffset.MinValue;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logger.LogError(e,"GetSharingStatus error : ");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateSharingStatus(List<string> users, DateTimeOffset expireAt)
|
|
||||||
{
|
|
||||||
foreach (var user in users)
|
|
||||||
{
|
|
||||||
AddStatus(user, expireAt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -63,31 +63,23 @@ public sealed class LightlessMediator : IHostedService
|
|||||||
|
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
while (!_loopCts.Token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
while (!_loopCts.Token.IsCancellationRequested)
|
while (!_processQueue)
|
||||||
{
|
{
|
||||||
while (!_processQueue)
|
|
||||||
{
|
|
||||||
await Task.Delay(100, _loopCts.Token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(100, _loopCts.Token).ConfigureAwait(false);
|
await Task.Delay(100, _loopCts.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
HashSet<MessageBase> processedMessages = [];
|
|
||||||
while (_messageQueue.TryDequeue(out var message))
|
|
||||||
{
|
|
||||||
if (processedMessages.Contains(message)) { continue; }
|
|
||||||
|
|
||||||
processedMessages.Add(message);
|
|
||||||
|
|
||||||
ExecuteMessage(message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
await Task.Delay(100, _loopCts.Token).ConfigureAwait(false);
|
||||||
{
|
|
||||||
_logger.LogInformation("LightlessMediator stopped");
|
HashSet<MessageBase> processedMessages = [];
|
||||||
|
while (_messageQueue.TryDequeue(out var message))
|
||||||
|
{
|
||||||
|
if (processedMessages.Contains(message)) { continue; }
|
||||||
|
processedMessages.Add(message);
|
||||||
|
|
||||||
|
ExecuteMessage(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -21,12 +21,6 @@ public record SwitchToIntroUiMessage : MessageBase;
|
|||||||
public record SwitchToMainUiMessage : MessageBase;
|
public record SwitchToMainUiMessage : MessageBase;
|
||||||
public record OpenSettingsUiMessage : MessageBase;
|
public record OpenSettingsUiMessage : MessageBase;
|
||||||
public record OpenLightfinderSettingsMessage : MessageBase;
|
public record OpenLightfinderSettingsMessage : MessageBase;
|
||||||
public enum PerformanceSettingsSection
|
|
||||||
{
|
|
||||||
TextureOptimization,
|
|
||||||
ModelOptimization,
|
|
||||||
}
|
|
||||||
public record OpenPerformanceSettingsMessage(PerformanceSettingsSection Section) : MessageBase;
|
|
||||||
public record DalamudLoginMessage : MessageBase;
|
public record DalamudLoginMessage : MessageBase;
|
||||||
public record DalamudLogoutMessage : MessageBase;
|
public record DalamudLogoutMessage : MessageBase;
|
||||||
public record ActorTrackedMessage(ActorObjectService.ActorDescriptor Descriptor) : SameThreadMessage;
|
public record ActorTrackedMessage(ActorObjectService.ActorDescriptor Descriptor) : SameThreadMessage;
|
||||||
@@ -79,7 +73,7 @@ public record HubClosedMessage(Exception? Exception) : SameThreadMessage;
|
|||||||
public record ResumeScanMessage(string Source) : MessageBase;
|
public record ResumeScanMessage(string Source) : MessageBase;
|
||||||
public record FileCacheInitializedMessage : MessageBase;
|
public record FileCacheInitializedMessage : MessageBase;
|
||||||
public record DownloadReadyMessage(Guid RequestId) : MessageBase;
|
public record DownloadReadyMessage(Guid RequestId) : MessageBase;
|
||||||
public record DownloadStartedMessage(GameObjectHandler DownloadId, IReadOnlyDictionary<string, FileDownloadStatus> DownloadStatus) : MessageBase;
|
public record DownloadStartedMessage(GameObjectHandler DownloadId, Dictionary<string, FileDownloadStatus> DownloadStatus) : MessageBase;
|
||||||
public record DownloadFinishedMessage(GameObjectHandler DownloadId) : MessageBase;
|
public record DownloadFinishedMessage(GameObjectHandler DownloadId) : MessageBase;
|
||||||
public record UiToggleMessage(Type UiType) : MessageBase;
|
public record UiToggleMessage(Type UiType) : MessageBase;
|
||||||
public record PlayerUploadingMessage(GameObjectHandler Handler, bool IsUploading) : MessageBase;
|
public record PlayerUploadingMessage(GameObjectHandler Handler, bool IsUploading) : MessageBase;
|
||||||
@@ -110,7 +104,6 @@ public record PairUiUpdatedMessage(PairUiSnapshot Snapshot) : MessageBase;
|
|||||||
public record CensusUpdateMessage(byte Gender, byte RaceId, byte TribeId) : MessageBase;
|
public record CensusUpdateMessage(byte Gender, byte RaceId, byte TribeId) : MessageBase;
|
||||||
public record TargetPairMessage(Pair Pair) : MessageBase;
|
public record TargetPairMessage(Pair Pair) : MessageBase;
|
||||||
public record PairFocusCharacterMessage(Pair Pair) : SameThreadMessage;
|
public record PairFocusCharacterMessage(Pair Pair) : SameThreadMessage;
|
||||||
public record PairOnlineMessage(PairUniqueIdentifier PairIdent) : MessageBase;
|
|
||||||
public record CombatStartMessage : MessageBase;
|
public record CombatStartMessage : MessageBase;
|
||||||
public record CombatEndMessage : MessageBase;
|
public record CombatEndMessage : MessageBase;
|
||||||
public record PerformanceStartMessage : MessageBase;
|
public record PerformanceStartMessage : MessageBase;
|
||||||
@@ -130,7 +123,7 @@ public record GPoseLobbyReceivePoseData(UserData UserData, PoseData PoseData) :
|
|||||||
public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase;
|
public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase;
|
||||||
public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase;
|
public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase;
|
||||||
public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase;
|
public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase;
|
||||||
public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl, string? Gid = null) : MessageBase;
|
public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase;
|
||||||
public record UserLeftSyncshell(string gid) : MessageBase;
|
public record UserLeftSyncshell(string gid) : MessageBase;
|
||||||
public record UserJoinedSyncshell(string gid) : MessageBase;
|
public record UserJoinedSyncshell(string gid) : MessageBase;
|
||||||
public record SyncshellBroadcastsUpdatedMessage : MessageBase;
|
public record SyncshellBroadcastsUpdatedMessage : MessageBase;
|
||||||
@@ -142,7 +135,5 @@ public record ChatChannelsUpdated : MessageBase;
|
|||||||
public record ChatChannelMessageAdded(string ChannelKey, ChatMessageEntry Message) : MessageBase;
|
public record ChatChannelMessageAdded(string ChannelKey, ChatMessageEntry Message) : MessageBase;
|
||||||
public record GroupCollectionChangedMessage : MessageBase;
|
public record GroupCollectionChangedMessage : MessageBase;
|
||||||
public record OpenUserProfileMessage(UserData User) : MessageBase;
|
public record OpenUserProfileMessage(UserData User) : MessageBase;
|
||||||
public record LocationSharingMessage(UserData User, LocationInfo LocationInfo, DateTimeOffset ExpireAt) : MessageBase;
|
|
||||||
public record MapChangedMessage(uint MapId) : MessageBase;
|
|
||||||
#pragma warning restore S2094
|
#pragma warning restore S2094
|
||||||
#pragma warning restore MA0048 // File name must match type name
|
#pragma warning restore MA0048 // File name must match type name
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,132 +0,0 @@
|
|||||||
namespace LightlessSync.Services.ModelDecimation;
|
|
||||||
|
|
||||||
internal static class ModelDecimationFilters
|
|
||||||
{
|
|
||||||
// MODELS ONLY HERE, NOT MATERIALS
|
|
||||||
internal static readonly string[] HairPaths =
|
|
||||||
[
|
|
||||||
"/hair/",
|
|
||||||
"hir.mdl",
|
|
||||||
];
|
|
||||||
|
|
||||||
internal static readonly string[] ClothingPaths =
|
|
||||||
[
|
|
||||||
"chara/equipment/",
|
|
||||||
"/equipment/",
|
|
||||||
|
|
||||||
"met.mdl",
|
|
||||||
"top.mdl",
|
|
||||||
"glv.mdl",
|
|
||||||
"dwn.mdl",
|
|
||||||
"sho.mdl",
|
|
||||||
];
|
|
||||||
|
|
||||||
internal static readonly string[] AccessoryPaths =
|
|
||||||
[
|
|
||||||
"/accessory/",
|
|
||||||
"chara/accessory/",
|
|
||||||
|
|
||||||
"ear.mdl",
|
|
||||||
"nek.mdl",
|
|
||||||
"wrs.mdl",
|
|
||||||
"ril.mdl",
|
|
||||||
"rir.mdl",
|
|
||||||
];
|
|
||||||
|
|
||||||
internal static readonly string[] BodyPaths =
|
|
||||||
[
|
|
||||||
"/body/",
|
|
||||||
"chara/equipment/e0000/model/",
|
|
||||||
"chara/equipment/e9903/model/",
|
|
||||||
"chara/equipment/e9903/model/",
|
|
||||||
"chara/equipment/e0279/model/",
|
|
||||||
];
|
|
||||||
|
|
||||||
internal static readonly string[] FaceHeadPaths =
|
|
||||||
[
|
|
||||||
"/face/",
|
|
||||||
"/obj/face/",
|
|
||||||
"/head/",
|
|
||||||
"fac.mdl",
|
|
||||||
];
|
|
||||||
|
|
||||||
internal static readonly string[] TailOrEarPaths =
|
|
||||||
[
|
|
||||||
"/tail/",
|
|
||||||
"/obj/tail/",
|
|
||||||
"/zear/",
|
|
||||||
"/obj/zear/",
|
|
||||||
|
|
||||||
"til.mdl",
|
|
||||||
"zer.mdl",
|
|
||||||
];
|
|
||||||
|
|
||||||
// BODY MATERIALS ONLY, NOT MESHES
|
|
||||||
internal static readonly string[] BodyMaterials =
|
|
||||||
[
|
|
||||||
"b0001_bibo.mtrl",
|
|
||||||
"b0101_bibo.mtrl",
|
|
||||||
|
|
||||||
"b0001_a.mtrl",
|
|
||||||
"b0001_b.mtrl",
|
|
||||||
|
|
||||||
"b0101_a.mtrl",
|
|
||||||
"b0101_b.mtrl",
|
|
||||||
];
|
|
||||||
|
|
||||||
internal static string NormalizePath(string path)
|
|
||||||
=> path.Replace('\\', '/').ToLowerInvariant();
|
|
||||||
|
|
||||||
internal static bool IsHairPath(string normalizedPath)
|
|
||||||
=> ContainsAny(normalizedPath, HairPaths);
|
|
||||||
|
|
||||||
internal static bool IsClothingPath(string normalizedPath)
|
|
||||||
=> ContainsAny(normalizedPath, ClothingPaths);
|
|
||||||
|
|
||||||
internal static bool IsAccessoryPath(string normalizedPath)
|
|
||||||
=> ContainsAny(normalizedPath, AccessoryPaths);
|
|
||||||
|
|
||||||
|
|
||||||
internal static bool IsBodyPath(string normalizedPath)
|
|
||||||
=> ContainsAny(normalizedPath, BodyPaths);
|
|
||||||
|
|
||||||
internal static bool IsFaceHeadPath(string normalizedPath)
|
|
||||||
=> ContainsAny(normalizedPath, FaceHeadPaths);
|
|
||||||
|
|
||||||
internal static bool IsTailOrEarPath(string normalizedPath)
|
|
||||||
=> ContainsAny(normalizedPath, TailOrEarPaths);
|
|
||||||
|
|
||||||
internal static bool ContainsAny(string normalizedPath, IReadOnlyList<string> markers)
|
|
||||||
{
|
|
||||||
for (var i = 0; i < markers.Count; i++)
|
|
||||||
{
|
|
||||||
if (normalizedPath.Contains(markers[i], StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool IsBodyMaterial(string materialPath)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(materialPath))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var normalized = NormalizePath(materialPath);
|
|
||||||
var nameStart = normalized.LastIndexOf('/');
|
|
||||||
var fileName = nameStart >= 0 ? normalized[(nameStart + 1)..] : normalized;
|
|
||||||
foreach (var marker in BodyMaterials)
|
|
||||||
{
|
|
||||||
if (fileName.Contains(marker, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,526 +0,0 @@
|
|||||||
using LightlessSync.FileCache;
|
|
||||||
using LightlessSync.LightlessConfiguration;
|
|
||||||
using LightlessSync.LightlessConfiguration.Configurations;
|
|
||||||
using LightlessSync.Utils;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace LightlessSync.Services.ModelDecimation;
|
|
||||||
|
|
||||||
public sealed class ModelDecimationService
|
|
||||||
{
|
|
||||||
private const int MaxConcurrentJobs = 1;
|
|
||||||
private const double MinTargetRatio = 0.01;
|
|
||||||
private const double MaxTargetRatio = 0.99;
|
|
||||||
|
|
||||||
private readonly ILogger<ModelDecimationService> _logger;
|
|
||||||
private readonly LightlessConfigService _configService;
|
|
||||||
private readonly FileCacheManager _fileCacheManager;
|
|
||||||
private readonly PlayerPerformanceConfigService _performanceConfigService;
|
|
||||||
private readonly XivDataStorageService _xivDataStorageService;
|
|
||||||
private readonly SemaphoreSlim _decimationSemaphore = new(MaxConcurrentJobs);
|
|
||||||
|
|
||||||
private readonly TaskRegistry<string> _decimationDeduplicator = new();
|
|
||||||
private readonly ConcurrentDictionary<string, string> _decimatedPaths = new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
private readonly ConcurrentDictionary<string, byte> _failedHashes = new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
public ModelDecimationService(
|
|
||||||
ILogger<ModelDecimationService> logger,
|
|
||||||
LightlessConfigService configService,
|
|
||||||
FileCacheManager fileCacheManager,
|
|
||||||
PlayerPerformanceConfigService performanceConfigService,
|
|
||||||
XivDataStorageService xivDataStorageService)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_configService = configService;
|
|
||||||
_fileCacheManager = fileCacheManager;
|
|
||||||
_performanceConfigService = performanceConfigService;
|
|
||||||
_xivDataStorageService = xivDataStorageService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ScheduleDecimation(string hash, string filePath, string? gamePath = null)
|
|
||||||
{
|
|
||||||
if (!ShouldScheduleDecimation(hash, filePath, gamePath))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_decimatedPaths.ContainsKey(hash) || _failedHashes.ContainsKey(hash) || _decimationDeduplicator.TryGetExisting(hash, out _))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogDebug("Queued model decimation for {Hash}", hash);
|
|
||||||
|
|
||||||
_decimationDeduplicator.GetOrStart(hash, async () =>
|
|
||||||
{
|
|
||||||
await _decimationSemaphore.WaitAsync().ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await DecimateInternalAsync(hash, filePath).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_failedHashes[hash] = 1;
|
|
||||||
_logger.LogWarning(ex, "Model decimation failed for {Hash}", hash);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_decimationSemaphore.Release();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ScheduleBatchDecimation(string hash, string filePath, ModelDecimationSettings settings)
|
|
||||||
{
|
|
||||||
if (!ShouldScheduleBatchDecimation(hash, filePath, settings))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_decimationDeduplicator.TryGetExisting(hash, out _))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_failedHashes.TryRemove(hash, out _);
|
|
||||||
_decimatedPaths.TryRemove(hash, out _);
|
|
||||||
|
|
||||||
_logger.LogInformation("Queued batch model decimation for {Hash}", hash);
|
|
||||||
|
|
||||||
_decimationDeduplicator.GetOrStart(hash, async () =>
|
|
||||||
{
|
|
||||||
await _decimationSemaphore.WaitAsync().ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await DecimateInternalAsync(hash, filePath, settings, allowExisting: false, destinationOverride: filePath, registerDecimatedPath: false).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_failedHashes[hash] = 1;
|
|
||||||
_logger.LogWarning(ex, "Batch model decimation failed for {Hash}", hash);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_decimationSemaphore.Release();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ShouldScheduleDecimation(string hash, string filePath, string? gamePath = null)
|
|
||||||
{
|
|
||||||
var threshold = Math.Max(0, _performanceConfigService.Current.ModelDecimationTriangleThreshold);
|
|
||||||
return IsDecimationEnabled()
|
|
||||||
&& filePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& IsDecimationAllowed(gamePath)
|
|
||||||
&& !ShouldSkipByTriangleCache(hash, threshold);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetPreferredPath(string hash, string originalPath)
|
|
||||||
{
|
|
||||||
if (!IsDecimationEnabled())
|
|
||||||
{
|
|
||||||
return originalPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_decimatedPaths.TryGetValue(hash, out var existing) && File.Exists(existing))
|
|
||||||
{
|
|
||||||
return existing;
|
|
||||||
}
|
|
||||||
|
|
||||||
var resolved = GetExistingDecimatedPath(hash);
|
|
||||||
if (!string.IsNullOrEmpty(resolved))
|
|
||||||
{
|
|
||||||
_decimatedPaths[hash] = resolved;
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
return originalPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task WaitForPendingJobsAsync(IEnumerable<string>? hashes, CancellationToken token)
|
|
||||||
{
|
|
||||||
if (hashes is null)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
var pending = new List<Task>();
|
|
||||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
foreach (var hash in hashes)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(hash) || !seen.Add(hash))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_decimationDeduplicator.TryGetExisting(hash, out var job))
|
|
||||||
{
|
|
||||||
pending.Add(job);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pending.Count == 0)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.WhenAll(pending).WaitAsync(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task DecimateInternalAsync(string hash, string sourcePath)
|
|
||||||
{
|
|
||||||
if (!TryGetDecimationSettings(out var settings))
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Model decimation disabled or invalid settings for {Hash}", hash);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
return DecimateInternalAsync(hash, sourcePath, settings, allowExisting: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task DecimateInternalAsync(
|
|
||||||
string hash,
|
|
||||||
string sourcePath,
|
|
||||||
ModelDecimationSettings settings,
|
|
||||||
bool allowExisting,
|
|
||||||
string? destinationOverride = null,
|
|
||||||
bool registerDecimatedPath = true)
|
|
||||||
{
|
|
||||||
if (!File.Exists(sourcePath))
|
|
||||||
{
|
|
||||||
_failedHashes[hash] = 1;
|
|
||||||
_logger.LogWarning("Cannot decimate model {Hash}; source path missing: {Path}", hash, sourcePath);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryNormalizeSettings(settings, out var normalized))
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Model decimation skipped for {Hash}; invalid settings.", hash);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogDebug(
|
|
||||||
"Starting model decimation for {Hash} (threshold {Threshold}, ratio {Ratio:0.##}, normalize tangents {NormalizeTangents}, avoid body intersection {AvoidBodyIntersection})",
|
|
||||||
hash,
|
|
||||||
normalized.TriangleThreshold,
|
|
||||||
normalized.TargetRatio,
|
|
||||||
normalized.NormalizeTangents,
|
|
||||||
normalized.AvoidBodyIntersection);
|
|
||||||
|
|
||||||
var destination = destinationOverride ?? Path.Combine(GetDecimatedDirectory(), $"{hash}.mdl");
|
|
||||||
var inPlace = string.Equals(destination, sourcePath, StringComparison.OrdinalIgnoreCase);
|
|
||||||
if (!inPlace && File.Exists(destination))
|
|
||||||
{
|
|
||||||
if (allowExisting)
|
|
||||||
{
|
|
||||||
if (registerDecimatedPath)
|
|
||||||
{
|
|
||||||
RegisterDecimatedModel(hash, sourcePath, destination);
|
|
||||||
}
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
TryDelete(destination);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!MdlDecimator.TryDecimate(sourcePath, destination, normalized, _logger))
|
|
||||||
{
|
|
||||||
_failedHashes[hash] = 1;
|
|
||||||
_logger.LogDebug("Model decimation skipped for {Hash}", hash);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (registerDecimatedPath)
|
|
||||||
{
|
|
||||||
RegisterDecimatedModel(hash, sourcePath, destination);
|
|
||||||
}
|
|
||||||
_logger.LogDebug("Decimated model {Hash} -> {Path}", hash, destination);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RegisterDecimatedModel(string hash, string sourcePath, string destination)
|
|
||||||
{
|
|
||||||
_decimatedPaths[hash] = destination;
|
|
||||||
|
|
||||||
var performanceConfig = _performanceConfigService.Current;
|
|
||||||
if (performanceConfig.KeepOriginalModelFiles)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(sourcePath, destination, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryReplaceCacheEntryWithDecimated(hash, sourcePath, destination))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
TryDelete(sourcePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryReplaceCacheEntryWithDecimated(string hash, string sourcePath, string destination)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var cacheEntry = _fileCacheManager.GetFileCacheByHash(hash);
|
|
||||||
if (cacheEntry is null || !cacheEntry.IsCacheEntry)
|
|
||||||
{
|
|
||||||
return File.Exists(sourcePath) ? false : true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var cacheFolder = _configService.Current.CacheFolder;
|
|
||||||
if (string.IsNullOrEmpty(cacheFolder))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!destination.StartsWith(cacheFolder, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var info = new FileInfo(destination);
|
|
||||||
if (!info.Exists)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var relative = Path.GetRelativePath(cacheFolder, destination)
|
|
||||||
.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
|
|
||||||
var sanitizedRelative = relative.TrimStart(Path.DirectorySeparatorChar);
|
|
||||||
var prefixed = Path.Combine(FileCacheManager.CachePrefix, sanitizedRelative);
|
|
||||||
|
|
||||||
var replacement = new FileCacheEntity(
|
|
||||||
hash,
|
|
||||||
prefixed,
|
|
||||||
info.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture),
|
|
||||||
info.Length,
|
|
||||||
cacheEntry.CompressedSize);
|
|
||||||
replacement.SetResolvedFilePath(destination);
|
|
||||||
|
|
||||||
if (!string.Equals(cacheEntry.PrefixedFilePath, prefixed, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
_fileCacheManager.RemoveHashedFile(cacheEntry.Hash, cacheEntry.PrefixedFilePath, removeDerivedFiles: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
_fileCacheManager.UpdateHashedFile(replacement, computeProperties: false);
|
|
||||||
_fileCacheManager.WriteOutFullCsv();
|
|
||||||
|
|
||||||
_logger.LogTrace("Replaced cache entry for model {Hash} to decimated path {Path}", hash, destination);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogTrace(ex, "Failed to replace cache entry for model {Hash}", hash);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsDecimationEnabled()
|
|
||||||
=> _performanceConfigService.Current.EnableModelDecimation;
|
|
||||||
|
|
||||||
private bool ShouldSkipByTriangleCache(string hash, int triangleThreshold)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(hash))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_xivDataStorageService.Current.TriangleDictionary.TryGetValue(hash, out var cachedTris) || cachedTris <= 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var threshold = Math.Max(0, triangleThreshold);
|
|
||||||
return threshold > 0 && cachedTris < threshold;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsDecimationAllowed(string? gamePath)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(gamePath))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var normalized = ModelDecimationFilters.NormalizePath(gamePath);
|
|
||||||
if (ModelDecimationFilters.IsHairPath(normalized))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ModelDecimationFilters.IsClothingPath(normalized))
|
|
||||||
{
|
|
||||||
return _performanceConfigService.Current.ModelDecimationAllowClothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ModelDecimationFilters.IsAccessoryPath(normalized))
|
|
||||||
{
|
|
||||||
return _performanceConfigService.Current.ModelDecimationAllowAccessories;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ModelDecimationFilters.IsBodyPath(normalized))
|
|
||||||
{
|
|
||||||
return _performanceConfigService.Current.ModelDecimationAllowBody;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ModelDecimationFilters.IsFaceHeadPath(normalized))
|
|
||||||
{
|
|
||||||
return _performanceConfigService.Current.ModelDecimationAllowFaceHead;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ModelDecimationFilters.IsTailOrEarPath(normalized))
|
|
||||||
{
|
|
||||||
return _performanceConfigService.Current.ModelDecimationAllowTail;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryGetDecimationSettings(out ModelDecimationSettings settings)
|
|
||||||
{
|
|
||||||
settings = new ModelDecimationSettings(
|
|
||||||
ModelDecimationDefaults.TriangleThreshold,
|
|
||||||
ModelDecimationDefaults.TargetRatio,
|
|
||||||
ModelDecimationDefaults.NormalizeTangents,
|
|
||||||
ModelDecimationDefaults.AvoidBodyIntersection,
|
|
||||||
new ModelDecimationAdvancedSettings());
|
|
||||||
|
|
||||||
var config = _performanceConfigService.Current;
|
|
||||||
if (!config.EnableModelDecimation)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var advanced = NormalizeAdvancedSettings(config.ModelDecimationAdvanced);
|
|
||||||
settings = new ModelDecimationSettings(
|
|
||||||
Math.Max(0, config.ModelDecimationTriangleThreshold),
|
|
||||||
config.ModelDecimationTargetRatio,
|
|
||||||
config.ModelDecimationNormalizeTangents,
|
|
||||||
config.ModelDecimationAvoidBodyIntersection,
|
|
||||||
advanced);
|
|
||||||
|
|
||||||
return TryNormalizeSettings(settings, out settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryNormalizeSettings(ModelDecimationSettings settings, out ModelDecimationSettings normalized)
|
|
||||||
{
|
|
||||||
var ratio = settings.TargetRatio;
|
|
||||||
if (double.IsNaN(ratio) || double.IsInfinity(ratio))
|
|
||||||
{
|
|
||||||
normalized = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ratio = Math.Clamp(ratio, MinTargetRatio, MaxTargetRatio);
|
|
||||||
var advanced = NormalizeAdvancedSettings(settings.Advanced);
|
|
||||||
normalized = new ModelDecimationSettings(
|
|
||||||
Math.Max(0, settings.TriangleThreshold),
|
|
||||||
ratio,
|
|
||||||
settings.NormalizeTangents,
|
|
||||||
settings.AvoidBodyIntersection,
|
|
||||||
advanced);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ModelDecimationAdvancedSettings NormalizeAdvancedSettings(ModelDecimationAdvancedSettings? settings)
|
|
||||||
{
|
|
||||||
var source = settings ?? new ModelDecimationAdvancedSettings();
|
|
||||||
return new ModelDecimationAdvancedSettings
|
|
||||||
{
|
|
||||||
MinComponentTriangles = Math.Clamp(source.MinComponentTriangles, 0, 1000),
|
|
||||||
MaxCollapseEdgeLengthFactor = ClampFloat(source.MaxCollapseEdgeLengthFactor, 0.1f, 10f, ModelDecimationAdvancedSettings.DefaultMaxCollapseEdgeLengthFactor),
|
|
||||||
NormalSimilarityThresholdDegrees = ClampFloat(source.NormalSimilarityThresholdDegrees, 0f, 180f, ModelDecimationAdvancedSettings.DefaultNormalSimilarityThresholdDegrees),
|
|
||||||
BoneWeightSimilarityThreshold = ClampFloat(source.BoneWeightSimilarityThreshold, 0f, 1f, ModelDecimationAdvancedSettings.DefaultBoneWeightSimilarityThreshold),
|
|
||||||
UvSimilarityThreshold = ClampFloat(source.UvSimilarityThreshold, 0f, 1f, ModelDecimationAdvancedSettings.DefaultUvSimilarityThreshold),
|
|
||||||
UvSeamAngleCos = ClampFloat(source.UvSeamAngleCos, -1f, 1f, ModelDecimationAdvancedSettings.DefaultUvSeamAngleCos),
|
|
||||||
BlockUvSeamVertices = source.BlockUvSeamVertices,
|
|
||||||
AllowBoundaryCollapses = source.AllowBoundaryCollapses,
|
|
||||||
BodyCollisionDistanceFactor = ClampFloat(source.BodyCollisionDistanceFactor, 0f, 10f, ModelDecimationAdvancedSettings.DefaultBodyCollisionDistanceFactor),
|
|
||||||
BodyCollisionNoOpDistanceFactor = ClampFloat(source.BodyCollisionNoOpDistanceFactor, 0f, 10f, ModelDecimationAdvancedSettings.DefaultBodyCollisionNoOpDistanceFactor),
|
|
||||||
BodyCollisionAdaptiveRelaxFactor = ClampFloat(source.BodyCollisionAdaptiveRelaxFactor, 0f, 10f, ModelDecimationAdvancedSettings.DefaultBodyCollisionAdaptiveRelaxFactor),
|
|
||||||
BodyCollisionAdaptiveNearRatio = ClampFloat(source.BodyCollisionAdaptiveNearRatio, 0f, 1f, ModelDecimationAdvancedSettings.DefaultBodyCollisionAdaptiveNearRatio),
|
|
||||||
BodyCollisionAdaptiveUvThreshold = ClampFloat(source.BodyCollisionAdaptiveUvThreshold, 0f, 1f, ModelDecimationAdvancedSettings.DefaultBodyCollisionAdaptiveUvThreshold),
|
|
||||||
BodyCollisionNoOpUvSeamAngleCos = ClampFloat(source.BodyCollisionNoOpUvSeamAngleCos, -1f, 1f, ModelDecimationAdvancedSettings.DefaultBodyCollisionNoOpUvSeamAngleCos),
|
|
||||||
BodyCollisionProtectionFactor = ClampFloat(source.BodyCollisionProtectionFactor, 0f, 10f, ModelDecimationAdvancedSettings.DefaultBodyCollisionProtectionFactor),
|
|
||||||
BodyProxyTargetRatioMin = ClampFloat(source.BodyProxyTargetRatioMin, 0f, 1f, ModelDecimationAdvancedSettings.DefaultBodyProxyTargetRatioMin),
|
|
||||||
BodyCollisionProxyInflate = ClampFloat(source.BodyCollisionProxyInflate, 0f, 0.1f, ModelDecimationAdvancedSettings.DefaultBodyCollisionProxyInflate),
|
|
||||||
BodyCollisionPenetrationFactor = ClampFloat(source.BodyCollisionPenetrationFactor, 0f, 1f, ModelDecimationAdvancedSettings.DefaultBodyCollisionPenetrationFactor),
|
|
||||||
MinBodyCollisionDistance = ClampFloat(source.MinBodyCollisionDistance, 1e-6f, 1f, ModelDecimationAdvancedSettings.DefaultMinBodyCollisionDistance),
|
|
||||||
MinBodyCollisionCellSize = ClampFloat(source.MinBodyCollisionCellSize, 1e-6f, 1f, ModelDecimationAdvancedSettings.DefaultMinBodyCollisionCellSize),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static float ClampFloat(float value, float min, float max, float fallback)
|
|
||||||
{
|
|
||||||
if (float.IsNaN(value) || float.IsInfinity(value))
|
|
||||||
{
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.Clamp(value, min, max);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool ShouldScheduleBatchDecimation(string hash, string filePath, ModelDecimationSettings settings)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(filePath) || !filePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryNormalizeSettings(settings, out _))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string? GetExistingDecimatedPath(string hash)
|
|
||||||
{
|
|
||||||
var candidate = Path.Combine(GetDecimatedDirectory(), $"{hash}.mdl");
|
|
||||||
return File.Exists(candidate) ? candidate : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetDecimatedDirectory()
|
|
||||||
{
|
|
||||||
var directory = Path.Combine(_configService.Current.CacheFolder, "decimated");
|
|
||||||
if (!Directory.Exists(directory))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(directory);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogTrace(ex, "Failed to create decimated directory {Directory}", directory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return directory;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void TryDelete(string? path)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(path))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (File.Exists(path))
|
|
||||||
{
|
|
||||||
File.Delete(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using LightlessSync.LightlessConfiguration.Configurations;
|
|
||||||
|
|
||||||
namespace LightlessSync.Services.ModelDecimation;
|
|
||||||
|
|
||||||
public readonly record struct ModelDecimationSettings(
|
|
||||||
int TriangleThreshold,
|
|
||||||
double TargetRatio,
|
|
||||||
bool NormalizeTangents,
|
|
||||||
bool AvoidBodyIntersection,
|
|
||||||
ModelDecimationAdvancedSettings Advanced);
|
|
||||||
@@ -2,8 +2,10 @@ using Dalamud.Game.ClientState.Objects.Enums;
|
|||||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
using Dalamud.Game.NativeWrapper;
|
using Dalamud.Game.NativeWrapper;
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
|
using Dalamud.Hooking;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
|
using Dalamud.Utility.Signatures;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
@@ -22,22 +24,27 @@ namespace LightlessSync.Services;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public unsafe class NameplateService : DisposableMediatorSubscriberBase
|
public unsafe class NameplateService : DisposableMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
|
private delegate nint UpdateNameplateDelegate(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo, NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex);
|
||||||
|
|
||||||
|
// Glyceri, Thanks :bow:
|
||||||
|
[Signature("40 53 55 57 41 56 48 81 EC ?? ?? ?? ?? 48 8B 84 24", DetourName = nameof(UpdateNameplateDetour))]
|
||||||
|
private readonly Hook<UpdateNameplateDelegate>? _nameplateHook = null;
|
||||||
|
|
||||||
private readonly ILogger<NameplateService> _logger;
|
private readonly ILogger<NameplateService> _logger;
|
||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly IClientState _clientState;
|
private readonly IClientState _clientState;
|
||||||
private readonly IGameGui _gameGui;
|
private readonly IGameGui _gameGui;
|
||||||
private readonly IObjectTable _objectTable;
|
private readonly IObjectTable _objectTable;
|
||||||
private readonly PairUiService _pairUiService;
|
private readonly PairUiService _pairUiService;
|
||||||
private readonly NameplateUpdateHookService _nameplateUpdateHookService;
|
|
||||||
|
|
||||||
public NameplateService(ILogger<NameplateService> logger,
|
public NameplateService(ILogger<NameplateService> logger,
|
||||||
LightlessConfigService configService,
|
LightlessConfigService configService,
|
||||||
IClientState clientState,
|
IClientState clientState,
|
||||||
IGameGui gameGui,
|
IGameGui gameGui,
|
||||||
IObjectTable objectTable,
|
IObjectTable objectTable,
|
||||||
|
IGameInteropProvider interop,
|
||||||
LightlessMediator lightlessMediator,
|
LightlessMediator lightlessMediator,
|
||||||
PairUiService pairUiService,
|
PairUiService pairUiService) : base(logger, lightlessMediator)
|
||||||
NameplateUpdateHookService nameplateUpdateHookService) : base(logger, lightlessMediator)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
@@ -45,18 +52,21 @@ public unsafe class NameplateService : DisposableMediatorSubscriberBase
|
|||||||
_gameGui = gameGui;
|
_gameGui = gameGui;
|
||||||
_objectTable = objectTable;
|
_objectTable = objectTable;
|
||||||
_pairUiService = pairUiService;
|
_pairUiService = pairUiService;
|
||||||
_nameplateUpdateHookService = nameplateUpdateHookService;
|
|
||||||
|
|
||||||
_nameplateUpdateHookService.NameplateUpdated += OnNameplateUpdated;
|
interop.InitializeFromAttributes(this);
|
||||||
|
_nameplateHook?.Enable();
|
||||||
Refresh();
|
Refresh();
|
||||||
|
|
||||||
Mediator.Subscribe<VisibilityChange>(this, (_) => Refresh());
|
Mediator.Subscribe<VisibilityChange>(this, (_) => Refresh());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Nameplate update handler, triggered by the signature hook service.
|
/// Detour for the game's internal nameplate update function.
|
||||||
|
/// This will be called whenever the client updates any nameplate.
|
||||||
|
///
|
||||||
|
/// We hook into it to apply our own nameplate coloring logic via <see cref="SetNameplate"/>,
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void OnNameplateUpdated(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo, NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex)
|
private nint UpdateNameplateDetour(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo, NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -64,8 +74,10 @@ public unsafe class NameplateService : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.LogError(e, "Error in NameplateService OnNameplateUpdated");
|
_logger.LogError(e, "Error in NameplateService UpdateNameplateDetour");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return _nameplateHook!.Original(raptureAtkModule, namePlateInfo, numArray, stringArray, battleChara, numArrayIndex, stringArrayIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -234,7 +246,7 @@ public unsafe class NameplateService : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (disposing)
|
if (disposing)
|
||||||
{
|
{
|
||||||
_nameplateUpdateHookService.NameplateUpdated -= OnNameplateUpdated;
|
_nameplateHook?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
using Dalamud.Hooking;
|
|
||||||
using Dalamud.Plugin.Services;
|
|
||||||
using Dalamud.Utility.Signatures;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace LightlessSync.Services;
|
|
||||||
|
|
||||||
public unsafe sealed class NameplateUpdateHookService : IDisposable
|
|
||||||
{
|
|
||||||
private delegate nint UpdateNameplateDelegate(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo,
|
|
||||||
NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex);
|
|
||||||
public delegate void NameplateUpdatedHandler(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo,
|
|
||||||
NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex);
|
|
||||||
|
|
||||||
// Glyceri, Thanks :bow:
|
|
||||||
[Signature("40 53 55 57 41 56 48 81 EC ?? ?? ?? ?? 48 8B 84 24", DetourName = nameof(UpdateNameplateDetour))]
|
|
||||||
private readonly Hook<UpdateNameplateDelegate>? _nameplateHook = null;
|
|
||||||
|
|
||||||
private readonly ILogger<NameplateUpdateHookService> _logger;
|
|
||||||
|
|
||||||
public NameplateUpdateHookService(ILogger<NameplateUpdateHookService> logger, IGameInteropProvider interop)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
|
|
||||||
interop.InitializeFromAttributes(this);
|
|
||||||
_nameplateHook?.Enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
public event NameplateUpdatedHandler? NameplateUpdated;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Detour for the game's internal nameplate update function.
|
|
||||||
/// This will be called whenever the client updates any nameplate.
|
|
||||||
/// </summary>
|
|
||||||
private nint UpdateNameplateDetour(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo,
|
|
||||||
NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
NameplateUpdated?.Invoke(raptureAtkModule, namePlateInfo, numArray, stringArray, battleChara, numArrayIndex, stringArrayIndex);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_logger.LogError(e, "Error in NameplateUpdateHookService UpdateNameplateDetour");
|
|
||||||
}
|
|
||||||
|
|
||||||
return _nameplateHook!.Original(raptureAtkModule, namePlateInfo, numArray, stringArray, battleChara, numArrayIndex, stringArrayIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_nameplateHook?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
using LightlessSync.Interop.Ipc;
|
|
||||||
using LightlessSync.LightlessConfiguration.Models;
|
|
||||||
using LightlessSync.LightlessConfiguration;
|
|
||||||
using LightlessSync.Services.Mediator;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace LightlessSync.Services;
|
|
||||||
|
|
||||||
public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriberBase
|
|
||||||
{
|
|
||||||
private readonly IpcManager _ipc;
|
|
||||||
private readonly TempCollectionConfigService _config;
|
|
||||||
private readonly CancellationTokenSource _cleanupCts = new();
|
|
||||||
private int _ran;
|
|
||||||
private const int CleanupBatchSize = 50;
|
|
||||||
private static readonly TimeSpan CleanupBatchDelay = TimeSpan.FromMilliseconds(50);
|
|
||||||
private static readonly TimeSpan OrphanCleanupDelay = TimeSpan.FromDays(1);
|
|
||||||
|
|
||||||
public PenumbraTempCollectionJanitor(
|
|
||||||
ILogger<PenumbraTempCollectionJanitor> logger,
|
|
||||||
LightlessMediator mediator,
|
|
||||||
IpcManager ipc,
|
|
||||||
TempCollectionConfigService config) : base(logger, mediator)
|
|
||||||
{
|
|
||||||
_ipc = ipc;
|
|
||||||
_config = config;
|
|
||||||
|
|
||||||
Mediator.Subscribe<PenumbraInitializedMessage>(this, _ => CleanupOrphansOnBoot());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Register(Guid id)
|
|
||||||
{
|
|
||||||
if (id == Guid.Empty) return;
|
|
||||||
var changed = false;
|
|
||||||
var config = _config.Current;
|
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
var existing = config.OrphanableTempCollectionEntries.FirstOrDefault(entry => entry.Id == id);
|
|
||||||
if (existing is null)
|
|
||||||
{
|
|
||||||
config.OrphanableTempCollectionEntries.Add(new OrphanableTempCollectionEntry
|
|
||||||
{
|
|
||||||
Id = id,
|
|
||||||
RegisteredAtUtc = now
|
|
||||||
});
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
else if (existing.RegisteredAtUtc == DateTime.MinValue)
|
|
||||||
{
|
|
||||||
existing.RegisteredAtUtc = now;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changed)
|
|
||||||
{
|
|
||||||
_config.Save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Unregister(Guid id)
|
|
||||||
{
|
|
||||||
if (id == Guid.Empty) return;
|
|
||||||
var config = _config.Current;
|
|
||||||
var changed = RemoveEntry(config.OrphanableTempCollectionEntries, id) > 0;
|
|
||||||
if (changed)
|
|
||||||
{
|
|
||||||
_config.Save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CleanupOrphansOnBoot()
|
|
||||||
{
|
|
||||||
if (Interlocked.Exchange(ref _ran, 1) == 1)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!_ipc.Penumbra.APIAvailable)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_ = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await CleanupOrphansOnBootAsync(_cleanupCts.Token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Error cleaning orphaned temp collections");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task CleanupOrphansOnBootAsync(CancellationToken token)
|
|
||||||
{
|
|
||||||
var config = _config.Current;
|
|
||||||
var entries = config.OrphanableTempCollectionEntries;
|
|
||||||
if (entries.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
var changed = EnsureEntryTimes(entries, now);
|
|
||||||
var cutoff = now - OrphanCleanupDelay;
|
|
||||||
var expired = entries
|
|
||||||
.Where(entry => entry.Id != Guid.Empty && entry.RegisteredAtUtc != DateTime.MinValue && entry.RegisteredAtUtc <= cutoff)
|
|
||||||
.Select(entry => entry.Id)
|
|
||||||
.Distinct()
|
|
||||||
.ToList();
|
|
||||||
if (expired.Count == 0)
|
|
||||||
{
|
|
||||||
if (changed)
|
|
||||||
{
|
|
||||||
_config.Save();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var appId = Guid.NewGuid();
|
|
||||||
Logger.LogInformation("Cleaning up {count} orphaned Lightless temp collections older than {delay}", expired.Count, OrphanCleanupDelay);
|
|
||||||
|
|
||||||
List<Guid> removedIds = [];
|
|
||||||
foreach (var id in expired)
|
|
||||||
{
|
|
||||||
if (token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _ipc.Penumbra.RemoveTemporaryCollectionAsync(Logger, appId, id).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogDebug(ex, "Failed removing orphaned temp collection {id}", id);
|
|
||||||
}
|
|
||||||
|
|
||||||
removedIds.Add(id);
|
|
||||||
if (removedIds.Count % CleanupBatchSize == 0)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await Task.Delay(CleanupBatchDelay, token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (removedIds.Count == 0)
|
|
||||||
{
|
|
||||||
if (changed)
|
|
||||||
{
|
|
||||||
_config.Save();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var id in removedIds)
|
|
||||||
{
|
|
||||||
RemoveEntry(entries, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
_config.Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
_cleanupCts.Cancel();
|
|
||||||
_cleanupCts.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
base.Dispose(disposing);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int RemoveEntry(List<OrphanableTempCollectionEntry> entries, Guid id)
|
|
||||||
{
|
|
||||||
var removed = 0;
|
|
||||||
for (var i = entries.Count - 1; i >= 0; i--)
|
|
||||||
{
|
|
||||||
if (entries[i].Id != id)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
entries.RemoveAt(i);
|
|
||||||
removed++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return removed;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool EnsureEntryTimes(List<OrphanableTempCollectionEntry> entries, DateTime now)
|
|
||||||
{
|
|
||||||
var changed = false;
|
|
||||||
foreach (var entry in entries)
|
|
||||||
{
|
|
||||||
if (entry.Id == Guid.Empty || entry.RegisteredAtUtc != DateTime.MinValue)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.RegisteredAtUtc = now;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -131,10 +131,7 @@ public sealed class PerformanceCollectorService : IHostedService
|
|||||||
DrawSeparator(sb, longestCounterName);
|
DrawSeparator(sb, longestCounterName);
|
||||||
}
|
}
|
||||||
|
|
||||||
var snapshot = entry.Value.Snapshot();
|
var pastEntries = limitBySeconds > 0 ? entry.Value.Where(e => e.Item1.AddMinutes(limitBySeconds / 60.0d) >= TimeOnly.FromDateTime(DateTime.Now)).ToList() : [.. entry.Value];
|
||||||
var pastEntries = limitBySeconds > 0
|
|
||||||
? snapshot.Where(e => e.Item1.AddMinutes(limitBySeconds / 60.0d) >= TimeOnly.FromDateTime(DateTime.Now)).ToList()
|
|
||||||
: snapshot;
|
|
||||||
|
|
||||||
if (pastEntries.Any())
|
if (pastEntries.Any())
|
||||||
{
|
{
|
||||||
@@ -192,11 +189,7 @@ public sealed class PerformanceCollectorService : IHostedService
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!entries.Value.TryGetLast(out var last))
|
var last = entries.Value.ToList()[^1];
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (last.Item1.AddMinutes(10) < TimeOnly.FromDateTime(DateTime.Now) && !PerformanceCounters.TryRemove(entries.Key, out _))
|
if (last.Item1.AddMinutes(10) < TimeOnly.FromDateTime(DateTime.Now) && !PerformanceCounters.TryRemove(entries.Key, out _))
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Could not remove performance counter {counter}", entries.Key);
|
_logger.LogDebug("Could not remove performance counter {counter}", entries.Key);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ using LightlessSync.LightlessConfiguration;
|
|||||||
using LightlessSync.PlayerData.Pairs;
|
using LightlessSync.PlayerData.Pairs;
|
||||||
using LightlessSync.Services.Events;
|
using LightlessSync.Services.Events;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Services.ModelDecimation;
|
|
||||||
using LightlessSync.Services.TextureCompression;
|
using LightlessSync.Services.TextureCompression;
|
||||||
using LightlessSync.UI;
|
using LightlessSync.UI;
|
||||||
using LightlessSync.WebAPI.Files.Models;
|
using LightlessSync.WebAPI.Files.Models;
|
||||||
@@ -19,14 +18,12 @@ public class PlayerPerformanceService
|
|||||||
private readonly ILogger<PlayerPerformanceService> _logger;
|
private readonly ILogger<PlayerPerformanceService> _logger;
|
||||||
private readonly LightlessMediator _mediator;
|
private readonly LightlessMediator _mediator;
|
||||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||||
private readonly ModelDecimationService _modelDecimationService;
|
|
||||||
private readonly TextureDownscaleService _textureDownscaleService;
|
private readonly TextureDownscaleService _textureDownscaleService;
|
||||||
private readonly Dictionary<string, bool> _warnedForPlayers = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, bool> _warnedForPlayers = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
public PlayerPerformanceService(ILogger<PlayerPerformanceService> logger, LightlessMediator mediator,
|
public PlayerPerformanceService(ILogger<PlayerPerformanceService> logger, LightlessMediator mediator,
|
||||||
PlayerPerformanceConfigService playerPerformanceConfigService, FileCacheManager fileCacheManager,
|
PlayerPerformanceConfigService playerPerformanceConfigService, FileCacheManager fileCacheManager,
|
||||||
XivDataAnalyzer xivDataAnalyzer, TextureDownscaleService textureDownscaleService,
|
XivDataAnalyzer xivDataAnalyzer, TextureDownscaleService textureDownscaleService)
|
||||||
ModelDecimationService modelDecimationService)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
@@ -34,7 +31,6 @@ public class PlayerPerformanceService
|
|||||||
_fileCacheManager = fileCacheManager;
|
_fileCacheManager = fileCacheManager;
|
||||||
_xivDataAnalyzer = xivDataAnalyzer;
|
_xivDataAnalyzer = xivDataAnalyzer;
|
||||||
_textureDownscaleService = textureDownscaleService;
|
_textureDownscaleService = textureDownscaleService;
|
||||||
_modelDecimationService = modelDecimationService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> CheckBothThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData)
|
public async Task<bool> CheckBothThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData)
|
||||||
@@ -115,12 +111,10 @@ public class PlayerPerformanceService
|
|||||||
var config = _playerPerformanceConfigService.Current;
|
var config = _playerPerformanceConfigService.Current;
|
||||||
|
|
||||||
long triUsage = 0;
|
long triUsage = 0;
|
||||||
long effectiveTriUsage = 0;
|
|
||||||
|
|
||||||
if (!charaData.FileReplacements.TryGetValue(API.Data.Enum.ObjectKind.Player, out List<FileReplacementData>? playerReplacements))
|
if (!charaData.FileReplacements.TryGetValue(API.Data.Enum.ObjectKind.Player, out List<FileReplacementData>? playerReplacements))
|
||||||
{
|
{
|
||||||
pairHandler.LastAppliedDataTris = 0;
|
pairHandler.LastAppliedDataTris = 0;
|
||||||
pairHandler.LastAppliedApproximateEffectiveTris = 0;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,40 +123,14 @@ public class PlayerPerformanceService
|
|||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var skipDecimation = config.SkipModelDecimationForPreferredPairs && pairHandler.IsDirectlyPaired && pairHandler.HasStickyPermissions;
|
|
||||||
|
|
||||||
foreach (var hash in moddedModelHashes)
|
foreach (var hash in moddedModelHashes)
|
||||||
{
|
{
|
||||||
var tris = await _xivDataAnalyzer.GetTrianglesByHash(hash).ConfigureAwait(false);
|
triUsage += await _xivDataAnalyzer.GetTrianglesByHash(hash).ConfigureAwait(false);
|
||||||
triUsage += tris;
|
|
||||||
|
|
||||||
long effectiveTris = tris;
|
|
||||||
var fileEntry = _fileCacheManager.GetFileCacheByHash(hash);
|
|
||||||
if (fileEntry != null)
|
|
||||||
{
|
|
||||||
var preferredPath = fileEntry.ResolvedFilepath;
|
|
||||||
if (!skipDecimation)
|
|
||||||
{
|
|
||||||
preferredPath = _modelDecimationService.GetPreferredPath(hash, fileEntry.ResolvedFilepath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.Equals(preferredPath, fileEntry.ResolvedFilepath, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
var decimatedTris = await _xivDataAnalyzer.GetEffectiveTrianglesByHash(hash, preferredPath).ConfigureAwait(false);
|
|
||||||
if (decimatedTris > 0)
|
|
||||||
{
|
|
||||||
effectiveTris = decimatedTris;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
effectiveTriUsage += effectiveTris;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pairHandler.LastAppliedDataTris = triUsage;
|
pairHandler.LastAppliedDataTris = triUsage;
|
||||||
pairHandler.LastAppliedApproximateEffectiveTris = effectiveTriUsage;
|
|
||||||
|
|
||||||
_logger.LogDebug("Calculated triangle usage for {p}", pairHandler);
|
_logger.LogDebug("Calculated VRAM usage for {p}", pairHandler);
|
||||||
|
|
||||||
// no warning of any kind on ignored pairs
|
// no warning of any kind on ignored pairs
|
||||||
if (config.UIDsToIgnore
|
if (config.UIDsToIgnore
|
||||||
@@ -199,9 +167,7 @@ public class PlayerPerformanceService
|
|||||||
public bool ComputeAndAutoPauseOnVRAMUsageThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData, List<DownloadFileTransfer> toDownloadFiles)
|
public bool ComputeAndAutoPauseOnVRAMUsageThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData, List<DownloadFileTransfer> toDownloadFiles)
|
||||||
{
|
{
|
||||||
var config = _playerPerformanceConfigService.Current;
|
var config = _playerPerformanceConfigService.Current;
|
||||||
bool skipDownscale = config.SkipTextureDownscaleForPreferredPairs
|
bool skipDownscale = pairHandler.IsDirectlyPaired && pairHandler.HasStickyPermissions;
|
||||||
&& pairHandler.IsDirectlyPaired
|
|
||||||
&& pairHandler.HasStickyPermissions;
|
|
||||||
|
|
||||||
long vramUsage = 0;
|
long vramUsage = 0;
|
||||||
long effectiveVramUsage = 0;
|
long effectiveVramUsage = 0;
|
||||||
@@ -308,4 +274,4 @@ public class PlayerPerformanceService
|
|||||||
|
|
||||||
private static bool CheckForThreshold(bool thresholdEnabled, long threshold, long value, bool checkForPrefPerm, bool isPrefPerm) =>
|
private static bool CheckForThreshold(bool thresholdEnabled, long threshold, long value, bool checkForPrefPerm, bool isPrefPerm) =>
|
||||||
thresholdEnabled && threshold > 0 && threshold < value && ((checkForPrefPerm && isPrefPerm) || !isPrefPerm);
|
thresholdEnabled && threshold > 0 && threshold < value && ((checkForPrefPerm && isPrefPerm) || !isPrefPerm);
|
||||||
}
|
}
|
||||||
@@ -101,9 +101,9 @@ public class ServerConfigurationManager
|
|||||||
}
|
}
|
||||||
hasMulti = false;
|
hasMulti = false;
|
||||||
|
|
||||||
var charaName = _dalamudUtil.GetPlayerName();
|
var charaName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult();
|
||||||
var worldId = _dalamudUtil.GetHomeWorldId();
|
var worldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult();
|
||||||
var cid = _dalamudUtil.GetCID();
|
var cid = _dalamudUtil.GetCIDAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
var auth = currentServer.Authentications.FindAll(f => string.Equals(f.CharacterName, charaName) && f.WorldId == worldId);
|
var auth = currentServer.Authentications.FindAll(f => string.Equals(f.CharacterName, charaName) && f.WorldId == worldId);
|
||||||
if (auth.Count >= 2)
|
if (auth.Count >= 2)
|
||||||
@@ -148,9 +148,9 @@ public class ServerConfigurationManager
|
|||||||
}
|
}
|
||||||
hasMulti = false;
|
hasMulti = false;
|
||||||
|
|
||||||
var charaName = _dalamudUtil.GetPlayerName();
|
var charaName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult();
|
||||||
var worldId = _dalamudUtil.GetHomeWorldId();
|
var worldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult();
|
||||||
var cid = _dalamudUtil.GetCID();
|
var cid = _dalamudUtil.GetCIDAsync().GetAwaiter().GetResult();
|
||||||
if (!currentServer.Authentications.Any() && currentServer.SecretKeys.Any())
|
if (!currentServer.Authentications.Any() && currentServer.SecretKeys.Any())
|
||||||
{
|
{
|
||||||
currentServer.Authentications.Add(new Authentication()
|
currentServer.Authentications.Add(new Authentication()
|
||||||
@@ -268,16 +268,16 @@ public class ServerConfigurationManager
|
|||||||
{
|
{
|
||||||
if (serverSelectionIndex == -1) serverSelectionIndex = CurrentServerIndex;
|
if (serverSelectionIndex == -1) serverSelectionIndex = CurrentServerIndex;
|
||||||
var server = GetServerByIndex(serverSelectionIndex);
|
var server = GetServerByIndex(serverSelectionIndex);
|
||||||
if (server.Authentications.Exists(c => string.Equals(c.CharacterName, _dalamudUtil.GetPlayerName(), StringComparison.Ordinal)
|
if (server.Authentications.Exists(c => string.Equals(c.CharacterName, _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(), StringComparison.Ordinal)
|
||||||
&& c.WorldId == _dalamudUtil.GetHomeWorldId()))
|
&& c.WorldId == _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult()))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
server.Authentications.Add(new Authentication()
|
server.Authentications.Add(new Authentication()
|
||||||
{
|
{
|
||||||
CharacterName = _dalamudUtil.GetPlayerName(),
|
CharacterName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(),
|
||||||
WorldId = _dalamudUtil.GetHomeWorldId(),
|
WorldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult(),
|
||||||
SecretKeyIdx = !server.UseOAuth2 ? server.SecretKeys.Last().Key : -1,
|
SecretKeyIdx = !server.UseOAuth2 ? server.SecretKeys.Last().Key : -1,
|
||||||
LastSeenCID = _dalamudUtil.GetCID()
|
LastSeenCID = _dalamudUtil.GetCIDAsync().GetAwaiter().GetResult()
|
||||||
});
|
});
|
||||||
Save();
|
Save();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ using LightlessSync.Interop.Ipc;
|
|||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace LightlessSync.Services.TextureCompression;
|
namespace LightlessSync.Services.TextureCompression;
|
||||||
|
|
||||||
@@ -28,9 +27,7 @@ public sealed class TextureCompressionService
|
|||||||
public async Task ConvertTexturesAsync(
|
public async Task ConvertTexturesAsync(
|
||||||
IReadOnlyList<TextureCompressionRequest> requests,
|
IReadOnlyList<TextureCompressionRequest> requests,
|
||||||
IProgress<TextureConversionProgress>? progress,
|
IProgress<TextureConversionProgress>? progress,
|
||||||
CancellationToken token,
|
CancellationToken token)
|
||||||
bool requestRedraw = true,
|
|
||||||
bool includeMipMaps = true)
|
|
||||||
{
|
{
|
||||||
if (requests.Count == 0)
|
if (requests.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -51,7 +48,7 @@ public sealed class TextureCompressionService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await RunPenumbraConversionAsync(request, textureType, total, completed, progress, token, requestRedraw, includeMipMaps).ConfigureAwait(false);
|
await RunPenumbraConversionAsync(request, textureType, total, completed, progress, token).ConfigureAwait(false);
|
||||||
|
|
||||||
completed++;
|
completed++;
|
||||||
}
|
}
|
||||||
@@ -68,16 +65,14 @@ public sealed class TextureCompressionService
|
|||||||
int total,
|
int total,
|
||||||
int completedBefore,
|
int completedBefore,
|
||||||
IProgress<TextureConversionProgress>? progress,
|
IProgress<TextureConversionProgress>? progress,
|
||||||
CancellationToken token,
|
CancellationToken token)
|
||||||
bool requestRedraw,
|
|
||||||
bool includeMipMaps)
|
|
||||||
{
|
{
|
||||||
var primaryPath = request.PrimaryFilePath;
|
var primaryPath = request.PrimaryFilePath;
|
||||||
var displayJob = new TextureConversionJob(
|
var displayJob = new TextureConversionJob(
|
||||||
primaryPath,
|
primaryPath,
|
||||||
primaryPath,
|
primaryPath,
|
||||||
targetType,
|
targetType,
|
||||||
IncludeMipMaps: includeMipMaps,
|
IncludeMipMaps: true,
|
||||||
request.DuplicateFilePaths);
|
request.DuplicateFilePaths);
|
||||||
|
|
||||||
var backupPath = CreateBackupCopy(primaryPath);
|
var backupPath = CreateBackupCopy(primaryPath);
|
||||||
@@ -88,7 +83,7 @@ public sealed class TextureCompressionService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
WaitForAccess(primaryPath);
|
WaitForAccess(primaryPath);
|
||||||
await _ipcManager.Penumbra.ConvertTextureFiles(_logger, new[] { conversionJob }, null, token, requestRedraw).ConfigureAwait(false);
|
await _ipcManager.Penumbra.ConvertTextureFiles(_logger, new[] { conversionJob }, null, token).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!IsValidConversionResult(displayJob.OutputFile))
|
if (!IsValidConversionResult(displayJob.OutputFile))
|
||||||
{
|
{
|
||||||
@@ -133,46 +128,19 @@ public sealed class TextureCompressionService
|
|||||||
var cacheEntries = _fileCacheManager.GetFileCachesByPaths(paths.ToArray());
|
var cacheEntries = _fileCacheManager.GetFileCachesByPaths(paths.ToArray());
|
||||||
foreach (var path in paths)
|
foreach (var path in paths)
|
||||||
{
|
{
|
||||||
var hasExpectedHash = TryGetExpectedHashFromPath(path, out var expectedHash);
|
|
||||||
if (!cacheEntries.TryGetValue(path, out var entry) || entry is null)
|
if (!cacheEntries.TryGetValue(path, out var entry) || entry is null)
|
||||||
{
|
{
|
||||||
if (hasExpectedHash)
|
entry = _fileCacheManager.CreateFileEntry(path);
|
||||||
{
|
|
||||||
entry = _fileCacheManager.CreateCacheEntryWithKnownHash(path, expectedHash);
|
|
||||||
}
|
|
||||||
|
|
||||||
entry ??= _fileCacheManager.CreateFileEntry(path);
|
|
||||||
if (entry is null)
|
if (entry is null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Unable to locate cache entry for {Path}; skipping hash refresh", path);
|
_logger.LogWarning("Unable to locate cache entry for {Path}; skipping hash refresh", path);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (hasExpectedHash && entry.IsCacheEntry && !string.Equals(entry.Hash, expectedHash, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Fixing cache hash mismatch for {Path}: {Current} -> {Expected}", path, entry.Hash, expectedHash);
|
|
||||||
_fileCacheManager.RemoveHashedFile(entry.Hash, entry.PrefixedFilePath, removeDerivedFiles: false);
|
|
||||||
var corrected = _fileCacheManager.CreateCacheEntryWithKnownHash(path, expectedHash);
|
|
||||||
if (corrected is not null)
|
|
||||||
{
|
|
||||||
entry = corrected;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (entry.IsCacheEntry)
|
_fileCacheManager.UpdateHashedFile(entry);
|
||||||
{
|
|
||||||
var info = new FileInfo(path);
|
|
||||||
entry.Size = info.Length;
|
|
||||||
entry.CompressedSize = null;
|
|
||||||
entry.LastModifiedDateTicks = info.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture);
|
|
||||||
_fileCacheManager.UpdateHashedFile(entry, computeProperties: false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_fileCacheManager.UpdateHashedFile(entry);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -181,35 +149,6 @@ public sealed class TextureCompressionService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryGetExpectedHashFromPath(string path, out string hash)
|
|
||||||
{
|
|
||||||
hash = Path.GetFileNameWithoutExtension(path);
|
|
||||||
if (string.IsNullOrWhiteSpace(hash))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hash.Length is not (40 or 64))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < hash.Length; i++)
|
|
||||||
{
|
|
||||||
var c = hash[i];
|
|
||||||
var isHex = (c >= '0' && c <= '9')
|
|
||||||
|| (c >= 'a' && c <= 'f')
|
|
||||||
|| (c >= 'A' && c <= 'F');
|
|
||||||
if (!isHex)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hash = hash.ToUpperInvariant();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly string WorkingDirectory =
|
private static readonly string WorkingDirectory =
|
||||||
Path.Combine(Path.GetTempPath(), "LightlessSync.TextureCompression");
|
Path.Combine(Path.GetTempPath(), "LightlessSync.TextureCompression");
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,9 @@ using System.Buffers.Binary;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
|
||||||
using OtterTex;
|
using OtterTex;
|
||||||
using OtterImage = OtterTex.Image;
|
using OtterImage = OtterTex.Image;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.Utils;
|
|
||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Lumina.Data.Files;
|
using Lumina.Data.Files;
|
||||||
@@ -32,12 +30,10 @@ public sealed class TextureDownscaleService
|
|||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||||
private readonly FileCacheManager _fileCacheManager;
|
private readonly FileCacheManager _fileCacheManager;
|
||||||
private readonly TextureCompressionService _textureCompressionService;
|
|
||||||
|
|
||||||
private readonly TaskRegistry<string> _downscaleDeduplicator = new();
|
private readonly ConcurrentDictionary<string, Task> _activeJobs = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private readonly ConcurrentDictionary<string, string> _downscaledPaths = new(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, string> _downscaledPaths = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private readonly SemaphoreSlim _downscaleSemaphore = new(4);
|
private readonly SemaphoreSlim _downscaleSemaphore = new(4);
|
||||||
private readonly SemaphoreSlim _compressionSemaphore = new(1);
|
|
||||||
private static readonly IReadOnlyDictionary<int, TextureCompressionTarget> BlockCompressedFormatMap =
|
private static readonly IReadOnlyDictionary<int, TextureCompressionTarget> BlockCompressedFormatMap =
|
||||||
new Dictionary<int, TextureCompressionTarget>
|
new Dictionary<int, TextureCompressionTarget>
|
||||||
{
|
{
|
||||||
@@ -72,50 +68,23 @@ public sealed class TextureDownscaleService
|
|||||||
ILogger<TextureDownscaleService> logger,
|
ILogger<TextureDownscaleService> logger,
|
||||||
LightlessConfigService configService,
|
LightlessConfigService configService,
|
||||||
PlayerPerformanceConfigService playerPerformanceConfigService,
|
PlayerPerformanceConfigService playerPerformanceConfigService,
|
||||||
FileCacheManager fileCacheManager,
|
FileCacheManager fileCacheManager)
|
||||||
TextureCompressionService textureCompressionService)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_playerPerformanceConfigService = playerPerformanceConfigService;
|
_playerPerformanceConfigService = playerPerformanceConfigService;
|
||||||
_fileCacheManager = fileCacheManager;
|
_fileCacheManager = fileCacheManager;
|
||||||
_textureCompressionService = textureCompressionService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ScheduleDownscale(string hash, string filePath, TextureMapKind mapKind)
|
public void ScheduleDownscale(string hash, string filePath, TextureMapKind mapKind)
|
||||||
=> ScheduleDownscale(hash, filePath, () => mapKind);
|
|
||||||
|
|
||||||
public void ScheduleDownscale(string hash, string filePath, Func<TextureMapKind> mapKindFactory)
|
|
||||||
{
|
{
|
||||||
if (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) return;
|
if (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) return;
|
||||||
if (_downscaleDeduplicator.TryGetExisting(hash, out _)) return;
|
if (_activeJobs.ContainsKey(hash)) return;
|
||||||
|
|
||||||
_downscaleDeduplicator.GetOrStart(hash, async () =>
|
_activeJobs[hash] = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
TextureMapKind mapKind;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
mapKind = mapKindFactory();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Failed to determine texture map kind for {Hash}; skipping downscale", hash);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await DownscaleInternalAsync(hash, filePath, mapKind).ConfigureAwait(false);
|
await DownscaleInternalAsync(hash, filePath, mapKind).ConfigureAwait(false);
|
||||||
});
|
}, CancellationToken.None);
|
||||||
}
|
|
||||||
|
|
||||||
public bool ShouldScheduleDownscale(string filePath)
|
|
||||||
{
|
|
||||||
if (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var performanceConfig = _playerPerformanceConfigService.Current;
|
|
||||||
return performanceConfig.EnableNonIndexTextureMipTrim
|
|
||||||
|| performanceConfig.EnableIndexTextureDownscale
|
|
||||||
|| performanceConfig.EnableUncompressedTextureCompression;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetPreferredPath(string hash, string originalPath)
|
public string GetPreferredPath(string hash, string originalPath)
|
||||||
@@ -152,7 +121,7 @@ public sealed class TextureDownscaleService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_downscaleDeduplicator.TryGetExisting(hash, out var job))
|
if (_activeJobs.TryGetValue(hash, out var job))
|
||||||
{
|
{
|
||||||
pending.Add(job);
|
pending.Add(job);
|
||||||
}
|
}
|
||||||
@@ -190,18 +159,10 @@ public sealed class TextureDownscaleService
|
|||||||
targetMaxDimension = ResolveTargetMaxDimension();
|
targetMaxDimension = ResolveTargetMaxDimension();
|
||||||
onlyDownscaleUncompressed = performanceConfig.OnlyDownscaleUncompressedTextures;
|
onlyDownscaleUncompressed = performanceConfig.OnlyDownscaleUncompressedTextures;
|
||||||
|
|
||||||
if (onlyDownscaleUncompressed && !headerInfo.HasValue)
|
|
||||||
{
|
|
||||||
_downscaledPaths[hash] = sourcePath;
|
|
||||||
_logger.LogTrace("Skipping downscale for texture {Hash}; format unknown and only-uncompressed enabled.", hash);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
destination = Path.Combine(GetDownscaledDirectory(), $"{hash}.tex");
|
destination = Path.Combine(GetDownscaledDirectory(), $"{hash}.tex");
|
||||||
if (File.Exists(destination))
|
if (File.Exists(destination))
|
||||||
{
|
{
|
||||||
RegisterDownscaledTexture(hash, sourcePath, destination);
|
RegisterDownscaledTexture(hash, sourcePath, destination);
|
||||||
await TryAutoCompressAsync(hash, destination, mapKind, null).ConfigureAwait(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +173,6 @@ public sealed class TextureDownscaleService
|
|||||||
if (performanceConfig.EnableNonIndexTextureMipTrim
|
if (performanceConfig.EnableNonIndexTextureMipTrim
|
||||||
&& await TryDropTopMipAsync(hash, sourcePath, destination, targetMaxDimension, onlyDownscaleUncompressed, headerInfo).ConfigureAwait(false))
|
&& await TryDropTopMipAsync(hash, sourcePath, destination, targetMaxDimension, onlyDownscaleUncompressed, headerInfo).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
await TryAutoCompressAsync(hash, destination, mapKind, null).ConfigureAwait(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +183,6 @@ public sealed class TextureDownscaleService
|
|||||||
|
|
||||||
_downscaledPaths[hash] = sourcePath;
|
_downscaledPaths[hash] = sourcePath;
|
||||||
_logger.LogTrace("Skipping downscale for non-index texture {Hash}; no mip reduction required.", hash);
|
_logger.LogTrace("Skipping downscale for non-index texture {Hash}; no mip reduction required.", hash);
|
||||||
await TryAutoCompressAsync(hash, sourcePath, mapKind, headerInfo).ConfigureAwait(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +190,6 @@ public sealed class TextureDownscaleService
|
|||||||
{
|
{
|
||||||
_downscaledPaths[hash] = sourcePath;
|
_downscaledPaths[hash] = sourcePath;
|
||||||
_logger.LogTrace("Skipping downscale for index texture {Hash}; feature disabled.", hash);
|
_logger.LogTrace("Skipping downscale for index texture {Hash}; feature disabled.", hash);
|
||||||
await TryAutoCompressAsync(hash, sourcePath, mapKind, headerInfo).ConfigureAwait(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,7 +199,6 @@ public sealed class TextureDownscaleService
|
|||||||
{
|
{
|
||||||
_downscaledPaths[hash] = sourcePath;
|
_downscaledPaths[hash] = sourcePath;
|
||||||
_logger.LogTrace("Skipping downscale for index texture {Hash}; header dimensions {Width}x{Height} within target.", hash, headerValue.Width, headerValue.Height);
|
_logger.LogTrace("Skipping downscale for index texture {Hash}; header dimensions {Width}x{Height} within target.", hash, headerValue.Width, headerValue.Height);
|
||||||
await TryAutoCompressAsync(hash, sourcePath, mapKind, headerInfo).ConfigureAwait(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,12 +206,10 @@ public sealed class TextureDownscaleService
|
|||||||
{
|
{
|
||||||
_downscaledPaths[hash] = sourcePath;
|
_downscaledPaths[hash] = sourcePath;
|
||||||
_logger.LogTrace("Skipping downscale for index texture {Hash}; block compressed format {Format}.", hash, headerInfo.Value.Format);
|
_logger.LogTrace("Skipping downscale for index texture {Hash}; block compressed format {Format}.", hash, headerInfo.Value.Format);
|
||||||
await TryAutoCompressAsync(hash, sourcePath, mapKind, headerInfo).ConfigureAwait(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using var sourceScratch = TexFileHelper.Load(sourcePath);
|
using var sourceScratch = TexFileHelper.Load(sourcePath);
|
||||||
var sourceFormat = sourceScratch.Meta.Format;
|
|
||||||
using var rgbaScratch = sourceScratch.GetRGBA(out var rgbaInfo).ThrowIfError(rgbaInfo);
|
using var rgbaScratch = sourceScratch.GetRGBA(out var rgbaInfo).ThrowIfError(rgbaInfo);
|
||||||
|
|
||||||
var bytesPerPixel = rgbaInfo.Meta.Format.BitsPerPixel() / 8;
|
var bytesPerPixel = rgbaInfo.Meta.Format.BitsPerPixel() / 8;
|
||||||
@@ -270,39 +225,16 @@ public sealed class TextureDownscaleService
|
|||||||
{
|
{
|
||||||
_downscaledPaths[hash] = sourcePath;
|
_downscaledPaths[hash] = sourcePath;
|
||||||
_logger.LogTrace("Skipping downscale for index texture {Hash}; already within bounds.", hash);
|
_logger.LogTrace("Skipping downscale for index texture {Hash}; already within bounds.", hash);
|
||||||
await TryAutoCompressAsync(hash, sourcePath, mapKind, headerInfo).ConfigureAwait(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using var resized = IndexDownscaler.Downscale(originalImage, targetSize.width, targetSize.height, BlockMultiple);
|
using var resized = IndexDownscaler.Downscale(originalImage, targetSize.width, targetSize.height, BlockMultiple);
|
||||||
|
|
||||||
var canReencodeWithPenumbra = TryResolveCompressionTarget(headerInfo, sourceFormat, out var compressionTarget);
|
|
||||||
using var resizedScratch = CreateScratchImage(resized, targetSize.width, targetSize.height);
|
using var resizedScratch = CreateScratchImage(resized, targetSize.width, targetSize.height);
|
||||||
if (!TryConvertForSave(resizedScratch, sourceFormat, out var finalScratch, canReencodeWithPenumbra))
|
using var finalScratch = resizedScratch.Convert(DXGIFormat.B8G8R8A8UNorm);
|
||||||
{
|
|
||||||
if (canReencodeWithPenumbra
|
|
||||||
&& await TryReencodeWithPenumbraAsync(hash, sourcePath, destination, resizedScratch, compressionTarget).ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
await TryAutoCompressAsync(hash, destination, mapKind, null).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_downscaledPaths[hash] = sourcePath;
|
TexFileHelper.Save(destination, finalScratch);
|
||||||
_logger.LogTrace(
|
RegisterDownscaledTexture(hash, sourcePath, destination);
|
||||||
"Skipping downscale for index texture {Hash}; failed to re-encode to {Format}.",
|
|
||||||
hash,
|
|
||||||
sourceFormat);
|
|
||||||
await TryAutoCompressAsync(hash, sourcePath, mapKind, headerInfo).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using (finalScratch)
|
|
||||||
{
|
|
||||||
TexFileHelper.Save(destination, finalScratch);
|
|
||||||
RegisterDownscaledTexture(hash, sourcePath, destination);
|
|
||||||
}
|
|
||||||
|
|
||||||
await TryAutoCompressAsync(hash, destination, mapKind, null).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -322,6 +254,7 @@ public sealed class TextureDownscaleService
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_downscaleSemaphore.Release();
|
_downscaleSemaphore.Release();
|
||||||
|
_activeJobs.TryRemove(hash, out _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,157 +307,6 @@ public sealed class TextureDownscaleService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryConvertForSave(
|
|
||||||
ScratchImage source,
|
|
||||||
DXGIFormat sourceFormat,
|
|
||||||
out ScratchImage result,
|
|
||||||
bool attemptPenumbraFallback)
|
|
||||||
{
|
|
||||||
var isCompressed = sourceFormat.IsCompressed();
|
|
||||||
var targetFormat = isCompressed ? sourceFormat : DXGIFormat.B8G8R8A8UNorm;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
result = source.Convert(targetFormat);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var compressedFallback = attemptPenumbraFallback
|
|
||||||
? " Attempting Penumbra re-encode."
|
|
||||||
: " Skipping downscale.";
|
|
||||||
_logger.LogWarning(
|
|
||||||
ex,
|
|
||||||
"Failed to convert downscaled texture to {Format}.{Fallback}",
|
|
||||||
targetFormat,
|
|
||||||
isCompressed ? compressedFallback : " Falling back to B8G8R8A8.");
|
|
||||||
if (isCompressed)
|
|
||||||
{
|
|
||||||
result = default!;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = source.Convert(DXGIFormat.B8G8R8A8UNorm);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryResolveCompressionTarget(TexHeaderInfo? headerInfo, DXGIFormat sourceFormat, out TextureCompressionTarget target)
|
|
||||||
{
|
|
||||||
if (headerInfo is { } info && TryGetCompressionTarget(info.Format, out target))
|
|
||||||
{
|
|
||||||
return _textureCompressionService.IsTargetSelectable(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sourceFormat.IsCompressed() && BlockCompressedFormatMap.TryGetValue((int)sourceFormat, out target))
|
|
||||||
{
|
|
||||||
return _textureCompressionService.IsTargetSelectable(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
target = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<bool> TryReencodeWithPenumbraAsync(
|
|
||||||
string hash,
|
|
||||||
string sourcePath,
|
|
||||||
string destination,
|
|
||||||
ScratchImage resizedScratch,
|
|
||||||
TextureCompressionTarget target)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var uncompressed = resizedScratch.Convert(DXGIFormat.B8G8R8A8UNorm);
|
|
||||||
TexFileHelper.Save(destination, uncompressed);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Failed to save uncompressed downscaled texture for {Hash}. Skipping downscale.", hash);
|
|
||||||
TryDelete(destination);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _compressionSemaphore.WaitAsync().ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var request = new TextureCompressionRequest(destination, Array.Empty<string>(), target);
|
|
||||||
await _textureCompressionService
|
|
||||||
.ConvertTexturesAsync(new[] { request }, null, CancellationToken.None, requestRedraw: false)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Failed to re-encode downscaled texture {Hash} to {Target}. Skipping downscale.", hash, target);
|
|
||||||
TryDelete(destination);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_compressionSemaphore.Release();
|
|
||||||
}
|
|
||||||
|
|
||||||
RegisterDownscaledTexture(hash, sourcePath, destination);
|
|
||||||
_logger.LogDebug("Downscaled texture {Hash} -> {Path} (re-encoded via Penumbra).", hash, destination);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task TryAutoCompressAsync(string hash, string texturePath, TextureMapKind mapKind, TexHeaderInfo? headerInfo)
|
|
||||||
{
|
|
||||||
var performanceConfig = _playerPerformanceConfigService.Current;
|
|
||||||
if (!performanceConfig.EnableUncompressedTextureCompression)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(texturePath) || !File.Exists(texturePath))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var info = headerInfo ?? (TryReadTexHeader(texturePath, out var header) ? header : (TexHeaderInfo?)null);
|
|
||||||
if (!info.HasValue)
|
|
||||||
{
|
|
||||||
_logger.LogTrace("Skipping auto-compress for texture {Hash}; unable to read header.", hash);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsBlockCompressedFormat(info.Value.Format))
|
|
||||||
{
|
|
||||||
_logger.LogTrace("Skipping auto-compress for texture {Hash}; already block-compressed.", hash);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var suggestion = TextureMetadataHelper.GetSuggestedTarget(info.Value.Format.ToString(), mapKind, texturePath);
|
|
||||||
if (suggestion is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var target = _textureCompressionService.NormalizeTarget(suggestion.Value.Target);
|
|
||||||
if (!_textureCompressionService.IsTargetSelectable(target))
|
|
||||||
{
|
|
||||||
_logger.LogTrace("Skipping auto-compress for texture {Hash}; target {Target} not supported.", hash, target);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _compressionSemaphore.WaitAsync().ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var includeMipMaps = !performanceConfig.SkipUncompressedTextureCompressionMipMaps;
|
|
||||||
var request = new TextureCompressionRequest(texturePath, Array.Empty<string>(), target);
|
|
||||||
await _textureCompressionService
|
|
||||||
.ConvertTexturesAsync(new[] { request }, null, CancellationToken.None, requestRedraw: false, includeMipMaps: includeMipMaps)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Auto-compress failed for texture {Hash} ({Path})", hash, texturePath);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_compressionSemaphore.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsIndexMap(TextureMapKind kind)
|
private static bool IsIndexMap(TextureMapKind kind)
|
||||||
=> kind is TextureMapKind.Mask
|
=> kind is TextureMapKind.Mask
|
||||||
or TextureMapKind.Index;
|
or TextureMapKind.Index;
|
||||||
@@ -873,7 +655,7 @@ public sealed class TextureDownscaleService
|
|||||||
|
|
||||||
if (!string.Equals(cacheEntry.PrefixedFilePath, prefixed, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(cacheEntry.PrefixedFilePath, prefixed, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
_fileCacheManager.RemoveHashedFile(cacheEntry.Hash, cacheEntry.PrefixedFilePath, removeDerivedFiles: false);
|
_fileCacheManager.RemoveHashedFile(cacheEntry.Hash, cacheEntry.PrefixedFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
_fileCacheManager.UpdateHashedFile(replacement, computeProperties: false);
|
_fileCacheManager.UpdateHashedFile(replacement, computeProperties: false);
|
||||||
|
|||||||
@@ -394,21 +394,6 @@ public sealed class TextureMetadataHelper
|
|||||||
if (string.IsNullOrEmpty(fileNameWithExtension) && string.IsNullOrEmpty(fileNameWithoutExtension))
|
if (string.IsNullOrEmpty(fileNameWithExtension) && string.IsNullOrEmpty(fileNameWithoutExtension))
|
||||||
return TextureMapKind.Unknown;
|
return TextureMapKind.Unknown;
|
||||||
|
|
||||||
if (normalized.Contains("/eye/eyelids_shadow.tex", StringComparison.Ordinal))
|
|
||||||
return TextureMapKind.Normal;
|
|
||||||
|
|
||||||
if (normalized.Contains("/ui/map/", StringComparison.Ordinal) && !string.IsNullOrEmpty(fileNameWithoutExtension))
|
|
||||||
{
|
|
||||||
if (fileNameWithoutExtension.EndsWith("m_m", StringComparison.Ordinal)
|
|
||||||
|| fileNameWithoutExtension.EndsWith("m_s", StringComparison.Ordinal))
|
|
||||||
return TextureMapKind.Mask;
|
|
||||||
|
|
||||||
if (fileNameWithoutExtension.EndsWith("_m", StringComparison.Ordinal)
|
|
||||||
|| fileNameWithoutExtension.EndsWith("_s", StringComparison.Ordinal)
|
|
||||||
|| fileNameWithoutExtension.EndsWith("d", StringComparison.Ordinal))
|
|
||||||
return TextureMapKind.Diffuse;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var (kind, token) in MapTokens)
|
foreach (var (kind, token) in MapTokens)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(fileNameWithExtension) &&
|
if (!string.IsNullOrEmpty(fileNameWithExtension) &&
|
||||||
@@ -578,16 +563,7 @@ public sealed class TextureMetadataHelper
|
|||||||
|
|
||||||
var normalized = format.ToUpperInvariant();
|
var normalized = format.ToUpperInvariant();
|
||||||
return normalized.Contains("A8", StringComparison.Ordinal)
|
return normalized.Contains("A8", StringComparison.Ordinal)
|
||||||
|| normalized.Contains("A1", StringComparison.Ordinal)
|
|
||||||
|| normalized.Contains("A4", StringComparison.Ordinal)
|
|
||||||
|| normalized.Contains("A16", StringComparison.Ordinal)
|
|
||||||
|| normalized.Contains("A32", StringComparison.Ordinal)
|
|
||||||
|| normalized.Contains("ARGB", StringComparison.Ordinal)
|
|| normalized.Contains("ARGB", StringComparison.Ordinal)
|
||||||
|| normalized.Contains("RGBA", StringComparison.Ordinal)
|
|
||||||
|| normalized.Contains("BGRA", StringComparison.Ordinal)
|
|
||||||
|| normalized.Contains("DXT3", StringComparison.Ordinal)
|
|
||||||
|| normalized.Contains("DXT5", StringComparison.Ordinal)
|
|
||||||
|| normalized.Contains("BC2", StringComparison.Ordinal)
|
|
||||||
|| normalized.Contains("BC3", StringComparison.Ordinal)
|
|| normalized.Contains("BC3", StringComparison.Ordinal)
|
||||||
|| normalized.Contains("BC7", StringComparison.Ordinal);
|
|| normalized.Contains("BC7", StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ using LightlessSync.UI.Tags;
|
|||||||
using LightlessSync.WebAPI;
|
using LightlessSync.WebAPI;
|
||||||
using LightlessSync.UI.Services;
|
using LightlessSync.UI.Services;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using LightlessSync.PlayerData.Factories;
|
|
||||||
|
|
||||||
namespace LightlessSync.Services;
|
namespace LightlessSync.Services;
|
||||||
|
|
||||||
@@ -24,7 +23,6 @@ public class UiFactory
|
|||||||
private readonly PerformanceCollectorService _performanceCollectorService;
|
private readonly PerformanceCollectorService _performanceCollectorService;
|
||||||
private readonly ProfileTagService _profileTagService;
|
private readonly ProfileTagService _profileTagService;
|
||||||
private readonly DalamudUtilService _dalamudUtilService;
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
private readonly PairFactory _pairFactory;
|
|
||||||
|
|
||||||
public UiFactory(
|
public UiFactory(
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
@@ -36,8 +34,7 @@ public class UiFactory
|
|||||||
LightlessProfileManager lightlessProfileManager,
|
LightlessProfileManager lightlessProfileManager,
|
||||||
PerformanceCollectorService performanceCollectorService,
|
PerformanceCollectorService performanceCollectorService,
|
||||||
ProfileTagService profileTagService,
|
ProfileTagService profileTagService,
|
||||||
DalamudUtilService dalamudUtilService,
|
DalamudUtilService dalamudUtilService)
|
||||||
PairFactory pairFactory)
|
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_lightlessMediator = lightlessMediator;
|
_lightlessMediator = lightlessMediator;
|
||||||
@@ -49,7 +46,6 @@ public class UiFactory
|
|||||||
_performanceCollectorService = performanceCollectorService;
|
_performanceCollectorService = performanceCollectorService;
|
||||||
_profileTagService = profileTagService;
|
_profileTagService = profileTagService;
|
||||||
_dalamudUtilService = dalamudUtilService;
|
_dalamudUtilService = dalamudUtilService;
|
||||||
_pairFactory = pairFactory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public SyncshellAdminUI CreateSyncshellAdminUi(GroupFullInfoDto dto)
|
public SyncshellAdminUI CreateSyncshellAdminUi(GroupFullInfoDto dto)
|
||||||
@@ -62,8 +58,7 @@ public class UiFactory
|
|||||||
_pairUiService,
|
_pairUiService,
|
||||||
dto,
|
dto,
|
||||||
_performanceCollectorService,
|
_performanceCollectorService,
|
||||||
_lightlessProfileManager,
|
_lightlessProfileManager);
|
||||||
_pairFactory);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair)
|
public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair)
|
||||||
@@ -110,7 +105,6 @@ public class UiFactory
|
|||||||
groupData: groupData,
|
groupData: groupData,
|
||||||
isLightfinderContext: isLightfinderContext,
|
isLightfinderContext: isLightfinderContext,
|
||||||
lightfinderCid: lightfinderCid,
|
lightfinderCid: lightfinderCid,
|
||||||
performanceCollector: _performanceCollectorService,
|
performanceCollector: _performanceCollectorService);
|
||||||
_apiController);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,20 +13,16 @@ namespace LightlessSync.Services;
|
|||||||
public sealed class UiService : DisposableMediatorSubscriberBase
|
public sealed class UiService : DisposableMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
private readonly List<WindowMediatorSubscriberBase> _createdWindows = [];
|
private readonly List<WindowMediatorSubscriberBase> _createdWindows = [];
|
||||||
private readonly List<WindowMediatorSubscriberBase> _registeredWindows = [];
|
|
||||||
private readonly HashSet<WindowMediatorSubscriberBase> _uiHiddenWindows = [];
|
|
||||||
private readonly IUiBuilder _uiBuilder;
|
private readonly IUiBuilder _uiBuilder;
|
||||||
private readonly FileDialogManager _fileDialogManager;
|
private readonly FileDialogManager _fileDialogManager;
|
||||||
private readonly ILogger<UiService> _logger;
|
private readonly ILogger<UiService> _logger;
|
||||||
private readonly LightlessConfigService _lightlessConfigService;
|
private readonly LightlessConfigService _lightlessConfigService;
|
||||||
private readonly DalamudUtilService _dalamudUtilService;
|
|
||||||
private readonly WindowSystem _windowSystem;
|
private readonly WindowSystem _windowSystem;
|
||||||
private readonly UiFactory _uiFactory;
|
private readonly UiFactory _uiFactory;
|
||||||
private readonly PairFactory _pairFactory;
|
private readonly PairFactory _pairFactory;
|
||||||
private bool _uiHideActive;
|
|
||||||
|
|
||||||
public UiService(ILogger<UiService> logger, IUiBuilder uiBuilder,
|
public UiService(ILogger<UiService> logger, IUiBuilder uiBuilder,
|
||||||
LightlessConfigService lightlessConfigService, DalamudUtilService dalamudUtilService, WindowSystem windowSystem,
|
LightlessConfigService lightlessConfigService, WindowSystem windowSystem,
|
||||||
IEnumerable<WindowMediatorSubscriberBase> windows,
|
IEnumerable<WindowMediatorSubscriberBase> windows,
|
||||||
UiFactory uiFactory, FileDialogManager fileDialogManager,
|
UiFactory uiFactory, FileDialogManager fileDialogManager,
|
||||||
LightlessMediator lightlessMediator, PairFactory pairFactory) : base(logger, lightlessMediator)
|
LightlessMediator lightlessMediator, PairFactory pairFactory) : base(logger, lightlessMediator)
|
||||||
@@ -35,7 +31,6 @@ public sealed class UiService : DisposableMediatorSubscriberBase
|
|||||||
_logger.LogTrace("Creating {type}", GetType().Name);
|
_logger.LogTrace("Creating {type}", GetType().Name);
|
||||||
_uiBuilder = uiBuilder;
|
_uiBuilder = uiBuilder;
|
||||||
_lightlessConfigService = lightlessConfigService;
|
_lightlessConfigService = lightlessConfigService;
|
||||||
_dalamudUtilService = dalamudUtilService;
|
|
||||||
_windowSystem = windowSystem;
|
_windowSystem = windowSystem;
|
||||||
_uiFactory = uiFactory;
|
_uiFactory = uiFactory;
|
||||||
_pairFactory = pairFactory;
|
_pairFactory = pairFactory;
|
||||||
@@ -48,7 +43,6 @@ public sealed class UiService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
foreach (var window in windows)
|
foreach (var window in windows)
|
||||||
{
|
{
|
||||||
_registeredWindows.Add(window);
|
|
||||||
_windowSystem.AddWindow(window);
|
_windowSystem.AddWindow(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,8 +176,6 @@ public sealed class UiService : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
_windowSystem.RemoveWindow(msg.Window);
|
_windowSystem.RemoveWindow(msg.Window);
|
||||||
_createdWindows.Remove(msg.Window);
|
_createdWindows.Remove(msg.Window);
|
||||||
_registeredWindows.Remove(msg.Window);
|
|
||||||
_uiHiddenWindows.Remove(msg.Window);
|
|
||||||
msg.Window.Dispose();
|
msg.Window.Dispose();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -227,72 +219,12 @@ public sealed class UiService : DisposableMediatorSubscriberBase
|
|||||||
MainStyle.PushStyle();
|
MainStyle.PushStyle();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var hideOtherUi = ShouldHideOtherUi();
|
|
||||||
UpdateUiHideState(hideOtherUi);
|
|
||||||
_windowSystem.Draw();
|
_windowSystem.Draw();
|
||||||
if (!hideOtherUi)
|
_fileDialogManager.Draw();
|
||||||
_fileDialogManager.Draw();
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
MainStyle.PopStyle();
|
MainStyle.PopStyle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private bool ShouldHideOtherUi()
|
|
||||||
{
|
|
||||||
var config = _lightlessConfigService.Current;
|
|
||||||
if (!config.ShowUiWhenUiHidden && _dalamudUtilService.IsGameUiHidden)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (!config.ShowUiInGpose && _dalamudUtilService.IsInGpose)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateUiHideState(bool hideOtherUi)
|
|
||||||
{
|
|
||||||
if (!hideOtherUi)
|
|
||||||
{
|
|
||||||
if (_uiHideActive)
|
|
||||||
{
|
|
||||||
foreach (var window in _uiHiddenWindows)
|
|
||||||
{
|
|
||||||
window.IsOpen = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_uiHiddenWindows.Clear();
|
|
||||||
_uiHideActive = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_uiHideActive = true;
|
|
||||||
foreach (var window in EnumerateManagedWindows())
|
|
||||||
{
|
|
||||||
if (window is ZoneChatUi)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!window.IsOpen)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
_uiHiddenWindows.Add(window);
|
|
||||||
window.IsOpen = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<WindowMediatorSubscriberBase> EnumerateManagedWindows()
|
|
||||||
{
|
|
||||||
foreach (var window in _registeredWindows)
|
|
||||||
{
|
|
||||||
yield return window;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var window in _createdWindows)
|
|
||||||
{
|
|
||||||
yield return window;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user