Compare commits
171 Commits
2.0.1.70-D
...
2.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
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: 56566003e0...8e4432af45
@@ -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: ""
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -33,14 +31,6 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
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;
|
||||||
@@ -55,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))
|
||||||
@@ -133,114 +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";
|
|
||||||
await File.WriteAllBytesAsync(tmpPath, compressed, token).ConfigureAwait(false);
|
|
||||||
File.Move(tmpPath, compressedPath, overwrite: true);
|
|
||||||
|
|
||||||
var compressedSize = compressed.LongLength;
|
|
||||||
SetSizeInfo(hash, originalSize, compressedSize);
|
|
||||||
UpdateEntitiesSizes(hash, originalSize, compressedSize);
|
|
||||||
|
|
||||||
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;
|
||||||
@@ -448,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)
|
||||||
@@ -1030,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)
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -25,10 +28,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
private readonly object _ownedHandlerLock = new();
|
private readonly object _ownedHandlerLock = new();
|
||||||
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 string[] _handledFileTypesWithRecording;
|
|
||||||
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;
|
||||||
@@ -42,7 +42,6 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
_dalamudUtil = dalamudUtil;
|
_dalamudUtil = dalamudUtil;
|
||||||
_actorObjectService = actorObjectService;
|
_actorObjectService = actorObjectService;
|
||||||
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||||
_handledFileTypesWithRecording = _handledRecordingFileTypes.Concat(_handledFileTypes).ToArray();
|
|
||||||
|
|
||||||
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));
|
||||||
@@ -52,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);
|
||||||
}
|
}
|
||||||
@@ -85,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
|
||||||
@@ -94,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;
|
||||||
@@ -137,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))
|
||||||
@@ -162,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);
|
||||||
@@ -207,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();
|
||||||
}
|
}
|
||||||
@@ -222,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)
|
||||||
@@ -297,13 +285,33 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private void DalamudUtil_FrameworkUpdate()
|
private void DalamudUtil_FrameworkUpdate()
|
||||||
{
|
{
|
||||||
RefreshPlayerRelatedAddressMap();
|
|
||||||
|
|
||||||
lock (_cacheAdditionLock)
|
lock (_cacheAdditionLock)
|
||||||
{
|
{
|
||||||
_cachedHandledPaths.Clear();
|
_cachedHandledPaths.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var activeDescriptors = new Dictionary<nint, ObjectKind>();
|
||||||
|
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
|
||||||
|
{
|
||||||
|
if (TryResolveObjectKind(descriptor, out var resolvedKind))
|
||||||
|
{
|
||||||
|
activeDescriptors[descriptor.Address] = resolvedKind;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var address in _cachedFrameAddresses.Keys.ToList())
|
||||||
|
{
|
||||||
|
if (!activeDescriptors.ContainsKey(address))
|
||||||
|
{
|
||||||
|
_cachedFrameAddresses.TryRemove(address, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var descriptor in activeDescriptors)
|
||||||
|
{
|
||||||
|
_cachedFrameAddresses[descriptor.Key] = descriptor.Value;
|
||||||
|
}
|
||||||
|
|
||||||
if (_lastClassJobId != _dalamudUtil.ClassJobId)
|
if (_lastClassJobId != _dalamudUtil.ClassJobId)
|
||||||
{
|
{
|
||||||
_lastClassJobId = _dalamudUtil.ClassJobId;
|
_lastClassJobId = _dalamudUtil.ClassJobId;
|
||||||
@@ -315,9 +323,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData);
|
PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData);
|
||||||
SemiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache.Concat(jobSpecificData ?? []).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
SemiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache.Concat(jobSpecificData ?? []).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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var kind in Enum.GetValues(typeof(ObjectKind)).Cast<ObjectKind>())
|
foreach (var kind in Enum.GetValues(typeof(ObjectKind)).Cast<ObjectKind>())
|
||||||
@@ -334,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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -349,24 +352,22 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
_semiTransientResources = null;
|
_semiTransientResources = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RefreshPlayerRelatedAddressMap()
|
private static bool TryResolveObjectKind(ActorObjectService.ActorDescriptor descriptor, out ObjectKind resolvedKind)
|
||||||
{
|
{
|
||||||
_playerRelatedByAddress.Clear();
|
if (descriptor.OwnedKind is ObjectKind ownedKind)
|
||||||
var updatedFrameAddresses = new ConcurrentDictionary<nint, ObjectKind>();
|
|
||||||
lock (_playerRelatedLock)
|
|
||||||
{
|
{
|
||||||
foreach (var handler in _playerRelatedPointers)
|
resolvedKind = ownedKind;
|
||||||
{
|
return true;
|
||||||
var address = (nint)handler.Address;
|
|
||||||
if (address != nint.Zero)
|
|
||||||
{
|
|
||||||
_playerRelatedByAddress[address] = handler;
|
|
||||||
updatedFrameAddresses[address] = handler.ObjectKind;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_cachedFrameAddresses = updatedFrameAddresses;
|
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)
|
||||||
@@ -374,15 +375,18 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
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)
|
||||||
{
|
{
|
||||||
@@ -461,84 +465,53 @@ 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 gameObjectAddress = msg.GameObject;
|
var gameObjectAddress = msg.GameObject;
|
||||||
if (!_cachedFrameAddresses.TryGetValue(gameObjectAddress, out var objectKind))
|
var filePath = msg.FilePath;
|
||||||
{
|
|
||||||
if (_actorObjectService.TryGetOwnedKind(gameObjectAddress, out var ownedKind))
|
|
||||||
{
|
|
||||||
objectKind = ownedKind;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var gamePath = NormalizeGamePath(msg.GamePath);
|
|
||||||
if (string.IsNullOrEmpty(gamePath))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore files already processed this frame
|
// ignore files already processed this frame
|
||||||
|
if (_cachedHandledPaths.Contains(gamePath)) return;
|
||||||
|
|
||||||
lock (_cacheAdditionLock)
|
lock (_cacheAdditionLock)
|
||||||
{
|
{
|
||||||
if (!_cachedHandledPaths.Add(gamePath))
|
_cachedHandledPaths.Add(gamePath);
|
||||||
{
|
}
|
||||||
return;
|
|
||||||
}
|
// replace individual mtrl stuff
|
||||||
|
if (filePath.StartsWith("|", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
filePath = filePath.Split("|")[2];
|
||||||
|
}
|
||||||
|
// replace filepath
|
||||||
|
filePath = filePath.ToLowerInvariant().Replace("\\", "/", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// ignore files that are the same
|
||||||
|
var replacedGamePath = gamePath.ToLowerInvariant().Replace("\\", "/", StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (string.Equals(filePath, replacedGamePath, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore files to not handle
|
// ignore files to not handle
|
||||||
var handledTypes = IsTransientRecording ? _handledFileTypesWithRecording : _handledFileTypes;
|
var handledTypes = IsTransientRecording ? _handledRecordingFileTypes.Concat(_handledFileTypes) : _handledFileTypes;
|
||||||
if (!HasHandledFileType(gamePath, handledTypes))
|
if (!handledTypes.Any(type => gamePath.EndsWith(type, StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
|
lock (_cacheAdditionLock)
|
||||||
|
{
|
||||||
|
_cachedHandledPaths.Add(gamePath);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var filePath = NormalizeFilePath(msg.FilePath);
|
// ignore files not belonging to anything player related
|
||||||
|
if (!_cachedFrameAddresses.TryGetValue(gameObjectAddress, out var objectKind))
|
||||||
// ignore files that are the same
|
|
||||||
if (string.Equals(filePath, gamePath, StringComparison.Ordinal))
|
|
||||||
{
|
{
|
||||||
|
lock (_cacheAdditionLock)
|
||||||
|
{
|
||||||
|
_cachedHandledPaths.Add(gamePath);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,15 +523,15 @@ 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(gamePath);
|
bool transientContains = transientResources.Contains(replacedGamePath);
|
||||||
bool semiTransientContains = SemiTransientResources.Values.Any(value => value.Contains(gamePath));
|
bool semiTransientContains = SemiTransientResources.SelectMany(k => k.Value).Any(f => string.Equals(f, gamePath, StringComparison.OrdinalIgnoreCase));
|
||||||
if (transientContains || semiTransientContains)
|
if (transientContains || semiTransientContains)
|
||||||
{
|
{
|
||||||
if (!IsTransientRecording)
|
if (!IsTransientRecording)
|
||||||
Logger.LogTrace("Not adding {replacedPath} => {filePath}, Reason: Transient: {contains}, SemiTransient: {contains2}", gamePath, filePath,
|
Logger.LogTrace("Not adding {replacedPath} => {filePath}, Reason: Transient: {contains}, SemiTransient: {contains2}", replacedGamePath, filePath,
|
||||||
transientContains, semiTransientContains);
|
transientContains, semiTransientContains);
|
||||||
alreadyTransient = true;
|
alreadyTransient = true;
|
||||||
}
|
}
|
||||||
@@ -566,10 +539,10 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (!IsTransientRecording)
|
if (!IsTransientRecording)
|
||||||
{
|
{
|
||||||
bool isAdded = transientResources.Add(gamePath);
|
bool isAdded = transientResources.Add(replacedGamePath);
|
||||||
if (isAdded)
|
if (isAdded)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", gamePath, owner?.ToString() ?? gameObjectAddress.ToString("X"), filePath);
|
Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", replacedGamePath, owner?.ToString() ?? gameObjectAddress.ToString("X"), filePath);
|
||||||
SendTransients(gameObjectAddress, objectKind);
|
SendTransients(gameObjectAddress, objectKind);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -577,7 +550,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (owner != null && IsTransientRecording)
|
if (owner != null && IsTransientRecording)
|
||||||
{
|
{
|
||||||
_recordedTransients.Add(new TransientRecord(owner, gamePath, filePath, alreadyTransient) { AddTransient = !alreadyTransient });
|
_recordedTransients.Add(new TransientRecord(owner, replacedGamePath, filePath, alreadyTransient) { AddTransient = !alreadyTransient });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -649,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);
|
||||||
|
|||||||
@@ -95,12 +95,6 @@ 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);
|
||||||
|
|
||||||
@@ -177,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()
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ public sealed class PenumbraCollections : PenumbraBase
|
|||||||
_activeTemporaryCollections.TryRemove(collectionId, out _);
|
_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)
|
||||||
{
|
{
|
||||||
@@ -109,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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
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;
|
||||||
@@ -14,11 +15,10 @@ public sealed class PenumbraResource : PenumbraBase
|
|||||||
{
|
{
|
||||||
private readonly ActorObjectService _actorObjectService;
|
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,
|
||||||
@@ -29,11 +29,14 @@ public sealed class PenumbraResource : PenumbraBase
|
|||||||
{
|
{
|
||||||
_actorObjectService = actorObjectService;
|
_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";
|
||||||
@@ -71,34 +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;
|
{
|
||||||
|
if (address != nint.Zero)
|
||||||
|
{
|
||||||
|
_trackedActors[(IntPtr)address] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public string[] ReverseResolveGameObjectPath(string moddedPath, int gameObjectIndex)
|
public void UntrackActor(nint address)
|
||||||
=> IsAvailable ? _reverseResolveGameObjectPath.Invoke(moddedPath, gameObjectIndex) : Array.Empty<string>();
|
{
|
||||||
|
if (address != nint.Zero)
|
||||||
|
{
|
||||||
|
_trackedActors.TryRemove((IntPtr)address, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void HandleResourceLoaded(nint ptr, string gamePath, string resolvedPath)
|
private void HandleResourceLoaded(nint ptr, string resolvedPath, string gamePath)
|
||||||
{
|
{
|
||||||
if (ptr == nint.Zero)
|
if (ptr == nint.Zero)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_actorObjectService.TryGetOwnedKind(ptr, out _))
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Compare(gamePath, resolvedPath, StringComparison.OrdinalIgnoreCase) == 0)
|
Mediator.Publish(new PenumbraResourceLoadMessage(ptr, resolvedPath, gamePath));
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Mediator.Publish(new PenumbraResourceLoadMessage(ptr, gamePath, resolvedPath));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
|
|||||||
@@ -19,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");
|
||||||
|
|||||||
@@ -10,7 +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 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;
|
||||||
|
|||||||
@@ -140,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;
|
||||||
@@ -155,5 +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 HashSet<Guid> OrphanableTempCollections { 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors></Authors>
|
<Authors></Authors>
|
||||||
<Company></Company>
|
<Company></Company>
|
||||||
<Version>2.0.1.70</Version>
|
<Version>2.0.0</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>
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ public class PlayerDataFactory
|
|||||||
|
|
||||||
// get all remaining paths and resolve them
|
// get all remaining paths and resolve them
|
||||||
var transientPaths = ManageSemiTransientData(objectKind);
|
var transientPaths = ManageSemiTransientData(objectKind);
|
||||||
var resolvedTransientPaths = await GetFileReplacementsFromPaths(playerRelatedObject, transientPaths, new HashSet<string>(StringComparer.Ordinal)).ConfigureAwait(false);
|
var resolvedTransientPaths = await GetFileReplacementsFromPaths(transientPaths, new HashSet<string>(StringComparer.Ordinal)).ConfigureAwait(false);
|
||||||
|
|
||||||
if (logDebug)
|
if (logDebug)
|
||||||
{
|
{
|
||||||
@@ -373,73 +373,11 @@ public class PlayerDataFactory
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IReadOnlyDictionary<string, string[]>> GetFileReplacementsFromPaths(GameObjectHandler handler, 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();
|
||||||
Dictionary<string, List<string>> resolvedPaths = new(StringComparer.Ordinal);
|
Dictionary<string, List<string>> resolvedPaths = new(StringComparer.Ordinal);
|
||||||
if (handler.ObjectKind != ObjectKind.Player)
|
|
||||||
{
|
|
||||||
var (objectIndex, forwardResolved, reverseResolved) = await _dalamudUtil.RunOnFrameworkThread(() =>
|
|
||||||
{
|
|
||||||
var idx = handler.GetGameObject()?.ObjectIndex;
|
|
||||||
if (!idx.HasValue)
|
|
||||||
{
|
|
||||||
return ((int?)null, Array.Empty<string>(), Array.Empty<string[]>());
|
|
||||||
}
|
|
||||||
|
|
||||||
var resolvedForward = new string[forwardPaths.Length];
|
|
||||||
for (int i = 0; i < forwardPaths.Length; i++)
|
|
||||||
{
|
|
||||||
resolvedForward[i] = _ipcManager.Penumbra.ResolveGameObjectPath(forwardPaths[i], idx.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
var resolvedReverse = new string[reversePaths.Length][];
|
|
||||||
for (int i = 0; i < reversePaths.Length; i++)
|
|
||||||
{
|
|
||||||
resolvedReverse[i] = _ipcManager.Penumbra.ReverseResolveGameObjectPath(reversePaths[i], idx.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (idx, resolvedForward, resolvedReverse);
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (objectIndex.HasValue)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < forwardPaths.Length; i++)
|
|
||||||
{
|
|
||||||
var filePath = forwardResolved[i]?.ToLowerInvariant();
|
|
||||||
if (string.IsNullOrEmpty(filePath))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resolvedPaths.TryGetValue(filePath, out var list))
|
|
||||||
{
|
|
||||||
list.Add(forwardPaths[i].ToLowerInvariant());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
resolvedPaths[filePath] = [forwardPaths[i].ToLowerInvariant()];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < reversePaths.Length; i++)
|
|
||||||
{
|
|
||||||
var filePath = reversePaths[i].ToLowerInvariant();
|
|
||||||
if (resolvedPaths.TryGetValue(filePath, out var list))
|
|
||||||
{
|
|
||||||
list.AddRange(reverseResolved[i].Select(c => c.ToLowerInvariant()));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
resolvedPaths[filePath] = new List<string>(reverseResolved[i].Select(c => c.ToLowerInvariant()).ToList());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolvedPaths.ToDictionary(k => k.Key, k => k.Value.ToArray(), StringComparer.OrdinalIgnoreCase).AsReadOnly();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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++)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,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;
|
||||||
@@ -49,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,7 +109,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
{
|
{
|
||||||
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)
|
||||||
@@ -153,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);
|
||||||
@@ -371,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)
|
||||||
{
|
{
|
||||||
@@ -383,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;
|
||||||
|
|||||||
@@ -25,11 +25,6 @@
|
|||||||
bool IsDownloading { get; }
|
bool IsDownloading { get; }
|
||||||
int PendingDownloadCount { get; }
|
int PendingDownloadCount { get; }
|
||||||
int ForbiddenDownloadCount { get; }
|
int ForbiddenDownloadCount { get; }
|
||||||
bool PendingModReapply { get; }
|
|
||||||
bool ModApplyDeferred { get; }
|
|
||||||
int MissingCriticalMods { get; }
|
|
||||||
int MissingNonCriticalMods { get; }
|
|
||||||
int MissingForbiddenMods { get; }
|
|
||||||
DateTime? InvisibleSinceUtc { get; }
|
DateTime? InvisibleSinceUtc { get; }
|
||||||
DateTime? VisibilityEvictionDueAtUtc { get; }
|
DateTime? VisibilityEvictionDueAtUtc { get; }
|
||||||
|
|
||||||
|
|||||||
@@ -87,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: () =>
|
||||||
{
|
{
|
||||||
@@ -113,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;
|
||||||
@@ -238,11 +218,6 @@ public class Pair
|
|||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,12 +16,7 @@ public sealed record PairDebugInfo(
|
|||||||
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,
|
||||||
@@ -39,10 +34,5 @@ public sealed record PairDebugInfo(
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
0,
|
0,
|
||||||
0,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0);
|
0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ using LightlessSync.Interop.Ipc;
|
|||||||
using LightlessSync.PlayerData.Factories;
|
using LightlessSync.PlayerData.Factories;
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.ActorTracking;
|
|
||||||
using LightlessSync.Services.Events;
|
using LightlessSync.Services.Events;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Services.PairProcessing;
|
using LightlessSync.Services.PairProcessing;
|
||||||
@@ -19,7 +18,6 @@ using LightlessSync.WebAPI.Files;
|
|||||||
using LightlessSync.WebAPI.Files.Models;
|
using LightlessSync.WebAPI.Files.Models;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
|
||||||
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||||
using FileReplacementDataComparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer;
|
using FileReplacementDataComparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer;
|
||||||
|
|
||||||
@@ -33,7 +31,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
private sealed record CombatData(Guid ApplicationId, CharacterData CharacterData, bool Forced);
|
private sealed record CombatData(Guid ApplicationId, CharacterData CharacterData, bool Forced);
|
||||||
|
|
||||||
private readonly DalamudUtilService _dalamudUtil;
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
private readonly ActorObjectService _actorObjectService;
|
|
||||||
private readonly FileDownloadManager _downloadManager;
|
private readonly FileDownloadManager _downloadManager;
|
||||||
private readonly FileCacheManager _fileDbManager;
|
private readonly FileCacheManager _fileDbManager;
|
||||||
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
||||||
@@ -46,7 +43,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
private readonly TextureDownscaleService _textureDownscaleService;
|
private readonly TextureDownscaleService _textureDownscaleService;
|
||||||
private readonly PairStateCache _pairStateCache;
|
private readonly PairStateCache _pairStateCache;
|
||||||
private readonly PairPerformanceMetricsCache _performanceMetricsCache;
|
private readonly PairPerformanceMetricsCache _performanceMetricsCache;
|
||||||
private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor;
|
|
||||||
private readonly PairManager _pairManager;
|
private readonly PairManager _pairManager;
|
||||||
private CancellationTokenSource? _applicationCancellationTokenSource;
|
private CancellationTokenSource? _applicationCancellationTokenSource;
|
||||||
private Guid _applicationId;
|
private Guid _applicationId;
|
||||||
@@ -60,16 +56,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
private bool _forceFullReapply;
|
private bool _forceFullReapply;
|
||||||
private Dictionary<(string GamePath, string? Hash), string>? _lastAppliedModdedPaths;
|
private Dictionary<(string GamePath, string? Hash), string>? _lastAppliedModdedPaths;
|
||||||
private bool _needsCollectionRebuild;
|
private bool _needsCollectionRebuild;
|
||||||
private bool _pendingModReapply;
|
|
||||||
private bool _lastModApplyDeferred;
|
|
||||||
private int _lastMissingCriticalMods;
|
|
||||||
private int _lastMissingNonCriticalMods;
|
|
||||||
private int _lastMissingForbiddenMods;
|
|
||||||
private bool _lastMissingCachedFiles;
|
|
||||||
private bool _isVisible;
|
private bool _isVisible;
|
||||||
private Guid _penumbraCollection;
|
private Guid _penumbraCollection;
|
||||||
private readonly object _collectionGate = new();
|
private readonly object _collectionGate = new();
|
||||||
private bool _redrawOnNextApplication = false;
|
private bool _redrawOnNextApplication = false;
|
||||||
|
private bool _explicitRedrawQueued;
|
||||||
private readonly object _initializationGate = new();
|
private readonly object _initializationGate = new();
|
||||||
private readonly object _pauseLock = new();
|
private readonly object _pauseLock = new();
|
||||||
private Task _pauseTransitionTask = Task.CompletedTask;
|
private Task _pauseTransitionTask = Task.CompletedTask;
|
||||||
@@ -82,23 +73,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
private readonly object _visibilityGraceGate = new();
|
private readonly object _visibilityGraceGate = new();
|
||||||
private CancellationTokenSource? _visibilityGraceCts;
|
private CancellationTokenSource? _visibilityGraceCts;
|
||||||
private static readonly TimeSpan VisibilityEvictionGrace = TimeSpan.FromMinutes(1);
|
private static readonly TimeSpan VisibilityEvictionGrace = TimeSpan.FromMinutes(1);
|
||||||
private static readonly HashSet<string> NonPriorityModExtensions = new(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{
|
|
||||||
".tmb",
|
|
||||||
".pap",
|
|
||||||
".atex",
|
|
||||||
".avfx",
|
|
||||||
".scd"
|
|
||||||
};
|
|
||||||
private DateTime? _invisibleSinceUtc;
|
private DateTime? _invisibleSinceUtc;
|
||||||
private DateTime? _visibilityEvictionDueAtUtc;
|
private DateTime? _visibilityEvictionDueAtUtc;
|
||||||
private DateTime _nextActorLookupUtc = DateTime.MinValue;
|
|
||||||
private static readonly TimeSpan ActorLookupInterval = TimeSpan.FromSeconds(1);
|
|
||||||
private static readonly SemaphoreSlim ActorInitializationLimiter = new(1, 1);
|
|
||||||
private readonly object _actorInitializationGate = new();
|
|
||||||
private ActorObjectService.ActorDescriptor? _pendingActorDescriptor;
|
|
||||||
private bool _actorInitializationInProgress;
|
|
||||||
private bool _frameworkUpdateSubscribed;
|
|
||||||
|
|
||||||
public DateTime? InvisibleSinceUtc => _invisibleSinceUtc;
|
public DateTime? InvisibleSinceUtc => _invisibleSinceUtc;
|
||||||
public DateTime? VisibilityEvictionDueAtUtc => _visibilityEvictionDueAtUtc;
|
public DateTime? VisibilityEvictionDueAtUtc => _visibilityEvictionDueAtUtc;
|
||||||
@@ -150,11 +126,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
public long LastAppliedApproximateVRAMBytes { get; set; } = -1;
|
public long LastAppliedApproximateVRAMBytes { get; set; } = -1;
|
||||||
public long LastAppliedApproximateEffectiveVRAMBytes { get; set; } = -1;
|
public long LastAppliedApproximateEffectiveVRAMBytes { get; set; } = -1;
|
||||||
public CharacterData? LastReceivedCharacterData { get; private set; }
|
public CharacterData? LastReceivedCharacterData { get; private set; }
|
||||||
public bool PendingModReapply => _pendingModReapply;
|
|
||||||
public bool ModApplyDeferred => _lastModApplyDeferred;
|
|
||||||
public int MissingCriticalMods => _lastMissingCriticalMods;
|
|
||||||
public int MissingNonCriticalMods => _lastMissingNonCriticalMods;
|
|
||||||
public int MissingForbiddenMods => _lastMissingForbiddenMods;
|
|
||||||
public DateTime? LastDataReceivedAt => _lastDataReceivedAt;
|
public DateTime? LastDataReceivedAt => _lastDataReceivedAt;
|
||||||
public DateTime? LastApplyAttemptAt => _lastApplyAttemptAt;
|
public DateTime? LastApplyAttemptAt => _lastApplyAttemptAt;
|
||||||
public DateTime? LastSuccessfulApplyAt => _lastSuccessfulApplyAt;
|
public DateTime? LastSuccessfulApplyAt => _lastSuccessfulApplyAt;
|
||||||
@@ -175,7 +146,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
FileDownloadManager transferManager,
|
FileDownloadManager transferManager,
|
||||||
PluginWarningNotificationService pluginWarningNotificationManager,
|
PluginWarningNotificationService pluginWarningNotificationManager,
|
||||||
DalamudUtilService dalamudUtil,
|
DalamudUtilService dalamudUtil,
|
||||||
ActorObjectService actorObjectService,
|
|
||||||
IHostApplicationLifetime lifetime,
|
IHostApplicationLifetime lifetime,
|
||||||
FileCacheManager fileDbManager,
|
FileCacheManager fileDbManager,
|
||||||
PlayerPerformanceService playerPerformanceService,
|
PlayerPerformanceService playerPerformanceService,
|
||||||
@@ -183,8 +153,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
ServerConfigurationManager serverConfigManager,
|
ServerConfigurationManager serverConfigManager,
|
||||||
TextureDownscaleService textureDownscaleService,
|
TextureDownscaleService textureDownscaleService,
|
||||||
PairStateCache pairStateCache,
|
PairStateCache pairStateCache,
|
||||||
PairPerformanceMetricsCache performanceMetricsCache,
|
PairPerformanceMetricsCache performanceMetricsCache) : base(logger, mediator)
|
||||||
PenumbraTempCollectionJanitor tempCollectionJanitor) : base(logger, mediator)
|
|
||||||
{
|
{
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
Ident = ident;
|
Ident = ident;
|
||||||
@@ -193,7 +162,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
_downloadManager = transferManager;
|
_downloadManager = transferManager;
|
||||||
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
||||||
_dalamudUtil = dalamudUtil;
|
_dalamudUtil = dalamudUtil;
|
||||||
_actorObjectService = actorObjectService;
|
|
||||||
_lifetime = lifetime;
|
_lifetime = lifetime;
|
||||||
_fileDbManager = fileDbManager;
|
_fileDbManager = fileDbManager;
|
||||||
_playerPerformanceService = playerPerformanceService;
|
_playerPerformanceService = playerPerformanceService;
|
||||||
@@ -202,7 +170,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
_textureDownscaleService = textureDownscaleService;
|
_textureDownscaleService = textureDownscaleService;
|
||||||
_pairStateCache = pairStateCache;
|
_pairStateCache = pairStateCache;
|
||||||
_performanceMetricsCache = performanceMetricsCache;
|
_performanceMetricsCache = performanceMetricsCache;
|
||||||
_tempCollectionJanitor = tempCollectionJanitor;
|
LastAppliedDataBytes = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Initialize()
|
public void Initialize()
|
||||||
@@ -217,7 +185,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ActorObjectService.ActorDescriptor? trackedDescriptor = null;
|
|
||||||
lock (_initializationGate)
|
lock (_initializationGate)
|
||||||
{
|
{
|
||||||
if (Initialized)
|
if (Initialized)
|
||||||
@@ -231,12 +198,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
_forceApplyMods = true;
|
_forceApplyMods = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var useFrameworkUpdate = !_actorObjectService.HooksActive;
|
Mediator.Subscribe<FrameworkUpdateMessage>(this, _ => FrameworkUpdate());
|
||||||
if (useFrameworkUpdate)
|
|
||||||
{
|
|
||||||
Mediator.Subscribe<FrameworkUpdateMessage>(this, _ => FrameworkUpdate());
|
|
||||||
_frameworkUpdateSubscribed = true;
|
|
||||||
}
|
|
||||||
Mediator.Subscribe<ZoneSwitchStartMessage>(this, _ =>
|
Mediator.Subscribe<ZoneSwitchStartMessage>(this, _ =>
|
||||||
{
|
{
|
||||||
_downloadCancellationTokenSource?.CancelDispose();
|
_downloadCancellationTokenSource?.CancelDispose();
|
||||||
@@ -272,49 +234,17 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
Mediator.Subscribe<CutsceneEndMessage>(this, _ => EnableSync());
|
Mediator.Subscribe<CutsceneEndMessage>(this, _ => EnableSync());
|
||||||
Mediator.Subscribe<GposeStartMessage>(this, _ => DisableSync());
|
Mediator.Subscribe<GposeStartMessage>(this, _ => DisableSync());
|
||||||
Mediator.Subscribe<GposeEndMessage>(this, _ => EnableSync());
|
Mediator.Subscribe<GposeEndMessage>(this, _ => EnableSync());
|
||||||
Mediator.Subscribe<ActorTrackedMessage>(this, msg => HandleActorTracked(msg.Descriptor));
|
Mediator.Subscribe<DownloadFinishedMessage>(this, msg =>
|
||||||
Mediator.Subscribe<ActorUntrackedMessage>(this, msg => HandleActorUntracked(msg.Descriptor));
|
{
|
||||||
Mediator.Subscribe<DownloadFinishedMessage>(this, msg =>
|
if (_charaHandler is null || !ReferenceEquals(msg.DownloadId, _charaHandler))
|
||||||
{
|
{
|
||||||
if (_charaHandler is null || !ReferenceEquals(msg.DownloadId, _charaHandler))
|
return;
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_pendingModReapply && IsVisible)
|
|
||||||
{
|
|
||||||
if (LastReceivedCharacterData is not null)
|
|
||||||
{
|
|
||||||
Logger.LogDebug("Downloads finished for {handler}, reapplying pending mod data", GetLogIdentifier());
|
|
||||||
ApplyLastReceivedData(forced: true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_cachedData is not null)
|
|
||||||
{
|
|
||||||
Logger.LogDebug("Downloads finished for {handler}, reapplying pending mod data from cache", GetLogIdentifier());
|
|
||||||
ApplyCharacterData(Guid.NewGuid(), _cachedData, forceApplyCustomization: true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TryApplyQueuedData();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!useFrameworkUpdate
|
|
||||||
&& _actorObjectService.TryGetActorByHash(Ident, out var descriptor)
|
|
||||||
&& descriptor.Address != nint.Zero)
|
|
||||||
{
|
|
||||||
trackedDescriptor = descriptor;
|
|
||||||
}
|
}
|
||||||
|
TryApplyQueuedData();
|
||||||
|
});
|
||||||
|
|
||||||
Initialized = true;
|
Initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackedDescriptor.HasValue)
|
|
||||||
{
|
|
||||||
HandleActorTracked(trackedDescriptor.Value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IReadOnlyList<PairConnection> GetCurrentPairs()
|
private IReadOnlyList<PairConnection> GetCurrentPairs()
|
||||||
@@ -425,7 +355,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
{
|
{
|
||||||
_penumbraCollection = created;
|
_penumbraCollection = created;
|
||||||
_pairStateCache.StoreTemporaryCollection(Ident, created);
|
_pairStateCache.StoreTemporaryCollection(Ident, created);
|
||||||
_tempCollectionJanitor.Register(created);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return _penumbraCollection;
|
return _penumbraCollection;
|
||||||
@@ -458,7 +387,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
_needsCollectionRebuild = true;
|
_needsCollectionRebuild = true;
|
||||||
_forceFullReapply = true;
|
_forceFullReapply = true;
|
||||||
_forceApplyMods = true;
|
_forceApplyMods = true;
|
||||||
_tempCollectionJanitor.Unregister(toRelease);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!releaseFromPenumbra || toRelease == Guid.Empty || !_ipcManager.Penumbra.APIAvailable)
|
if (!releaseFromPenumbra || toRelease == Guid.Empty || !_ipcManager.Penumbra.APIAvailable)
|
||||||
@@ -558,10 +486,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasMissingCachedFiles = HasMissingCachedFiles(LastReceivedCharacterData);
|
var shouldForce = forced || HasMissingCachedFiles(LastReceivedCharacterData);
|
||||||
var missingResolved = _lastMissingCachedFiles && !hasMissingCachedFiles;
|
|
||||||
_lastMissingCachedFiles = hasMissingCachedFiles;
|
|
||||||
var shouldForce = forced || missingResolved;
|
|
||||||
|
|
||||||
if (IsPaused())
|
if (IsPaused())
|
||||||
{
|
{
|
||||||
@@ -704,7 +629,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(replacement.FileSwapPath))
|
if (!string.IsNullOrEmpty(replacement.FileSwapPath))
|
||||||
{
|
{
|
||||||
if (Path.IsPathRooted(replacement.FileSwapPath) && !File.Exists(replacement.FileSwapPath))
|
if (!File.Exists(replacement.FileSwapPath))
|
||||||
{
|
{
|
||||||
Logger.LogTrace("Missing file swap path {Path} detected for {Handler}", replacement.FileSwapPath, GetLogIdentifier());
|
Logger.LogTrace("Missing file swap path {Path} detected for {Handler}", replacement.FileSwapPath, GetLogIdentifier());
|
||||||
return true;
|
return true;
|
||||||
@@ -812,67 +737,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsForbiddenHash(string hash)
|
|
||||||
=> _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, hash, StringComparison.Ordinal));
|
|
||||||
|
|
||||||
private static bool IsNonPriorityModPath(string? gamePath)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(gamePath))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var extension = Path.GetExtension(gamePath);
|
|
||||||
return !string.IsNullOrEmpty(extension) && NonPriorityModExtensions.Contains(extension);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsCriticalModReplacement(FileReplacementData replacement)
|
|
||||||
{
|
|
||||||
foreach (var gamePath in replacement.GamePaths)
|
|
||||||
{
|
|
||||||
if (!IsNonPriorityModPath(gamePath))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CountMissingReplacements(IEnumerable<FileReplacementData> missing, out int critical, out int nonCritical, out int forbidden)
|
|
||||||
{
|
|
||||||
critical = 0;
|
|
||||||
nonCritical = 0;
|
|
||||||
forbidden = 0;
|
|
||||||
|
|
||||||
foreach (var replacement in missing)
|
|
||||||
{
|
|
||||||
if (IsForbiddenHash(replacement.Hash))
|
|
||||||
{
|
|
||||||
forbidden++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsCriticalModReplacement(replacement))
|
|
||||||
{
|
|
||||||
critical++;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
nonCritical++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void RemoveModApplyChanges(Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData)
|
|
||||||
{
|
|
||||||
foreach (var changes in updatedData.Values)
|
|
||||||
{
|
|
||||||
changes.Remove(PlayerChanges.ModFiles);
|
|
||||||
changes.Remove(PlayerChanges.ModManip);
|
|
||||||
changes.Remove(PlayerChanges.ForcedRedraw);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CanApplyNow()
|
private bool CanApplyNow()
|
||||||
{
|
{
|
||||||
return !_dalamudUtil.IsInCombat
|
return !_dalamudUtil.IsInCombat
|
||||||
@@ -896,16 +760,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
_lastBlockingConditions = Array.Empty<string>();
|
_lastBlockingConditions = Array.Empty<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DeferApplication(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization, UserData user, string reason,
|
|
||||||
string failureKey, LogLevel logLevel, string logMessage, params object?[] logArgs)
|
|
||||||
{
|
|
||||||
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning, reason)));
|
|
||||||
Logger.Log(logLevel, logMessage, logArgs);
|
|
||||||
RecordFailure(reason, failureKey);
|
|
||||||
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
|
||||||
SetUploading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false)
|
public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false)
|
||||||
{
|
{
|
||||||
_lastApplyAttemptAt = DateTime.UtcNow;
|
_lastApplyAttemptAt = DateTime.UtcNow;
|
||||||
@@ -923,48 +777,72 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
if (_dalamudUtil.IsInCombat)
|
if (_dalamudUtil.IsInCombat)
|
||||||
{
|
{
|
||||||
const string reason = "Cannot apply character data: you are in combat, deferring application";
|
const string reason = "Cannot apply character data: you are in combat, deferring application";
|
||||||
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Combat", LogLevel.Debug,
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
||||||
"[BASE-{appBase}] Received data but player is in combat", applicationBase);
|
reason)));
|
||||||
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is in combat", applicationBase);
|
||||||
|
RecordFailure(reason, "Combat");
|
||||||
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||||
|
SetUploading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_dalamudUtil.IsPerforming)
|
if (_dalamudUtil.IsPerforming)
|
||||||
{
|
{
|
||||||
const string reason = "Cannot apply character data: you are performing music, deferring application";
|
const string reason = "Cannot apply character data: you are performing music, deferring application";
|
||||||
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Performance", LogLevel.Debug,
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
||||||
"[BASE-{appBase}] Received data but player is performing", applicationBase);
|
reason)));
|
||||||
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is performing", applicationBase);
|
||||||
|
RecordFailure(reason, "Performance");
|
||||||
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||||
|
SetUploading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_dalamudUtil.IsInInstance)
|
if (_dalamudUtil.IsInInstance)
|
||||||
{
|
{
|
||||||
const string reason = "Cannot apply character data: you are in an instance, deferring application";
|
const string reason = "Cannot apply character data: you are in an instance, deferring application";
|
||||||
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Instance", LogLevel.Debug,
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
||||||
"[BASE-{appBase}] Received data but player is in instance", applicationBase);
|
reason)));
|
||||||
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is in instance", applicationBase);
|
||||||
|
RecordFailure(reason, "Instance");
|
||||||
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||||
|
SetUploading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_dalamudUtil.IsInCutscene)
|
if (_dalamudUtil.IsInCutscene)
|
||||||
{
|
{
|
||||||
const string reason = "Cannot apply character data: you are in a cutscene, deferring application";
|
const string reason = "Cannot apply character data: you are in a cutscene, deferring application";
|
||||||
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Cutscene", LogLevel.Debug,
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
||||||
"[BASE-{appBase}] Received data but player is in a cutscene", applicationBase);
|
reason)));
|
||||||
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is in a cutscene", applicationBase);
|
||||||
|
RecordFailure(reason, "Cutscene");
|
||||||
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||||
|
SetUploading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_dalamudUtil.IsInGpose)
|
if (_dalamudUtil.IsInGpose)
|
||||||
{
|
{
|
||||||
const string reason = "Cannot apply character data: you are in GPose, deferring application";
|
const string reason = "Cannot apply character data: you are in GPose, deferring application";
|
||||||
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "GPose", LogLevel.Debug,
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
||||||
"[BASE-{appBase}] Received data but player is in GPose", applicationBase);
|
reason)));
|
||||||
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is in GPose", applicationBase);
|
||||||
|
RecordFailure(reason, "GPose");
|
||||||
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||||
|
SetUploading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_ipcManager.Penumbra.APIAvailable || !_ipcManager.Glamourer.APIAvailable)
|
if (!_ipcManager.Penumbra.APIAvailable || !_ipcManager.Glamourer.APIAvailable)
|
||||||
{
|
{
|
||||||
const string reason = "Cannot apply character data: Penumbra or Glamourer is not available, deferring application";
|
const string reason = "Cannot apply character data: Penumbra or Glamourer is not available, deferring application";
|
||||||
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "PluginUnavailable", LogLevel.Information,
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
||||||
"[BASE-{appbase}] Application of data for {player} while Penumbra/Glamourer unavailable, returning", applicationBase, GetLogIdentifier());
|
reason)));
|
||||||
|
Logger.LogInformation("[BASE-{appbase}] Application of data for {player} while Penumbra/Glamourer unavailable, returning", applicationBase, GetLogIdentifier());
|
||||||
|
RecordFailure(reason, "PluginUnavailable");
|
||||||
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||||
|
SetUploading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1007,10 +885,13 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
_forceApplyMods = false;
|
_forceApplyMods = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_explicitRedrawQueued = false;
|
||||||
|
|
||||||
if (_redrawOnNextApplication && charaDataToUpdate.TryGetValue(ObjectKind.Player, out var player))
|
if (_redrawOnNextApplication && charaDataToUpdate.TryGetValue(ObjectKind.Player, out var player))
|
||||||
{
|
{
|
||||||
player.Add(PlayerChanges.ForcedRedraw);
|
player.Add(PlayerChanges.ForcedRedraw);
|
||||||
_redrawOnNextApplication = false;
|
_redrawOnNextApplication = false;
|
||||||
|
_explicitRedrawQueued = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (charaDataToUpdate.TryGetValue(ObjectKind.Player, out var playerChanges))
|
if (charaDataToUpdate.TryGetValue(ObjectKind.Player, out var playerChanges))
|
||||||
@@ -1204,14 +1085,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
|
|
||||||
Logger.LogDebug("[{applicationId}] Applying Customization Data for {handler}", applicationId, handler);
|
Logger.LogDebug("[{applicationId}] Applying Customization Data for {handler}", applicationId, handler);
|
||||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handler, applicationId, 30000, token).ConfigureAwait(false);
|
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handler, applicationId, 30000, token).ConfigureAwait(false);
|
||||||
if (handler.Address != nint.Zero)
|
|
||||||
{
|
|
||||||
await _actorObjectService.WaitForFullyLoadedAsync(handler.Address, token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
var tasks = new List<Task>();
|
|
||||||
bool needsRedraw = false;
|
|
||||||
foreach (var change in changes.Value.OrderBy(p => (int)p))
|
foreach (var change in changes.Value.OrderBy(p => (int)p))
|
||||||
{
|
{
|
||||||
Logger.LogDebug("[{applicationId}] Processing {change} for {handler}", applicationId, change, handler);
|
Logger.LogDebug("[{applicationId}] Processing {change} for {handler}", applicationId, change, handler);
|
||||||
@@ -1220,39 +1094,45 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
case PlayerChanges.Customize:
|
case PlayerChanges.Customize:
|
||||||
if (charaData.CustomizePlusData.TryGetValue(changes.Key, out var customizePlusData))
|
if (charaData.CustomizePlusData.TryGetValue(changes.Key, out var customizePlusData))
|
||||||
{
|
{
|
||||||
tasks.Add(ApplyCustomizeAsync(handler.Address, customizePlusData, changes.Key));
|
_customizeIds[changes.Key] = await _ipcManager.CustomizePlus.SetBodyScaleAsync(handler.Address, customizePlusData).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else if (_customizeIds.TryGetValue(changes.Key, out var customizeId))
|
else if (_customizeIds.TryGetValue(changes.Key, out var customizeId))
|
||||||
{
|
{
|
||||||
tasks.Add(RevertCustomizeAsync(customizeId, changes.Key));
|
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
||||||
|
_customizeIds.Remove(changes.Key);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PlayerChanges.Heels:
|
case PlayerChanges.Heels:
|
||||||
tasks.Add(_ipcManager.Heels.SetOffsetForPlayerAsync(handler.Address, charaData.HeelsData));
|
await _ipcManager.Heels.SetOffsetForPlayerAsync(handler.Address, charaData.HeelsData).ConfigureAwait(false);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PlayerChanges.Honorific:
|
case PlayerChanges.Honorific:
|
||||||
tasks.Add(_ipcManager.Honorific.SetTitleAsync(handler.Address, charaData.HonorificData));
|
await _ipcManager.Honorific.SetTitleAsync(handler.Address, charaData.HonorificData).ConfigureAwait(false);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PlayerChanges.Glamourer:
|
case PlayerChanges.Glamourer:
|
||||||
if (charaData.GlamourerData.TryGetValue(changes.Key, out var glamourerData))
|
if (charaData.GlamourerData.TryGetValue(changes.Key, out var glamourerData))
|
||||||
{
|
{
|
||||||
tasks.Add(_ipcManager.Glamourer.ApplyAllAsync(Logger, handler, glamourerData, applicationId, token));
|
await _ipcManager.Glamourer.ApplyAllAsync(Logger, handler, glamourerData, applicationId, token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PlayerChanges.Moodles:
|
case PlayerChanges.Moodles:
|
||||||
tasks.Add(_ipcManager.Moodles.SetStatusAsync(handler.Address, charaData.MoodlesData));
|
await _ipcManager.Moodles.SetStatusAsync(handler.Address, charaData.MoodlesData).ConfigureAwait(false);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PlayerChanges.PetNames:
|
case PlayerChanges.PetNames:
|
||||||
tasks.Add(_ipcManager.PetNames.SetPlayerData(handler.Address, charaData.PetNamesData));
|
await _ipcManager.PetNames.SetPlayerData(handler.Address, charaData.PetNamesData).ConfigureAwait(false);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PlayerChanges.ForcedRedraw:
|
case PlayerChanges.ForcedRedraw:
|
||||||
needsRedraw = true;
|
if (!ShouldPerformForcedRedraw(changes.Key, changes.Value, charaData))
|
||||||
|
{
|
||||||
|
Logger.LogTrace("[{applicationId}] Skipping forced redraw for {handler}", applicationId, handler);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -1260,16 +1140,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
}
|
}
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tasks.Count > 0)
|
|
||||||
{
|
|
||||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (needsRedraw)
|
|
||||||
{
|
|
||||||
await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -1277,6 +1147,44 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool ShouldPerformForcedRedraw(ObjectKind objectKind, ICollection<PlayerChanges> changeSet, CharacterData newData)
|
||||||
|
{
|
||||||
|
if (objectKind != ObjectKind.Player)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasModFiles = changeSet.Contains(PlayerChanges.ModFiles);
|
||||||
|
var hasManip = changeSet.Contains(PlayerChanges.ModManip);
|
||||||
|
var modsChanged = hasModFiles && PlayerModFilesChanged(newData, _cachedData);
|
||||||
|
var manipChanged = hasManip && !string.Equals(_cachedData?.ManipulationData, newData.ManipulationData, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
if (modsChanged)
|
||||||
|
{
|
||||||
|
_explicitRedrawQueued = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manipChanged)
|
||||||
|
{
|
||||||
|
_explicitRedrawQueued = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_explicitRedrawQueued)
|
||||||
|
{
|
||||||
|
_explicitRedrawQueued = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((hasModFiles || hasManip) && (_forceFullReapply || _needsCollectionRebuild))
|
||||||
|
{
|
||||||
|
_explicitRedrawQueued = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static Dictionary<ObjectKind, HashSet<PlayerChanges>> BuildFullChangeSet(CharacterData characterData)
|
private static Dictionary<ObjectKind, HashSet<PlayerChanges>> BuildFullChangeSet(CharacterData characterData)
|
||||||
{
|
{
|
||||||
@@ -1431,7 +1339,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
bool skipDownscaleForPair = ShouldSkipDownscale();
|
bool skipDownscaleForPair = ShouldSkipDownscale();
|
||||||
var user = GetPrimaryUserData();
|
var user = GetPrimaryUserData();
|
||||||
Dictionary<(string GamePath, string? Hash), string> moddedPaths;
|
Dictionary<(string GamePath, string? Hash), string> moddedPaths;
|
||||||
List<FileReplacementData> missingReplacements = [];
|
|
||||||
|
|
||||||
if (updateModdedPaths)
|
if (updateModdedPaths)
|
||||||
{
|
{
|
||||||
@@ -1443,7 +1350,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
{
|
{
|
||||||
int attempts = 0;
|
int attempts = 0;
|
||||||
List<FileReplacementData> toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
List<FileReplacementData> toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
||||||
missingReplacements = toDownloadReplacements;
|
|
||||||
|
|
||||||
while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested)
|
while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
@@ -1493,7 +1399,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
}
|
}
|
||||||
|
|
||||||
toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
||||||
missingReplacements = toDownloadReplacements;
|
|
||||||
|
|
||||||
if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal))))
|
if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal))))
|
||||||
{
|
{
|
||||||
@@ -1517,54 +1422,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
: [];
|
: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
var wantsModApply = updateModdedPaths || updateManip;
|
|
||||||
var pendingModReapply = false;
|
|
||||||
var deferModApply = false;
|
|
||||||
|
|
||||||
if (wantsModApply && missingReplacements.Count > 0)
|
|
||||||
{
|
|
||||||
CountMissingReplacements(missingReplacements, out var missingCritical, out var missingNonCritical, out var missingForbidden);
|
|
||||||
_lastMissingCriticalMods = missingCritical;
|
|
||||||
_lastMissingNonCriticalMods = missingNonCritical;
|
|
||||||
_lastMissingForbiddenMods = missingForbidden;
|
|
||||||
|
|
||||||
var hasCriticalMissing = missingCritical > 0;
|
|
||||||
var hasNonCriticalMissing = missingNonCritical > 0;
|
|
||||||
var hasDownloadableMissing = missingReplacements.Any(replacement => !IsForbiddenHash(replacement.Hash));
|
|
||||||
var hasDownloadableCriticalMissing = hasCriticalMissing
|
|
||||||
&& missingReplacements.Any(replacement => !IsForbiddenHash(replacement.Hash) && IsCriticalModReplacement(replacement));
|
|
||||||
|
|
||||||
pendingModReapply = hasDownloadableMissing;
|
|
||||||
_lastModApplyDeferred = false;
|
|
||||||
|
|
||||||
if (hasDownloadableCriticalMissing)
|
|
||||||
{
|
|
||||||
deferModApply = true;
|
|
||||||
_lastModApplyDeferred = true;
|
|
||||||
Logger.LogDebug("[BASE-{appBase}] Critical mod files missing for {handler}, deferring mod apply ({count} missing)",
|
|
||||||
applicationBase, GetLogIdentifier(), missingReplacements.Count);
|
|
||||||
}
|
|
||||||
else if (hasNonCriticalMissing && hasDownloadableMissing)
|
|
||||||
{
|
|
||||||
Logger.LogDebug("[BASE-{appBase}] Non-critical mod files missing for {handler}, applying partial mods and reapplying after downloads ({count} missing)",
|
|
||||||
applicationBase, GetLogIdentifier(), missingReplacements.Count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_lastMissingCriticalMods = 0;
|
|
||||||
_lastMissingNonCriticalMods = 0;
|
|
||||||
_lastMissingForbiddenMods = 0;
|
|
||||||
_lastModApplyDeferred = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deferModApply)
|
|
||||||
{
|
|
||||||
updateModdedPaths = false;
|
|
||||||
updateManip = false;
|
|
||||||
RemoveModApplyChanges(updatedData);
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadToken.ThrowIfCancellationRequested();
|
downloadToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var handlerForApply = _charaHandler;
|
var handlerForApply = _charaHandler;
|
||||||
@@ -1597,7 +1454,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
|
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
|
||||||
var token = _applicationCancellationTokenSource.Token;
|
var token = _applicationCancellationTokenSource.Token;
|
||||||
|
|
||||||
_applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, wantsModApply, pendingModReapply, token);
|
_applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, token);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -1606,7 +1463,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task ApplyCharacterDataAsync(Guid applicationBase, GameObjectHandler handlerForApply, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData, bool updateModdedPaths, bool updateManip,
|
private async Task ApplyCharacterDataAsync(Guid applicationBase, GameObjectHandler handlerForApply, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData, bool updateModdedPaths, bool updateManip,
|
||||||
Dictionary<(string GamePath, string? Hash), string> moddedPaths, bool wantsModApply, bool pendingModReapply, CancellationToken token)
|
Dictionary<(string GamePath, string? Hash), string> moddedPaths, CancellationToken token)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -1615,10 +1472,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
|
|
||||||
Logger.LogDebug("[{applicationId}] Waiting for initial draw for for {handler}", _applicationId, handlerForApply);
|
Logger.LogDebug("[{applicationId}] Waiting for initial draw for for {handler}", _applicationId, handlerForApply);
|
||||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handlerForApply, _applicationId, 30000, token).ConfigureAwait(false);
|
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handlerForApply, _applicationId, 30000, token).ConfigureAwait(false);
|
||||||
if (handlerForApply.Address != nint.Zero)
|
|
||||||
{
|
|
||||||
await _actorObjectService.WaitForFullyLoadedAsync(handlerForApply.Address, token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
@@ -1685,11 +1538,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
|
|
||||||
_cachedData = charaData;
|
_cachedData = charaData;
|
||||||
_pairStateCache.Store(Ident, charaData);
|
_pairStateCache.Store(Ident, charaData);
|
||||||
if (wantsModApply)
|
_forceFullReapply = false;
|
||||||
{
|
|
||||||
_pendingModReapply = pendingModReapply;
|
|
||||||
}
|
|
||||||
_forceFullReapply = _pendingModReapply;
|
|
||||||
_needsCollectionRebuild = false;
|
_needsCollectionRebuild = false;
|
||||||
if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0)
|
if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0)
|
||||||
{
|
{
|
||||||
@@ -1735,15 +1584,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
|
|
||||||
private void FrameworkUpdate()
|
private void FrameworkUpdate()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(PlayerName) && _charaHandler is null)
|
if (string.IsNullOrEmpty(PlayerName))
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
if (now < _nextActorLookupUtc)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_nextActorLookupUtc = now + ActorLookupInterval;
|
|
||||||
var pc = _dalamudUtil.FindPlayerByNameHash(Ident);
|
var pc = _dalamudUtil.FindPlayerByNameHash(Ident);
|
||||||
if (pc == default((string, nint))) return;
|
if (pc == default((string, nint))) return;
|
||||||
Logger.LogDebug("One-Time Initializing {handler}", GetLogIdentifier());
|
Logger.LogDebug("One-Time Initializing {handler}", GetLogIdentifier());
|
||||||
@@ -1753,11 +1595,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
$"Initializing User For Character {pc.Name}")));
|
$"Initializing User For Character {pc.Name}")));
|
||||||
}
|
}
|
||||||
|
|
||||||
TryHandleVisibilityUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TryHandleVisibilityUpdate()
|
|
||||||
{
|
|
||||||
if (_charaHandler?.Address != nint.Zero && !IsVisible && !_pauseRequested)
|
if (_charaHandler?.Address != nint.Zero && !IsVisible && !_pauseRequested)
|
||||||
{
|
{
|
||||||
Guid appData = Guid.NewGuid();
|
Guid appData = Guid.NewGuid();
|
||||||
@@ -1804,24 +1641,16 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
}
|
}
|
||||||
else if (_charaHandler?.Address == nint.Zero && IsVisible)
|
else if (_charaHandler?.Address == nint.Zero && IsVisible)
|
||||||
{
|
{
|
||||||
HandleVisibilityLoss(logChange: true);
|
IsVisible = false;
|
||||||
|
_charaHandler.Invalidate();
|
||||||
|
_downloadCancellationTokenSource?.CancelDispose();
|
||||||
|
_downloadCancellationTokenSource = null;
|
||||||
|
Logger.LogTrace("{handler} visibility changed, now: {visi}", GetLogIdentifier(), IsVisible);
|
||||||
}
|
}
|
||||||
|
|
||||||
TryApplyQueuedData();
|
TryApplyQueuedData();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleVisibilityLoss(bool logChange)
|
|
||||||
{
|
|
||||||
IsVisible = false;
|
|
||||||
_charaHandler?.Invalidate();
|
|
||||||
_downloadCancellationTokenSource?.CancelDispose();
|
|
||||||
_downloadCancellationTokenSource = null;
|
|
||||||
if (logChange)
|
|
||||||
{
|
|
||||||
Logger.LogTrace("{handler} visibility changed, now: {visi}", GetLogIdentifier(), IsVisible);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Initialize(string name)
|
private void Initialize(string name)
|
||||||
{
|
{
|
||||||
PlayerName = name;
|
PlayerName = name;
|
||||||
@@ -2148,164 +1977,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
}
|
}
|
||||||
|
|
||||||
_dataReceivedInDowntime = null;
|
_dataReceivedInDowntime = null;
|
||||||
_ = Task.Run(() =>
|
ApplyCharacterData(pending.ApplicationId,
|
||||||
{
|
pending.CharacterData, pending.Forced);
|
||||||
try
|
|
||||||
{
|
|
||||||
ApplyCharacterData(pending.ApplicationId, pending.CharacterData, pending.Forced);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Failed applying queued data for {handler}", GetLogIdentifier());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor)
|
|
||||||
{
|
|
||||||
if (!TryResolveDescriptorHash(descriptor, out var hashedCid))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!string.Equals(hashedCid, Ident, StringComparison.Ordinal))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (descriptor.Address == nint.Zero)
|
|
||||||
return;
|
|
||||||
|
|
||||||
RefreshTrackedHandler(descriptor);
|
|
||||||
QueueActorInitialization(descriptor);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void QueueActorInitialization(ActorObjectService.ActorDescriptor descriptor)
|
|
||||||
{
|
|
||||||
lock (_actorInitializationGate)
|
|
||||||
{
|
|
||||||
_pendingActorDescriptor = descriptor;
|
|
||||||
if (_actorInitializationInProgress)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_actorInitializationInProgress = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = Task.Run(InitializeFromTrackedAsync);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task InitializeFromTrackedAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await ActorInitializationLimiter.WaitAsync().ConfigureAwait(false);
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
ActorObjectService.ActorDescriptor? descriptor;
|
|
||||||
lock (_actorInitializationGate)
|
|
||||||
{
|
|
||||||
descriptor = _pendingActorDescriptor;
|
|
||||||
_pendingActorDescriptor = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!descriptor.HasValue)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_frameworkUpdateSubscribed && _actorObjectService.HooksActive)
|
|
||||||
{
|
|
||||||
Mediator.Unsubscribe<FrameworkUpdateMessage>(this);
|
|
||||||
_frameworkUpdateSubscribed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(PlayerName) || _charaHandler is null)
|
|
||||||
{
|
|
||||||
Logger.LogDebug("Actor tracked for {handler}, initializing from hook", GetLogIdentifier());
|
|
||||||
Initialize(descriptor.Value.Name);
|
|
||||||
Mediator.Publish(new EventMessage(new Event(PlayerName, GetPrimaryUserData(), nameof(PairHandlerAdapter), EventSeverity.Informational,
|
|
||||||
$"Initializing User For Character {descriptor.Value.Name}")));
|
|
||||||
}
|
|
||||||
|
|
||||||
RefreshTrackedHandler(descriptor.Value);
|
|
||||||
TryHandleVisibilityUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
ActorInitializationLimiter.Release();
|
|
||||||
lock (_actorInitializationGate)
|
|
||||||
{
|
|
||||||
_actorInitializationInProgress = false;
|
|
||||||
if (_pendingActorDescriptor.HasValue)
|
|
||||||
{
|
|
||||||
_actorInitializationInProgress = true;
|
|
||||||
_ = Task.Run(InitializeFromTrackedAsync);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RefreshTrackedHandler(ActorObjectService.ActorDescriptor descriptor)
|
|
||||||
{
|
|
||||||
if (_charaHandler is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (descriptor.Address == nint.Zero)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (_charaHandler.Address == descriptor.Address)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_charaHandler.Refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleActorUntracked(ActorObjectService.ActorDescriptor descriptor)
|
|
||||||
{
|
|
||||||
if (!TryResolveDescriptorHash(descriptor, out var hashedCid))
|
|
||||||
{
|
|
||||||
if (_charaHandler is null || _charaHandler.Address == nint.Zero)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (descriptor.Address != _charaHandler.Address)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (!string.Equals(hashedCid, Ident, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_charaHandler is null || _charaHandler.Address == nint.Zero)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (descriptor.Address != _charaHandler.Address)
|
|
||||||
return;
|
|
||||||
|
|
||||||
HandleVisibilityLoss(logChange: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryResolveDescriptorHash(ActorObjectService.ActorDescriptor descriptor, out string hashedCid)
|
|
||||||
{
|
|
||||||
hashedCid = descriptor.HashedContentId ?? string.Empty;
|
|
||||||
if (!string.IsNullOrEmpty(hashedCid))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (descriptor.ObjectKind != DalamudObjectKind.Player || descriptor.Address == nint.Zero)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
hashedCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(descriptor.Address);
|
|
||||||
return !string.IsNullOrEmpty(hashedCid);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ApplyCustomizeAsync(nint address, string customizeData, ObjectKind kind)
|
|
||||||
{
|
|
||||||
_customizeIds[kind] = await _ipcManager.CustomizePlus.SetBodyScaleAsync(address, customizeData).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RevertCustomizeAsync(Guid? customizeId, ObjectKind kind)
|
|
||||||
{
|
|
||||||
if (!customizeId.HasValue)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId.Value).ConfigureAwait(false);
|
|
||||||
_customizeIds.Remove(kind);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ using LightlessSync.FileCache;
|
|||||||
using LightlessSync.Interop.Ipc;
|
using LightlessSync.Interop.Ipc;
|
||||||
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.PairProcessing;
|
using LightlessSync.Services.PairProcessing;
|
||||||
using LightlessSync.Services.ServerConfiguration;
|
using LightlessSync.Services.ServerConfiguration;
|
||||||
@@ -31,7 +30,6 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
private readonly TextureDownscaleService _textureDownscaleService;
|
private readonly TextureDownscaleService _textureDownscaleService;
|
||||||
private readonly PairStateCache _pairStateCache;
|
private readonly PairStateCache _pairStateCache;
|
||||||
private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache;
|
private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache;
|
||||||
private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor;
|
|
||||||
|
|
||||||
public PairHandlerAdapterFactory(
|
public PairHandlerAdapterFactory(
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
@@ -49,8 +47,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
ServerConfigurationManager serverConfigManager,
|
ServerConfigurationManager serverConfigManager,
|
||||||
TextureDownscaleService textureDownscaleService,
|
TextureDownscaleService textureDownscaleService,
|
||||||
PairStateCache pairStateCache,
|
PairStateCache pairStateCache,
|
||||||
PairPerformanceMetricsCache pairPerformanceMetricsCache,
|
PairPerformanceMetricsCache pairPerformanceMetricsCache)
|
||||||
PenumbraTempCollectionJanitor tempCollectionJanitor)
|
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
@@ -68,14 +65,12 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
_textureDownscaleService = textureDownscaleService;
|
_textureDownscaleService = textureDownscaleService;
|
||||||
_pairStateCache = pairStateCache;
|
_pairStateCache = pairStateCache;
|
||||||
_pairPerformanceMetricsCache = pairPerformanceMetricsCache;
|
_pairPerformanceMetricsCache = pairPerformanceMetricsCache;
|
||||||
_tempCollectionJanitor = tempCollectionJanitor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
@@ -86,7 +81,6 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
downloadManager,
|
downloadManager,
|
||||||
_pluginWarningNotificationManager,
|
_pluginWarningNotificationManager,
|
||||||
dalamudUtilService,
|
dalamudUtilService,
|
||||||
actorObjectService,
|
|
||||||
_lifetime,
|
_lifetime,
|
||||||
_fileCacheManager,
|
_fileCacheManager,
|
||||||
_playerPerformanceService,
|
_playerPerformanceService,
|
||||||
@@ -94,7 +88,6 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
_serverConfigManager,
|
_serverConfigManager,
|
||||||
_textureDownscaleService,
|
_textureDownscaleService,
|
||||||
_pairStateCache,
|
_pairStateCache,
|
||||||
_pairPerformanceMetricsCache,
|
_pairPerformanceMetricsCache);
|
||||||
_tempCollectionJanitor);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +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.UI.Models;
|
|
||||||
|
|
||||||
namespace LightlessSync;
|
namespace LightlessSync;
|
||||||
|
|
||||||
@@ -52,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,7 +105,6 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
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(gameGui);
|
services.AddSingleton(gameGui);
|
||||||
services.AddSingleton(gameInteropProvider);
|
|
||||||
services.AddSingleton(addonLifecycle);
|
services.AddSingleton(addonLifecycle);
|
||||||
services.AddSingleton<IUiBuilder>(pluginInterface.UiBuilder);
|
services.AddSingleton<IUiBuilder>(pluginInterface.UiBuilder);
|
||||||
|
|
||||||
@@ -117,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>();
|
||||||
@@ -136,10 +133,8 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
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<TextureMetadataHelper>(sp =>
|
services.AddSingleton<TextureMetadataHelper>(sp =>
|
||||||
new TextureMetadataHelper(sp.GetRequiredService<ILogger<TextureMetadataHelper>>(), gameData));
|
new TextureMetadataHelper(sp.GetRequiredService<ILogger<TextureMetadataHelper>>(), gameData));
|
||||||
@@ -206,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(
|
||||||
@@ -219,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>(),
|
||||||
@@ -274,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,
|
||||||
@@ -282,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(
|
||||||
@@ -315,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(
|
||||||
@@ -479,8 +458,7 @@ 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>(),
|
sp.GetRequiredService<LightFinderScannerService>()));
|
||||||
sp.GetRequiredService<LightFinderPlateHandler>()));
|
|
||||||
|
|
||||||
services.AddScoped<WindowMediatorSubscriberBase, SyncshellFinderUI>(sp => new SyncshellFinderUI(
|
services.AddScoped<WindowMediatorSubscriberBase, SyncshellFinderUI>(sp => new SyncshellFinderUI(
|
||||||
sp.GetRequiredService<ILogger<SyncshellFinderUI>>(),
|
sp.GetRequiredService<ILogger<SyncshellFinderUI>>(),
|
||||||
@@ -549,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>());
|
||||||
@@ -570,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();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
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;
|
||||||
@@ -9,8 +9,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
|||||||
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;
|
||||||
|
|
||||||
@@ -33,17 +31,13 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
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 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;
|
||||||
@@ -61,29 +55,21 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 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)
|
||||||
@@ -127,7 +113,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
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;
|
||||||
@@ -151,16 +136,15 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,7 +207,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var isLoaded = await _framework.RunOnFrameworkThread(() => IsObjectFullyLoaded(address)).ConfigureAwait(false);
|
var isLoaded = await _framework.RunOnFrameworkThread(() => IsObjectFullyLoaded(address)).ConfigureAwait(false);
|
||||||
if (!IsZoning && isLoaded)
|
if (isLoaded)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -313,12 +297,10 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
{
|
{
|
||||||
DisposeHooks();
|
DisposeHooks();
|
||||||
_activePlayers.Clear();
|
_activePlayers.Clear();
|
||||||
_gposePlayers.Clear();
|
|
||||||
_actorsByHash.Clear();
|
_actorsByHash.Clear();
|
||||||
_actorsByName.Clear();
|
_actorsByName.Clear();
|
||||||
_pendingHashResolutions.Clear();
|
_ownedTracker.Reset();
|
||||||
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
|
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
|
||||||
Volatile.Write(ref _gposeSnapshot, GposeSnapshot.Empty);
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,7 +336,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
_onCompanionTerminateHook.Enable();
|
_onCompanionTerminateHook.Enable();
|
||||||
|
|
||||||
_hooksActive = true;
|
_hooksActive = true;
|
||||||
_logger.LogTrace("ActorObjectService hooks enabled.");
|
_logger.LogDebug("ActorObjectService hooks enabled.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task WarmupExistingActors()
|
private Task WarmupExistingActors()
|
||||||
@@ -368,21 +350,36 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
|
|
||||||
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);
|
||||||
@@ -419,7 +416,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
|
|
||||||
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,
|
||||||
@@ -481,196 +478,50 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
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 && (nint)gameObject == expectedMinionOrMount)
|
return (LightlessObjectKind.Player, entityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
|
||||||
return (LightlessObjectKind.Pet, ownerId);
|
|
||||||
|
|
||||||
var expectedCompanion = GetCompanionAddress(localPlayerAddress, localEntityId);
|
|
||||||
if (expectedCompanion != nint.Zero && (nint)gameObject == expectedCompanion)
|
|
||||||
return (LightlessObjectKind.Companion, ownerId);
|
|
||||||
|
|
||||||
return (null, ownerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe nint GetMinionOrMountAddress(nint localPlayerAddress, uint ownerEntityId)
|
|
||||||
{
|
|
||||||
if (localPlayerAddress == nint.Zero)
|
|
||||||
return nint.Zero;
|
|
||||||
|
|
||||||
var playerObject = (GameObject*)localPlayerAddress;
|
|
||||||
var candidateAddress = _objectTable.GetObjectAddress(playerObject->ObjectIndex + 1);
|
|
||||||
if (candidateAddress != nint.Zero)
|
|
||||||
{
|
{
|
||||||
var candidate = (GameObject*)candidateAddress;
|
DalamudObjectKind.MountType => LightlessObjectKind.MinionOrMount,
|
||||||
var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
|
DalamudObjectKind.Companion => LightlessObjectKind.MinionOrMount,
|
||||||
if (candidateKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
|
DalamudObjectKind.BattleNpc => gameObject->BattleNpcSubKind switch
|
||||||
{
|
{
|
||||||
if (ownerEntityId == 0 || ResolveOwnerId(candidate) == ownerEntityId)
|
BattleNpcSubKind.Buddy => LightlessObjectKind.Companion,
|
||||||
return candidateAddress;
|
BattleNpcSubKind.Pet => LightlessObjectKind.Pet,
|
||||||
}
|
_ => (LightlessObjectKind?)null,
|
||||||
}
|
},
|
||||||
|
_ => (LightlessObjectKind?)null,
|
||||||
|
};
|
||||||
|
|
||||||
if (ownerEntityId == 0)
|
return (ownedKind, ownerId);
|
||||||
return candidateAddress;
|
|
||||||
|
|
||||||
foreach (var obj in _objectTable)
|
|
||||||
{
|
|
||||||
if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (obj.ObjectKind is not (DalamudObjectKind.MountType or DalamudObjectKind.Companion))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var candidate = (GameObject*)obj.Address;
|
|
||||||
if (ResolveOwnerId(candidate) == ownerEntityId)
|
|
||||||
return obj.Address;
|
|
||||||
}
|
|
||||||
|
|
||||||
return candidateAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var obj in _objectTable)
|
|
||||||
{
|
|
||||||
if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (obj.ObjectKind != DalamudObjectKind.BattleNpc)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var candidate = (GameObject*)obj.Address;
|
|
||||||
if (candidate->BattleNpcSubKind != BattleNpcSubKind.Pet)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (ResolveOwnerId(candidate) == ownerEntityId)
|
|
||||||
return obj.Address;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var obj in _objectTable)
|
|
||||||
{
|
|
||||||
if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (obj.ObjectKind != DalamudObjectKind.BattleNpc)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var candidate = (GameObject*)obj.Address;
|
|
||||||
if (candidate->BattleNpcSubKind != BattleNpcSubKind.Buddy)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (ResolveOwnerId(candidate) == ownerEntityId)
|
|
||||||
return obj.Address;
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -683,7 +534,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
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,
|
||||||
@@ -707,14 +558,10 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
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();
|
||||||
@@ -727,47 +574,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
{
|
{
|
||||||
_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)
|
||||||
@@ -799,15 +605,30 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
|
|
||||||
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)
|
||||||
@@ -833,122 +654,29 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
{
|
{
|
||||||
_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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -966,24 +694,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
_ = _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
|
||||||
@@ -1015,7 +725,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
|
|
||||||
if (hadHooks)
|
if (hadHooks)
|
||||||
{
|
{
|
||||||
_logger.LogTrace("ActorObjectService hooks disabled.");
|
_logger.LogDebug("ActorObjectService hooks disabled.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1060,89 +770,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
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 static unsafe bool IsObjectFullyLoaded(nint address)
|
private static unsafe bool IsObjectFullyLoaded(nint address)
|
||||||
{
|
{
|
||||||
if (address == nint.Zero)
|
if (address == nint.Zero)
|
||||||
@@ -1156,10 +783,13 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
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;
|
||||||
|
|
||||||
@@ -1169,6 +799,109 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class OwnedObjectTracker
|
||||||
|
{
|
||||||
|
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(
|
||||||
IReadOnlyList<nint> RenderedPlayers,
|
IReadOnlyList<nint> RenderedPlayers,
|
||||||
IReadOnlyList<nint> RenderedCompanions,
|
IReadOnlyList<nint> RenderedCompanions,
|
||||||
@@ -1192,27 +925,14 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
|
|
||||||
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;
|
||||||
@@ -99,13 +98,11 @@ 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)
|
||||||
{
|
{
|
||||||
var normalized = new HashSet<string>(
|
var normalized = new HashSet<string>(
|
||||||
@@ -128,7 +125,6 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
@@ -140,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;
|
||||||
}
|
}
|
||||||
@@ -189,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>();
|
||||||
@@ -215,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;
|
||||||
@@ -259,79 +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;
|
entry.Size = normalSize;
|
||||||
Triangles = triangles;
|
entry.CompressedSize = compressedsize.Item2.LongLength;
|
||||||
CacheEntries = cacheEntries;
|
}
|
||||||
|
OriginalSize = normalSize;
|
||||||
|
CompressedSize = compressedsize.Item2.LongLength;
|
||||||
|
RefreshFormat();
|
||||||
}
|
}
|
||||||
|
public long OriginalSize { get; private set; } = OriginalSize;
|
||||||
public async Task ComputeSizes(FileCacheManager fileCacheManager, CancellationToken token, bool force = false)
|
public long CompressedSize { get; private set; } = CompressedSize;
|
||||||
{
|
public long Triangles { get; private set; } = Triangles;
|
||||||
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);
|
|
||||||
|
|
||||||
fileCacheManager.SetSizeInfo(Hash, original, compressedLen);
|
|
||||||
FileCacheManager.ApplySizesToEntries(CacheEntries, original, compressedLen);
|
|
||||||
|
|
||||||
OriginalSize = original;
|
|
||||||
CompressedSize = compressedLen;
|
|
||||||
|
|
||||||
if (string.Equals(FileType, "tex", StringComparison.OrdinalIgnoreCase))
|
|
||||||
RefreshFormat();
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
||||||
|
{
|
||||||
|
_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,275 +0,0 @@
|
|||||||
using Dalamud.Interface.Textures.TextureWraps;
|
|
||||||
using LightlessSync.UI;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace LightlessSync.Services.Chat;
|
|
||||||
|
|
||||||
public sealed class ChatEmoteService : IDisposable
|
|
||||||
{
|
|
||||||
private const string GlobalEmoteSetUrl = "https://7tv.io/v3/emote-sets/global";
|
|
||||||
|
|
||||||
private readonly ILogger<ChatEmoteService> _logger;
|
|
||||||
private readonly HttpClient _httpClient;
|
|
||||||
private readonly UiSharedService _uiSharedService;
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_httpClient = httpClient;
|
|
||||||
_uiSharedService = uiSharedService;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.Texture is not null)
|
|
||||||
{
|
|
||||||
texture = entry.Texture;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.EnsureLoading(QueueEmoteDownload);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
foreach (var entry in _emotes.Values)
|
|
||||||
{
|
|
||||||
entry.Texture?.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 url = TryBuildEmoteUrl(emoteElement);
|
|
||||||
if (string.IsNullOrWhiteSpace(url))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
_emotes.TryAdd(name, new EmoteEntry(url));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Failed to load 7TV emote set");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? TryBuildEmoteUrl(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 fileName = PickBestStaticFile(filesElement);
|
|
||||||
if (string.IsNullOrWhiteSpace(fileName))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseUrl.TrimEnd('/') + "/" + fileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? PickBestStaticFile(JsonElement filesElement)
|
|
||||||
{
|
|
||||||
string? png1x = null;
|
|
||||||
string? webp1x = null;
|
|
||||||
string? pngFallback = null;
|
|
||||||
string? webpFallback = null;
|
|
||||||
|
|
||||||
foreach (var file in filesElement.EnumerateArray())
|
|
||||||
{
|
|
||||||
if (file.TryGetProperty("static", out var staticElement) && staticElement.ValueKind == JsonValueKind.False)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!file.TryGetProperty("name", out var nameElement))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var name = nameElement.GetString();
|
|
||||||
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.EndsWith(".png", StringComparison.OrdinalIgnoreCase) && pngFallback is null)
|
|
||||||
{
|
|
||||||
pngFallback = name;
|
|
||||||
}
|
|
||||||
else if (name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase) && webpFallback is null)
|
|
||||||
{
|
|
||||||
webpFallback = name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return png1x ?? webp1x ?? pngFallback ?? webpFallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void QueueEmoteDownload(EmoteEntry entry)
|
|
||||||
{
|
|
||||||
_ = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await _downloadGate.WaitAsync().ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var data = await _httpClient.GetByteArrayAsync(entry.Url).ConfigureAwait(false);
|
|
||||||
var texture = _uiSharedService.LoadImage(data);
|
|
||||||
entry.SetTexture(texture);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogDebug(ex, "Failed to load 7TV emote {Url}", entry.Url);
|
|
||||||
entry.MarkFailed();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_downloadGate.Release();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class EmoteEntry
|
|
||||||
{
|
|
||||||
private int _loadingState;
|
|
||||||
|
|
||||||
public EmoteEntry(string url)
|
|
||||||
{
|
|
||||||
Url = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Url { get; }
|
|
||||||
public IDalamudTextureWrap? Texture { get; private set; }
|
|
||||||
|
|
||||||
public void EnsureLoading(Action<EmoteEntry> queueDownload)
|
|
||||||
{
|
|
||||||
if (Texture is not null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Interlocked.CompareExchange(ref _loadingState, 1, 0) != 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
queueDownload(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetTexture(IDalamudTextureWrap texture)
|
|
||||||
{
|
|
||||||
Texture = texture;
|
|
||||||
Interlocked.Exchange(ref _loadingState, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void MarkFailed()
|
|
||||||
{
|
|
||||||
Interlocked.Exchange(ref _loadingState, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
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;
|
||||||
@@ -26,7 +24,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
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();
|
||||||
|
|
||||||
@@ -39,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;
|
||||||
@@ -57,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;
|
||||||
@@ -66,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;
|
||||||
@@ -77,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)
|
||||||
{
|
{
|
||||||
@@ -111,8 +98,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
state.Messages.ToList()));
|
state.Messages.ToList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
_cachedChannelSnapshots = snapshots;
|
|
||||||
_channelsSnapshotDirty = false;
|
|
||||||
return snapshots;
|
return snapshots;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +186,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
if (!wasEnabled)
|
if (!wasEnabled)
|
||||||
{
|
{
|
||||||
_chatEnabled = true;
|
_chatEnabled = true;
|
||||||
MarkChannelsSnapshotDirtyLocked();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,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();
|
||||||
@@ -576,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;
|
||||||
|
|
||||||
@@ -702,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)
|
||||||
@@ -737,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))
|
||||||
@@ -781,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)
|
||||||
{
|
{
|
||||||
@@ -797,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
|
||||||
{
|
{
|
||||||
@@ -823,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -879,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,
|
||||||
@@ -1024,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1056,8 +1023,6 @@ 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Mediator.Publish(new ChatChannelMessageAdded(key, message));
|
Mediator.Publish(new ChatChannelMessageAdded(key, message));
|
||||||
@@ -1161,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)
|
||||||
{
|
{
|
||||||
@@ -1171,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1248,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)
|
||||||
{
|
{
|
||||||
@@ -1309,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1323,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);
|
||||||
@@ -1342,28 +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 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)
|
||||||
|
|||||||
@@ -4,22 +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 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;
|
||||||
@@ -30,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;
|
||||||
@@ -45,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,
|
||||||
@@ -53,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;
|
||||||
@@ -72,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)
|
||||||
@@ -105,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;
|
||||||
@@ -141,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);
|
||||||
|
|
||||||
@@ -210,18 +199,6 @@ 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)
|
||||||
@@ -241,7 +218,7 @@ internal class ContextMenuService : IHostedService
|
|||||||
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);
|
||||||
@@ -250,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}@{world.Name}.", NotificationType.Info);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
@@ -23,7 +24,6 @@ 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 VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
|
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
|
||||||
@@ -37,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;
|
||||||
@@ -61,7 +60,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
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)
|
||||||
{
|
{
|
||||||
@@ -73,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;
|
||||||
@@ -82,26 +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])))
|
||||||
.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) =>
|
||||||
{
|
{
|
||||||
@@ -133,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;
|
||||||
@@ -273,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];
|
||||||
@@ -286,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; }
|
||||||
|
|
||||||
@@ -307,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;
|
||||||
}
|
}
|
||||||
@@ -370,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)
|
||||||
@@ -382,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()
|
||||||
@@ -398,8 +355,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
var playerAddress = playerPointer.Value;
|
var playerAddress = playerPointer.Value;
|
||||||
var ownerEntityId = ((Character*)playerAddress)->EntityId;
|
var ownerEntityId = ((Character*)playerAddress)->EntityId;
|
||||||
var candidateAddress = _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1);
|
if (ownerEntityId == 0) return IntPtr.Zero;
|
||||||
if (ownerEntityId == 0) return candidateAddress;
|
|
||||||
|
|
||||||
if (playerAddress == _actorObjectService.LocalPlayerAddress)
|
if (playerAddress == _actorObjectService.LocalPlayerAddress)
|
||||||
{
|
{
|
||||||
@@ -410,17 +366,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (candidateAddress != nint.Zero)
|
|
||||||
{
|
|
||||||
var candidate = (GameObject*)candidateAddress;
|
|
||||||
var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
|
|
||||||
if ((candidateKind == DalamudObjectKind.MountType || candidateKind == DalamudObjectKind.Companion)
|
|
||||||
&& ResolveOwnerId(candidate) == ownerEntityId)
|
|
||||||
{
|
|
||||||
return candidateAddress;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var ownedObject = FindOwnedObject(ownerEntityId, playerAddress, static kind =>
|
var ownedObject = FindOwnedObject(ownerEntityId, playerAddress, static kind =>
|
||||||
kind == DalamudObjectKind.MountType || kind == DalamudObjectKind.Companion);
|
kind == DalamudObjectKind.MountType || kind == DalamudObjectKind.Companion);
|
||||||
if (ownedObject != nint.Zero)
|
if (ownedObject != nint.Zero)
|
||||||
@@ -428,7 +373,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
return ownedObject;
|
return ownedObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
return candidateAddress;
|
return _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IntPtr> GetMinionOrMountAsync(IntPtr? playerPointer = null)
|
public async Task<IntPtr> GetMinionOrMountAsync(IntPtr? playerPointer = null)
|
||||||
@@ -443,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 FindOwnedPet(ownerEntityId, ownerAddress);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IntPtr> GetPetAsync(IntPtr? playerPointer = null)
|
public async Task<IntPtr> GetPetAsync(IntPtr? playerPointer = null)
|
||||||
@@ -495,60 +425,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
return nint.Zero;
|
return nint.Zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe nint FindOwnedPet(uint ownerEntityId, nint ownerAddress)
|
|
||||||
{
|
|
||||||
if (ownerEntityId == 0)
|
|
||||||
{
|
|
||||||
return nint.Zero;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var obj in _objectTable)
|
|
||||||
{
|
|
||||||
if (obj is null || obj.Address == nint.Zero || obj.Address == ownerAddress)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (obj.ObjectKind != DalamudObjectKind.BattleNpc)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var candidate = (GameObject*)obj.Address;
|
|
||||||
if (candidate->BattleNpcSubKind != BattleNpcSubKind.Pet)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ResolveOwnerId(candidate) == ownerEntityId)
|
|
||||||
{
|
|
||||||
return obj.Address;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 uint ResolveOwnerId(GameObject* gameObject)
|
private static unsafe uint ResolveOwnerId(GameObject* gameObject)
|
||||||
{
|
{
|
||||||
if (gameObject == null)
|
if (gameObject == null)
|
||||||
@@ -596,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)
|
||||||
@@ -645,100 +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; //TODO:Need API update first
|
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 unsafe void SetMarkerAndOpenMap(Vector3 position, Map map)
|
public unsafe void SetMarkerAndOpenMap(Vector3 position, Map map)
|
||||||
@@ -750,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);
|
||||||
@@ -922,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;
|
||||||
@@ -988,12 +846,9 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
_performanceCollector.LogPerformance(this, $"TrackedActorsToState",
|
_performanceCollector.LogPerformance(this, $"TrackedActorsToState",
|
||||||
() =>
|
() =>
|
||||||
{
|
{
|
||||||
if (!_actorObjectService.HooksActive || !isNormalFrameworkUpdate || _actorObjectService.HasPendingHashResolutions)
|
_actorObjectService.RefreshTrackedActors();
|
||||||
{
|
|
||||||
_actorObjectService.RefreshTrackedActors();
|
|
||||||
}
|
|
||||||
|
|
||||||
var playerDescriptors = _actorObjectService.PlayerDescriptors;
|
var playerDescriptors = _actorObjectService.PlayerCharacterDescriptors;
|
||||||
for (var i = 0; i < playerDescriptors.Count; i++)
|
for (var i = 0; i < playerDescriptors.Count; i++)
|
||||||
{
|
{
|
||||||
var actor = playerDescriptors[i];
|
var actor = playerDescriptors[i];
|
||||||
|
|||||||
@@ -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
@@ -15,14 +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 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);
|
||||||
@@ -45,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;
|
||||||
@@ -74,8 +69,6 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
if (!_broadcastService.IsBroadcasting)
|
if (!_broadcastService.IsBroadcasting)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
TryPrimeLocalBroadcastCache();
|
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
foreach (var address in _actorTracker.PlayerAddresses)
|
foreach (var address in _actorTracker.PlayerAddresses)
|
||||||
@@ -136,7 +129,6 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
_lightFinderPlateHandler.UpdateBroadcastingCids(activeCids);
|
_lightFinderPlateHandler.UpdateBroadcastingCids(activeCids);
|
||||||
_lightFinderNativePlateHandler.UpdateBroadcastingCids(activeCids);
|
|
||||||
UpdateSyncshellBroadcasts();
|
UpdateSyncshellBroadcasts();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,58 +140,18 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
_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;
|
|
||||||
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, null),
|
|
||||||
(_, old) => new BroadcastEntry(true, expiry, old.GID));
|
|
||||||
|
|
||||||
_pendingLocalBroadcast = false;
|
|
||||||
_pendingLocalTtl = 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateSyncshellBroadcasts()
|
private void UpdateSyncshellBroadcasts()
|
||||||
{
|
{
|
||||||
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))
|
||||||
{
|
{
|
||||||
@@ -211,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,
|
||||||
@@ -231,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;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,71 +0,0 @@
|
|||||||
using LightlessSync.Interop.Ipc;
|
|
||||||
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 LightlessConfigService _config;
|
|
||||||
private int _ran;
|
|
||||||
|
|
||||||
public PenumbraTempCollectionJanitor(
|
|
||||||
ILogger<PenumbraTempCollectionJanitor> logger,
|
|
||||||
LightlessMediator mediator,
|
|
||||||
IpcManager ipc,
|
|
||||||
LightlessConfigService config) : base(logger, mediator)
|
|
||||||
{
|
|
||||||
_ipc = ipc;
|
|
||||||
_config = config;
|
|
||||||
|
|
||||||
Mediator.Subscribe<PenumbraInitializedMessage>(this, _ => CleanupOrphansOnBoot());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Register(Guid id)
|
|
||||||
{
|
|
||||||
if (id == Guid.Empty) return;
|
|
||||||
if (_config.Current.OrphanableTempCollections.Add(id))
|
|
||||||
_config.Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Unregister(Guid id)
|
|
||||||
{
|
|
||||||
if (id == Guid.Empty) return;
|
|
||||||
if (_config.Current.OrphanableTempCollections.Remove(id))
|
|
||||||
_config.Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CleanupOrphansOnBoot()
|
|
||||||
{
|
|
||||||
if (Interlocked.Exchange(ref _ran, 1) == 1)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!_ipc.Penumbra.APIAvailable)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var ids = _config.Current.OrphanableTempCollections.ToArray();
|
|
||||||
if (ids.Length == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var appId = Guid.NewGuid();
|
|
||||||
Logger.LogInformation("Cleaning up {count} orphaned Lightless temp collections found in configuration", ids.Length);
|
|
||||||
|
|
||||||
foreach (var id in ids)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_ipc.Penumbra.RemoveTemporaryCollectionAsync(Logger, appId, id)
|
|
||||||
.GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogDebug(ex, "Failed removing orphaned temp collection {id}", id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_config.Current.OrphanableTempCollections.Clear();
|
|
||||||
_config.Save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,26 +133,6 @@ public class DrawUserPair
|
|||||||
UiSharedService.AttachToolTip("This reapplies the last received character data to this character");
|
UiSharedService.AttachToolTip("This reapplies the last received character data to this character");
|
||||||
}
|
}
|
||||||
|
|
||||||
var isPaused = _pair.UserPair!.OwnPermissions.IsPaused();
|
|
||||||
if (!isPaused)
|
|
||||||
{
|
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Pause, "Toggle Pause State", _menuWidth, true))
|
|
||||||
{
|
|
||||||
_ = _apiController.PauseAsync(_pair.UserData);
|
|
||||||
ImGui.CloseCurrentPopup();
|
|
||||||
}
|
|
||||||
UiSharedService.AttachToolTip("Pauses syncing with this user.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Play, "Toggle Unpause State", _menuWidth, true))
|
|
||||||
{
|
|
||||||
_ = _apiController.UnpauseAsync(_pair.UserData);
|
|
||||||
ImGui.CloseCurrentPopup();
|
|
||||||
}
|
|
||||||
UiSharedService.AttachToolTip("Resumes syncing with this user.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Cycle pause state", _menuWidth, true))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Cycle pause state", _menuWidth, true))
|
||||||
{
|
{
|
||||||
_ = _apiController.CyclePauseAsync(_pair);
|
_ = _apiController.CyclePauseAsync(_pair);
|
||||||
@@ -757,19 +737,14 @@ public class DrawUserPair
|
|||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Hold CTRL and click to remove user " + (_pair.UserData.AliasOrUID) + " from Syncshell");
|
UiSharedService.AttachToolTip("Hold CTRL and click to remove user " + (_pair.UserData.AliasOrUID) + " from Syncshell");
|
||||||
|
|
||||||
var banEnabled = UiSharedService.CtrlPressed();
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, "Ban User", _menuWidth, true))
|
||||||
var banLabel = banEnabled ? "Ban user" : "Ban user (Hold CTRL)";
|
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, banLabel, _menuWidth, true) && banEnabled)
|
|
||||||
{
|
{
|
||||||
_mediator.Publish(new OpenBanUserPopupMessage(_pair, group));
|
_mediator.Publish(new OpenBanUserPopupMessage(_pair, group));
|
||||||
ImGui.CloseCurrentPopup();
|
ImGui.CloseCurrentPopup();
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Hold CTRL to ban user " + (_pair.UserData.AliasOrUID) + " from this Syncshell");
|
UiSharedService.AttachToolTip("Ban user from this Syncshell");
|
||||||
|
|
||||||
if (showOwnerActions)
|
ImGui.Separator();
|
||||||
{
|
|
||||||
ImGui.Separator();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showOwnerActions)
|
if (showOwnerActions)
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ using LightlessSync.Services.TextureCompression;
|
|||||||
using LightlessSync.Utils;
|
using LightlessSync.Utils;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using OtterTex;
|
using OtterTex;
|
||||||
using System.Buffers.Binary;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
@@ -50,7 +49,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
private readonly Dictionary<string, TextureCompressionTarget> _textureSelections = new(StringComparer.OrdinalIgnoreCase);
|
private readonly Dictionary<string, TextureCompressionTarget> _textureSelections = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private readonly HashSet<string> _selectedTextureKeys = new(StringComparer.OrdinalIgnoreCase);
|
private readonly HashSet<string> _selectedTextureKeys = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private readonly Dictionary<string, TexturePreviewState> _texturePreviews = new(StringComparer.OrdinalIgnoreCase);
|
private readonly Dictionary<string, TexturePreviewState> _texturePreviews = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private readonly Dictionary<string, TextureResolutionInfo?> _textureResolutionCache = new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
private readonly Dictionary<ObjectKind, TextureWorkspaceTab> _textureWorkspaceTabs = new();
|
private readonly Dictionary<ObjectKind, TextureWorkspaceTab> _textureWorkspaceTabs = new();
|
||||||
private readonly List<string> _storedPathsToRemove = [];
|
private readonly List<string> _storedPathsToRemove = [];
|
||||||
private readonly Dictionary<string, string> _filePathResolve = [];
|
private readonly Dictionary<string, string> _filePathResolve = [];
|
||||||
@@ -90,9 +88,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
private bool _showAlreadyAddedTransients = false;
|
private bool _showAlreadyAddedTransients = false;
|
||||||
private bool _acknowledgeReview = false;
|
private bool _acknowledgeReview = false;
|
||||||
|
|
||||||
private Task<TextureRowBuildResult>? _textureRowsBuildTask;
|
|
||||||
private CancellationTokenSource? _textureRowsBuildCts;
|
|
||||||
|
|
||||||
private ObjectKind _selectedObjectTab;
|
private ObjectKind _selectedObjectTab;
|
||||||
|
|
||||||
private TextureUsageCategory? _textureCategoryFilter = null;
|
private TextureUsageCategory? _textureCategoryFilter = null;
|
||||||
@@ -209,9 +204,9 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_cachedAnalysis = CloneAnalysis(_characterAnalyzer.LastAnalysis);
|
_cachedAnalysis = _characterAnalyzer.LastAnalysis.DeepClone();
|
||||||
_hasUpdate = false;
|
_hasUpdate = false;
|
||||||
InvalidateTextureRows();
|
_textureRowsDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawContentTabs()
|
private void DrawContentTabs()
|
||||||
@@ -755,7 +750,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
_selectedTextureKeys.Clear();
|
_selectedTextureKeys.Clear();
|
||||||
_textureSelections.Clear();
|
_textureSelections.Clear();
|
||||||
ResetTextureFilters();
|
ResetTextureFilters();
|
||||||
InvalidateTextureRows();
|
_textureRowsDirty = true;
|
||||||
_conversionFailed = false;
|
_conversionFailed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -767,8 +762,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
preview.Texture?.Dispose();
|
preview.Texture?.Dispose();
|
||||||
}
|
}
|
||||||
_texturePreviews.Clear();
|
_texturePreviews.Clear();
|
||||||
_textureRowsBuildCts?.Cancel();
|
|
||||||
_textureRowsBuildCts?.Dispose();
|
|
||||||
_conversionProgress.ProgressChanged -= ConversionProgress_ProgressChanged;
|
_conversionProgress.ProgressChanged -= ConversionProgress_ProgressChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -782,108 +775,18 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
private void EnsureTextureRows()
|
private void EnsureTextureRows()
|
||||||
{
|
{
|
||||||
if (_cachedAnalysis == null)
|
if (!_textureRowsDirty || _cachedAnalysis == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_textureRowsDirty && _textureRowsBuildTask == null)
|
|
||||||
{
|
|
||||||
_textureRowsBuildCts?.Dispose();
|
|
||||||
_textureRowsBuildCts = new();
|
|
||||||
var snapshot = _cachedAnalysis;
|
|
||||||
_textureRowsBuildTask = Task.Run(() => BuildTextureRows(snapshot, _textureRowsBuildCts.Token), _textureRowsBuildCts.Token);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_textureRowsBuildTask == null || !_textureRowsBuildTask.IsCompleted)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var completedTask = _textureRowsBuildTask;
|
|
||||||
_textureRowsBuildTask = null;
|
|
||||||
_textureRowsBuildCts?.Dispose();
|
|
||||||
_textureRowsBuildCts = null;
|
|
||||||
|
|
||||||
if (completedTask.IsCanceled)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (completedTask.IsFaulted)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(completedTask.Exception, "Failed to build texture rows.");
|
|
||||||
_textureRowsDirty = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ApplyTextureRowBuild(completedTask.Result);
|
|
||||||
_textureRowsDirty = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ApplyTextureRowBuild(TextureRowBuildResult result)
|
|
||||||
{
|
|
||||||
_textureRows.Clear();
|
_textureRows.Clear();
|
||||||
_textureRows.AddRange(result.Rows);
|
|
||||||
|
|
||||||
foreach (var row in _textureRows)
|
|
||||||
{
|
|
||||||
if (row.IsAlreadyCompressed)
|
|
||||||
{
|
|
||||||
_selectedTextureKeys.Remove(row.Key);
|
|
||||||
_textureSelections.Remove(row.Key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_selectedTextureKeys.RemoveWhere(key => !result.ValidKeys.Contains(key));
|
|
||||||
|
|
||||||
foreach (var key in _texturePreviews.Keys.ToArray())
|
|
||||||
{
|
|
||||||
if (!result.ValidKeys.Contains(key) && _texturePreviews.TryGetValue(key, out var preview))
|
|
||||||
{
|
|
||||||
preview.Texture?.Dispose();
|
|
||||||
_texturePreviews.Remove(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var key in _textureResolutionCache.Keys.ToArray())
|
|
||||||
{
|
|
||||||
if (!result.ValidKeys.Contains(key))
|
|
||||||
{
|
|
||||||
_textureResolutionCache.Remove(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var key in _textureSelections.Keys.ToArray())
|
|
||||||
{
|
|
||||||
if (!result.ValidKeys.Contains(key))
|
|
||||||
{
|
|
||||||
_textureSelections.Remove(key);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
_textureSelections[key] = _textureCompressionService.NormalizeTarget(_textureSelections[key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(_selectedTextureKey) && !result.ValidKeys.Contains(_selectedTextureKey))
|
|
||||||
{
|
|
||||||
_selectedTextureKey = string.Empty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private TextureRowBuildResult BuildTextureRows(
|
|
||||||
Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>> analysis,
|
|
||||||
CancellationToken token)
|
|
||||||
{
|
|
||||||
var rows = new List<TextureRow>();
|
|
||||||
HashSet<string> validKeys = new(StringComparer.OrdinalIgnoreCase);
|
HashSet<string> validKeys = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
foreach (var (objectKind, entries) in analysis)
|
foreach (var (objectKind, entries) in _cachedAnalysis)
|
||||||
{
|
{
|
||||||
foreach (var entry in entries.Values)
|
foreach (var entry in entries.Values)
|
||||||
{
|
{
|
||||||
token.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
if (!string.Equals(entry.FileType, "tex", StringComparison.Ordinal))
|
if (!string.Equals(entry.FileType, "tex", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -925,11 +828,17 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
suggestion?.Reason);
|
suggestion?.Reason);
|
||||||
|
|
||||||
validKeys.Add(row.Key);
|
validKeys.Add(row.Key);
|
||||||
rows.Add(row);
|
_textureRows.Add(row);
|
||||||
|
|
||||||
|
if (row.IsAlreadyCompressed)
|
||||||
|
{
|
||||||
|
_selectedTextureKeys.Remove(row.Key);
|
||||||
|
_textureSelections.Remove(row.Key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rows.Sort((a, b) =>
|
_textureRows.Sort((a, b) =>
|
||||||
{
|
{
|
||||||
var comp = a.ObjectKind.CompareTo(b.ObjectKind);
|
var comp = a.ObjectKind.CompareTo(b.ObjectKind);
|
||||||
if (comp != 0)
|
if (comp != 0)
|
||||||
@@ -942,14 +851,34 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
return string.Compare(a.DisplayName, b.DisplayName, StringComparison.OrdinalIgnoreCase);
|
return string.Compare(a.DisplayName, b.DisplayName, StringComparison.OrdinalIgnoreCase);
|
||||||
});
|
});
|
||||||
|
|
||||||
return new TextureRowBuildResult(rows, validKeys);
|
_selectedTextureKeys.RemoveWhere(key => !validKeys.Contains(key));
|
||||||
}
|
|
||||||
|
|
||||||
private void InvalidateTextureRows()
|
foreach (var key in _texturePreviews.Keys.ToArray())
|
||||||
{
|
{
|
||||||
_textureRowsDirty = true;
|
if (!validKeys.Contains(key) && _texturePreviews.TryGetValue(key, out var preview))
|
||||||
_textureRowsBuildCts?.Cancel();
|
{
|
||||||
_textureResolutionCache.Clear();
|
preview.Texture?.Dispose();
|
||||||
|
_texturePreviews.Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var key in _textureSelections.Keys.ToArray())
|
||||||
|
{
|
||||||
|
if (!validKeys.Contains(key))
|
||||||
|
{
|
||||||
|
_textureSelections.Remove(key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_textureSelections[key] = _textureCompressionService.NormalizeTarget(_textureSelections[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(_selectedTextureKey) && !validKeys.Contains(_selectedTextureKey))
|
||||||
|
{
|
||||||
|
_selectedTextureKey = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
_textureRowsDirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string MakeTextureKey(ObjectKind objectKind, string primaryFilePath) =>
|
private static string MakeTextureKey(ObjectKind objectKind, string primaryFilePath) =>
|
||||||
@@ -964,35 +893,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
_textureSearch = string.Empty;
|
_textureSearch = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>> CloneAnalysis(
|
|
||||||
Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>> source)
|
|
||||||
{
|
|
||||||
var clone = new Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>>(source.Count);
|
|
||||||
|
|
||||||
foreach (var (objectKind, entries) in source)
|
|
||||||
{
|
|
||||||
var entryClone = new Dictionary<string, CharacterAnalyzer.FileDataEntry>(entries.Count, entries.Comparer);
|
|
||||||
|
|
||||||
foreach (var (hash, entry) in entries)
|
|
||||||
{
|
|
||||||
entryClone[hash] = new CharacterAnalyzer.FileDataEntry(
|
|
||||||
hash: hash,
|
|
||||||
fileType: entry.FileType,
|
|
||||||
gamePaths: entry.GamePaths?.ToList() ?? [],
|
|
||||||
filePaths: entry.FilePaths?.ToList() ?? [],
|
|
||||||
originalSize: entry.OriginalSize,
|
|
||||||
compressedSize: entry.CompressedSize,
|
|
||||||
triangles: entry.Triangles,
|
|
||||||
cacheEntries: entry.CacheEntries
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
clone[objectKind] = entryClone;
|
|
||||||
}
|
|
||||||
|
|
||||||
return clone;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawAnalysisOverview(int totalFiles, long totalActualSize, long totalCompressedSize, long totalTriangles, string breakdownTooltip)
|
private void DrawAnalysisOverview(int totalFiles, long totalActualSize, long totalCompressedSize, long totalTriangles, string breakdownTooltip)
|
||||||
{
|
{
|
||||||
var scale = ImGuiHelpers.GlobalScale;
|
var scale = ImGuiHelpers.GlobalScale;
|
||||||
@@ -1191,10 +1091,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
public bool IsAlreadyCompressed => CurrentTarget.HasValue;
|
public bool IsAlreadyCompressed => CurrentTarget.HasValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record TextureRowBuildResult(
|
|
||||||
List<TextureRow> Rows,
|
|
||||||
HashSet<string> ValidKeys);
|
|
||||||
|
|
||||||
private sealed class TexturePreviewState
|
private sealed class TexturePreviewState
|
||||||
{
|
{
|
||||||
public Task? LoadTask { get; set; }
|
public Task? LoadTask { get; set; }
|
||||||
@@ -1203,22 +1099,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
public DateTime LastAccessUtc { get; set; } = DateTime.UtcNow;
|
public DateTime LastAccessUtc { get; set; } = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly struct TextureResolutionInfo
|
|
||||||
{
|
|
||||||
public TextureResolutionInfo(ushort width, ushort height, ushort depth, ushort mipLevels)
|
|
||||||
{
|
|
||||||
Width = width;
|
|
||||||
Height = height;
|
|
||||||
Depth = depth;
|
|
||||||
MipLevels = mipLevels;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ushort Width { get; }
|
|
||||||
public ushort Height { get; }
|
|
||||||
public ushort Depth { get; }
|
|
||||||
public ushort MipLevels { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawTextureWorkspace(ObjectKind objectKind, IReadOnlyList<IGrouping<string, CharacterAnalyzer.FileDataEntry>> otherFileGroups)
|
private void DrawTextureWorkspace(ObjectKind objectKind, IReadOnlyList<IGrouping<string, CharacterAnalyzer.FileDataEntry>> otherFileGroups)
|
||||||
{
|
{
|
||||||
if (!_textureWorkspaceTabs.ContainsKey(objectKind))
|
if (!_textureWorkspaceTabs.ContainsKey(objectKind))
|
||||||
@@ -1263,11 +1143,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
private void DrawTextureTabContent(ObjectKind objectKind)
|
private void DrawTextureTabContent(ObjectKind objectKind)
|
||||||
{
|
{
|
||||||
var scale = ImGuiHelpers.GlobalScale;
|
var scale = ImGuiHelpers.GlobalScale;
|
||||||
if (_textureRowsBuildTask != null && !_textureRowsBuildTask.IsCompleted && _textureRows.Count == 0)
|
|
||||||
{
|
|
||||||
UiSharedService.ColorText("Building texture list.", ImGuiColors.DalamudGrey);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var objectRows = _textureRows.Where(row => row.ObjectKind == objectKind).ToList();
|
var objectRows = _textureRows.Where(row => row.ObjectKind == objectKind).ToList();
|
||||||
var hasAnyTextureRows = objectRows.Count > 0;
|
var hasAnyTextureRows = objectRows.Count > 0;
|
||||||
var availableCategories = objectRows.Select(row => row.Category)
|
var availableCategories = objectRows.Select(row => row.Category)
|
||||||
@@ -1529,24 +1404,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
ResetTextureFilters();
|
ResetTextureFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGuiHelpers.ScaledDummy(6);
|
|
||||||
ImGui.Separator();
|
|
||||||
ImGuiHelpers.ScaledDummy(4);
|
|
||||||
UiSharedService.ColorText("Texture row colors", UIColors.Get("LightlessPurple"));
|
|
||||||
DrawTextureRowLegendItem("Selected", UIColors.Get("LightlessYellow"), "This row is selected in the texture table.");
|
|
||||||
DrawTextureRowLegendItem("Already compressed", UIColors.Get("LightlessGreenDefault"), "Texture is already stored in a compressed format.");
|
|
||||||
DrawTextureRowLegendItem("Missing analysis data", UIColors.Get("DimRed"), "File size data has not been computed yet.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void DrawTextureRowLegendItem(string label, Vector4 color, string description)
|
|
||||||
{
|
|
||||||
var scale = ImGuiHelpers.GlobalScale;
|
|
||||||
var swatchSize = new Vector2(12f * scale, 12f * scale);
|
|
||||||
ImGui.ColorButton($"##textureRowLegend{label}", color, ImGuiColorEditFlags.NoTooltip | ImGuiColorEditFlags.NoDragDrop, swatchSize);
|
|
||||||
ImGui.SameLine(0f, 6f * scale);
|
|
||||||
var wrapPos = ImGui.GetCursorPosX() + ImGui.GetContentRegionAvail().X;
|
|
||||||
UiSharedService.TextWrapped($"{label}: {description}", wrapPos);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void DrawEnumFilterCombo<T>(
|
private static void DrawEnumFilterCombo<T>(
|
||||||
@@ -1953,7 +1810,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Sync, "Refresh", 130f * scale))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Sync, "Refresh", 130f * scale))
|
||||||
{
|
{
|
||||||
InvalidateTextureRows();
|
_textureRowsDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
TextureRow? lastSelected = null;
|
TextureRow? lastSelected = null;
|
||||||
@@ -2119,7 +1976,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
_selectedTextureKeys.Clear();
|
_selectedTextureKeys.Clear();
|
||||||
_textureSelections.Clear();
|
_textureSelections.Clear();
|
||||||
InvalidateTextureRows();
|
_textureRowsDirty = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2340,68 +2197,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private TextureResolutionInfo? GetTextureResolution(TextureRow row)
|
|
||||||
{
|
|
||||||
if (_textureResolutionCache.TryGetValue(row.Key, out var cached))
|
|
||||||
{
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
var info = TryReadTextureResolution(row.PrimaryFilePath, out var resolved)
|
|
||||||
? resolved
|
|
||||||
: (TextureResolutionInfo?)null;
|
|
||||||
_textureResolutionCache[row.Key] = info;
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryReadTextureResolution(string path, out TextureResolutionInfo info)
|
|
||||||
{
|
|
||||||
info = default;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Span<byte> buffer = stackalloc byte[16];
|
|
||||||
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
|
|
||||||
var read = stream.Read(buffer);
|
|
||||||
if (read < buffer.Length)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var width = BinaryPrimitives.ReadUInt16LittleEndian(buffer[8..10]);
|
|
||||||
var height = BinaryPrimitives.ReadUInt16LittleEndian(buffer[10..12]);
|
|
||||||
var depth = BinaryPrimitives.ReadUInt16LittleEndian(buffer[12..14]);
|
|
||||||
var mipLevels = BinaryPrimitives.ReadUInt16LittleEndian(buffer[14..16]);
|
|
||||||
|
|
||||||
if (width == 0 || height == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (depth == 0)
|
|
||||||
{
|
|
||||||
depth = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mipLevels == 0)
|
|
||||||
{
|
|
||||||
mipLevels = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
info = new TextureResolutionInfo(width, height, depth, mipLevels);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatTextureResolution(TextureResolutionInfo info)
|
|
||||||
=> info.Depth > 1
|
|
||||||
? $"{info.Width} x {info.Height} x {info.Depth}"
|
|
||||||
: $"{info.Width} x {info.Height}";
|
|
||||||
|
|
||||||
private void DrawTextureRow(TextureRow row, IReadOnlyList<TextureCompressionTarget> targets, int index)
|
private void DrawTextureRow(TextureRow row, IReadOnlyList<TextureCompressionTarget> targets, int index)
|
||||||
{
|
{
|
||||||
var key = row.Key;
|
var key = row.Key;
|
||||||
@@ -2670,9 +2465,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
MetaRow(FontAwesomeIcon.LayerGroup, "Map Type", row.MapKind.ToString());
|
MetaRow(FontAwesomeIcon.LayerGroup, "Map Type", row.MapKind.ToString());
|
||||||
MetaRow(FontAwesomeIcon.Fingerprint, "Hash", row.Hash, UIColors.Get("LightlessBlue"));
|
MetaRow(FontAwesomeIcon.Fingerprint, "Hash", row.Hash, UIColors.Get("LightlessBlue"));
|
||||||
MetaRow(FontAwesomeIcon.InfoCircle, "Current Format", row.Format);
|
MetaRow(FontAwesomeIcon.InfoCircle, "Current Format", row.Format);
|
||||||
var resolution = GetTextureResolution(row);
|
|
||||||
var resolutionLabel = resolution.HasValue ? FormatTextureResolution(resolution.Value) : "Unknown";
|
|
||||||
MetaRow(FontAwesomeIcon.Images, "Resolution", resolutionLabel);
|
|
||||||
|
|
||||||
var selectedLabel = hasSelectedInfo ? selectedInfo!.Title : selectedTarget.ToString();
|
var selectedLabel = hasSelectedInfo ? selectedInfo!.Title : selectedTarget.ToString();
|
||||||
var selectionColor = hasSelectedInfo ? UIColors.Get("LightlessYellow") : UIColors.Get("LightlessGreen");
|
var selectionColor = hasSelectedInfo ? UIColors.Get("LightlessYellow") : UIColors.Get("LightlessGreen");
|
||||||
|
|||||||
@@ -164,25 +164,9 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
const float rounding = 6f;
|
const float rounding = 6f;
|
||||||
var shadowOffset = new Vector2(2, 2);
|
var shadowOffset = new Vector2(2, 2);
|
||||||
|
|
||||||
List<KeyValuePair<GameObjectHandler, Dictionary<string, FileDownloadStatus>>> transfers;
|
foreach (var transfer in _currentDownloads.ToList())
|
||||||
try
|
|
||||||
{
|
|
||||||
transfers = _currentDownloads.ToList();
|
|
||||||
}
|
|
||||||
catch (ArgumentException)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
foreach (var transfer in transfers)
|
|
||||||
{
|
{
|
||||||
var transferKey = transfer.Key;
|
var transferKey = transfer.Key;
|
||||||
|
|
||||||
// Skip if no valid game object
|
|
||||||
if (transferKey.GetGameObject() == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var rawPos = _dalamudUtilService.WorldToScreen(transferKey.GetGameObject());
|
var rawPos = _dalamudUtilService.WorldToScreen(transferKey.GetGameObject());
|
||||||
|
|
||||||
// If RawPos is zero, remove it from smoothed dictionary
|
// If RawPos is zero, remove it from smoothed dictionary
|
||||||
|
|||||||
@@ -46,12 +46,10 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
|||||||
private string? _lightfinderText;
|
private string? _lightfinderText;
|
||||||
private string? _lightfinderTooltip;
|
private string? _lightfinderTooltip;
|
||||||
private Colors _lightfinderColors;
|
private Colors _lightfinderColors;
|
||||||
private readonly object _localHashedCidLock = new();
|
|
||||||
private string? _localHashedCid;
|
private string? _localHashedCid;
|
||||||
private DateTime _localHashedCidFetchedAt = DateTime.MinValue;
|
private DateTime _localHashedCidFetchedAt = DateTime.MinValue;
|
||||||
private DateTime _localHashedCidNextErrorLog = DateTime.MinValue;
|
private DateTime _localHashedCidNextErrorLog = DateTime.MinValue;
|
||||||
private DateTime _pairRequestNextErrorLog = DateTime.MinValue;
|
private DateTime _pairRequestNextErrorLog = DateTime.MinValue;
|
||||||
private int _localHashedCidRefreshActive;
|
|
||||||
|
|
||||||
public DtrEntry(
|
public DtrEntry(
|
||||||
ILogger<DtrEntry> logger,
|
ILogger<DtrEntry> logger,
|
||||||
@@ -341,61 +339,29 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
|||||||
private string? GetLocalHashedCid()
|
private string? GetLocalHashedCid()
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
lock (_localHashedCidLock)
|
if (_localHashedCid is not null && now - _localHashedCidFetchedAt < _localHashedCidCacheDuration)
|
||||||
{
|
|
||||||
if (_localHashedCid is not null && now - _localHashedCidFetchedAt < _localHashedCidCacheDuration)
|
|
||||||
{
|
|
||||||
return _localHashedCid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QueueLocalHashedCidRefresh();
|
|
||||||
|
|
||||||
lock (_localHashedCidLock)
|
|
||||||
{
|
|
||||||
return _localHashedCid;
|
return _localHashedCid;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void QueueLocalHashedCidRefresh()
|
try
|
||||||
{
|
|
||||||
if (Interlocked.Exchange(ref _localHashedCidRefreshActive, 1) != 0)
|
|
||||||
{
|
{
|
||||||
return;
|
var cid = _dalamudUtilService.GetCIDAsync().GetAwaiter().GetResult();
|
||||||
|
var hashedCid = cid.ToString().GetHash256();
|
||||||
|
_localHashedCid = hashedCid;
|
||||||
|
_localHashedCidFetchedAt = now;
|
||||||
|
return hashedCid;
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
_ = Task.Run(async () =>
|
|
||||||
{
|
{
|
||||||
try
|
if (now >= _localHashedCidNextErrorLog)
|
||||||
{
|
{
|
||||||
var cid = _dalamudUtilService.GetCID();
|
_logger.LogDebug(ex, "Failed to refresh local hashed CID for Lightfinder DTR entry.");
|
||||||
var hashedCid = cid.ToString().GetHash256();
|
_localHashedCidNextErrorLog = now + _localHashedCidErrorCooldown;
|
||||||
lock (_localHashedCidLock)
|
|
||||||
{
|
|
||||||
_localHashedCid = hashedCid;
|
|
||||||
_localHashedCidFetchedAt = DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
lock (_localHashedCidLock)
|
|
||||||
{
|
|
||||||
if (now >= _localHashedCidNextErrorLog)
|
|
||||||
{
|
|
||||||
_logger.LogDebug(ex, "Failed to refresh local hashed CID for Lightfinder DTR entry.");
|
|
||||||
_localHashedCidNextErrorLog = now + _localHashedCidErrorCooldown;
|
|
||||||
}
|
|
||||||
|
|
||||||
_localHashedCid = null;
|
_localHashedCid = null;
|
||||||
_localHashedCidFetchedAt = now;
|
_localHashedCidFetchedAt = now;
|
||||||
}
|
return null;
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
Interlocked.Exchange(ref _localHashedCidRefreshActive, 0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<string> GetNearbyBroadcasts()
|
private List<string> GetNearbyBroadcasts()
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ namespace LightlessSync.UI
|
|||||||
private readonly LightFinderService _broadcastService;
|
private readonly LightFinderService _broadcastService;
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
private readonly LightFinderScannerService _broadcastScannerService;
|
private readonly LightFinderScannerService _broadcastScannerService;
|
||||||
private readonly LightFinderPlateHandler _lightFinderPlateHandler;
|
|
||||||
|
|
||||||
private IReadOnlyList<GroupFullInfoDto> _allSyncshells = Array.Empty<GroupFullInfoDto>();
|
private IReadOnlyList<GroupFullInfoDto> _allSyncshells = Array.Empty<GroupFullInfoDto>();
|
||||||
private string _userUid = string.Empty;
|
private string _userUid = string.Empty;
|
||||||
@@ -39,8 +38,7 @@ namespace LightlessSync.UI
|
|||||||
UiSharedService uiShared,
|
UiSharedService uiShared,
|
||||||
ApiController apiController,
|
ApiController apiController,
|
||||||
LightFinderScannerService broadcastScannerService
|
LightFinderScannerService broadcastScannerService
|
||||||
,
|
) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService)
|
||||||
LightFinderPlateHandler lightFinderPlateHandler) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService)
|
|
||||||
{
|
{
|
||||||
_broadcastService = broadcastService;
|
_broadcastService = broadcastService;
|
||||||
_uiSharedService = uiShared;
|
_uiSharedService = uiShared;
|
||||||
@@ -52,7 +50,6 @@ namespace LightlessSync.UI
|
|||||||
WindowBuilder.For(this)
|
WindowBuilder.For(this)
|
||||||
.SetSizeConstraints(new Vector2(600, 465), new Vector2(750, 525))
|
.SetSizeConstraints(new Vector2(600, 465), new Vector2(750, 525))
|
||||||
.Apply();
|
.Apply();
|
||||||
_lightFinderPlateHandler = lightFinderPlateHandler;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RebuildSyncshellDropdownOptions()
|
private void RebuildSyncshellDropdownOptions()
|
||||||
@@ -383,47 +380,9 @@ namespace LightlessSync.UI
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
if (ImGui.BeginTabItem("Debug"))
|
if (ImGui.BeginTabItem("Debug"))
|
||||||
{
|
{
|
||||||
if (ImGui.CollapsingHeader("LightFinder Plates", ImGuiTreeNodeFlags.DefaultOpen))
|
|
||||||
{
|
|
||||||
var h = _lightFinderPlateHandler;
|
|
||||||
|
|
||||||
var enabled = h.DebugEnabled;
|
|
||||||
if (ImGui.Checkbox("Enable LightFinder debug", ref enabled))
|
|
||||||
h.DebugEnabled = enabled;
|
|
||||||
|
|
||||||
if (h.DebugEnabled)
|
|
||||||
{
|
|
||||||
ImGui.Indent();
|
|
||||||
|
|
||||||
var disableOcc = h.DebugDisableOcclusion;
|
|
||||||
if (ImGui.Checkbox("Disable occlusion (force draw)", ref disableOcc))
|
|
||||||
h.DebugDisableOcclusion = disableOcc;
|
|
||||||
|
|
||||||
var drawUiRects = h.DebugDrawUiRects;
|
|
||||||
if (ImGui.Checkbox("Draw UI rects", ref drawUiRects))
|
|
||||||
h.DebugDrawUiRects = drawUiRects;
|
|
||||||
|
|
||||||
var drawLabelRects = h.DebugDrawLabelRects;
|
|
||||||
if (ImGui.Checkbox("Draw label rects", ref drawLabelRects))
|
|
||||||
h.DebugDrawLabelRects = drawLabelRects;
|
|
||||||
|
|
||||||
ImGui.Separator();
|
|
||||||
ImGui.TextUnformatted($"Labels last frame: {h.DebugLabelCountLastFrame}");
|
|
||||||
ImGui.TextUnformatted($"UI rects last frame: {h.DebugUiRectCountLastFrame}");
|
|
||||||
ImGui.TextUnformatted($"Occluded last frame: {h.DebugOccludedCountLastFrame}");
|
|
||||||
ImGui.TextUnformatted($"Last NamePlate frame: {h.DebugLastNameplateFrame}");
|
|
||||||
|
|
||||||
ImGui.Unindent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.Separator();
|
|
||||||
|
|
||||||
ImGui.Text("Broadcast Cache");
|
ImGui.Text("Broadcast Cache");
|
||||||
|
|
||||||
if (ImGui.BeginTable("##BroadcastCacheTable", 4,
|
if (ImGui.BeginTable("##BroadcastCacheTable", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY, new Vector2(-1, 225f)))
|
||||||
ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY,
|
|
||||||
new Vector2(-1, 225f)))
|
|
||||||
{
|
{
|
||||||
ImGui.TableSetupColumn("CID", ImGuiTableColumnFlags.WidthStretch);
|
ImGui.TableSetupColumn("CID", ImGuiTableColumnFlags.WidthStretch);
|
||||||
ImGui.TableSetupColumn("IsBroadcasting", ImGuiTableColumnFlags.WidthStretch);
|
ImGui.TableSetupColumn("IsBroadcasting", ImGuiTableColumnFlags.WidthStretch);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -222,9 +222,7 @@ public class AnimatedHeader
|
|||||||
|
|
||||||
if (ImGui.IsItemHovered() && !string.IsNullOrEmpty(button.Tooltip))
|
if (ImGui.IsItemHovered() && !string.IsNullOrEmpty(button.Tooltip))
|
||||||
{
|
{
|
||||||
ImGui.PushFont(UiBuilder.DefaultFont);
|
|
||||||
ImGui.SetTooltip(button.Tooltip);
|
ImGui.SetTooltip(button.Tooltip);
|
||||||
ImGui.PopFont();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
currentX -= buttonSize.X + spacing;
|
currentX -= buttonSize.X + spacing;
|
||||||
|
|||||||
@@ -297,25 +297,6 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
var ownerTab = ImRaii.TabItem("Owner Settings");
|
var ownerTab = ImRaii.TabItem("Owner Settings");
|
||||||
if (ownerTab)
|
if (ownerTab)
|
||||||
{
|
{
|
||||||
bool isChatDisabled = perm.IsDisableChat();
|
|
||||||
ImGui.AlignTextToFramePadding();
|
|
||||||
ImGui.TextUnformatted("Syncshell Chat");
|
|
||||||
_uiSharedService.BooleanToColoredIcon(!isChatDisabled);
|
|
||||||
ImGui.SameLine(230);
|
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, isChatDisabled ? UIColors.Get("PairBlue") : UIColors.Get("DimRed")))
|
|
||||||
{
|
|
||||||
if (_uiSharedService.IconTextButton(
|
|
||||||
isChatDisabled ? FontAwesomeIcon.Comment : FontAwesomeIcon.Ban,
|
|
||||||
isChatDisabled ? "Enable syncshell chat" : "Disable syncshell chat"))
|
|
||||||
{
|
|
||||||
perm.SetDisableChat(!isChatDisabled);
|
|
||||||
_ = _apiController.GroupChangeGroupPermissionState(new(GroupFullInfo.Group, perm));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
UiSharedService.AttachToolTip("Disables syncshell chat for all members.");
|
|
||||||
|
|
||||||
ImGuiHelpers.ScaledDummy(6f);
|
|
||||||
|
|
||||||
ImGui.AlignTextToFramePadding();
|
ImGui.AlignTextToFramePadding();
|
||||||
ImGui.TextUnformatted("New Password");
|
ImGui.TextUnformatted("New Password");
|
||||||
var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
|
var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
|
||||||
|
|||||||
@@ -140,10 +140,19 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts().ToList() ?? [];
|
string? myHashedCid = null;
|
||||||
_broadcastScannerService.TryGetLocalHashedCid(out var localHashedCid);
|
try
|
||||||
|
{
|
||||||
|
var cid = _dalamudUtilService.GetCID();
|
||||||
|
myHashedCid = cid.ToString().GetHash256();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Failed to get CID, not excluding own broadcast.");
|
||||||
|
}
|
||||||
|
var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts().Where(b => !string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal)).ToList() ?? [];
|
||||||
|
|
||||||
var cardData = new List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)>();
|
var cardData = new List<(GroupJoinDto Shell, string BroadcasterName)>();
|
||||||
|
|
||||||
foreach (var shell in _nearbySyncshells)
|
foreach (var shell in _nearbySyncshells)
|
||||||
{
|
{
|
||||||
@@ -176,15 +185,9 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
broadcasterName = !string.IsNullOrEmpty(worldName)
|
broadcasterName = !string.IsNullOrEmpty(worldName)
|
||||||
? $"{name} ({worldName})"
|
? $"{name} ({worldName})"
|
||||||
: name;
|
: name;
|
||||||
|
|
||||||
var isSelfBroadcast = !string.IsNullOrEmpty(localHashedCid)
|
|
||||||
&& string.Equals(broadcast.HashedCID, localHashedCid, StringComparison.Ordinal);
|
|
||||||
|
|
||||||
cardData.Add((shell, broadcasterName, isSelfBroadcast));
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cardData.Add((shell, broadcasterName, false));
|
cardData.Add((shell, broadcasterName));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cardData.Count == 0)
|
if (cardData.Count == 0)
|
||||||
@@ -207,7 +210,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
DrawConfirmation();
|
DrawConfirmation();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawSyncshellList(List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)> listData)
|
private void DrawSyncshellList(List<(GroupJoinDto Shell, string BroadcasterName)> listData)
|
||||||
{
|
{
|
||||||
const int shellsPerPage = 3;
|
const int shellsPerPage = 3;
|
||||||
var totalPages = (int)Math.Ceiling(listData.Count / (float)shellsPerPage);
|
var totalPages = (int)Math.Ceiling(listData.Count / (float)shellsPerPage);
|
||||||
@@ -224,10 +227,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
for (int index = firstIndex; index < lastExclusive; index++)
|
for (int index = firstIndex; index < lastExclusive; index++)
|
||||||
{
|
{
|
||||||
var (shell, broadcasterName, isSelfBroadcast) = listData[index];
|
var (shell, broadcasterName) = listData[index];
|
||||||
var broadcasterLabel = string.IsNullOrEmpty(broadcasterName)
|
|
||||||
? (isSelfBroadcast ? "You" : string.Empty)
|
|
||||||
: (isSelfBroadcast ? $"{broadcasterName} (You)" : broadcasterName);
|
|
||||||
|
|
||||||
ImGui.PushID(shell.Group.GID);
|
ImGui.PushID(shell.Group.GID);
|
||||||
float rowHeight = 74f * ImGuiHelpers.GlobalScale;
|
float rowHeight = 74f * ImGuiHelpers.GlobalScale;
|
||||||
@@ -239,7 +239,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
var style = ImGui.GetStyle();
|
var style = ImGui.GetStyle();
|
||||||
float startX = ImGui.GetCursorPosX();
|
float startX = ImGui.GetCursorPosX();
|
||||||
float regionW = ImGui.GetContentRegionAvail().X;
|
float regionW = ImGui.GetContentRegionAvail().X;
|
||||||
float rightTxtW = ImGui.CalcTextSize(broadcasterLabel).X;
|
float rightTxtW = ImGui.CalcTextSize(broadcasterName).X;
|
||||||
|
|
||||||
_uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple"));
|
_uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple"));
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
@@ -252,7 +252,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X;
|
float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X;
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.SetCursorPosX(rightX);
|
ImGui.SetCursorPosX(rightX);
|
||||||
ImGui.TextUnformatted(broadcasterLabel);
|
ImGui.TextUnformatted(broadcasterName);
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
ImGui.SetTooltip("Broadcaster of the syncshell.");
|
ImGui.SetTooltip("Broadcaster of the syncshell.");
|
||||||
|
|
||||||
@@ -291,7 +291,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
float joinX = rowStartLocal.X + (tagsWidth > 0 ? tagsWidth + style.ItemSpacing.X : 0f);
|
float joinX = rowStartLocal.X + (tagsWidth > 0 ? tagsWidth + style.ItemSpacing.X : 0f);
|
||||||
|
|
||||||
ImGui.SetCursorPos(new Vector2(joinX, btnBaselineY));
|
ImGui.SetCursorPos(new Vector2(joinX, btnBaselineY));
|
||||||
DrawJoinButton(shell, isSelfBroadcast);
|
DrawJoinButton(shell);
|
||||||
|
|
||||||
float btnHeight = ImGui.GetFrameHeightWithSpacing();
|
float btnHeight = ImGui.GetFrameHeightWithSpacing();
|
||||||
float rowHeightUsed = MathF.Max(tagsHeight, btnHeight);
|
float rowHeightUsed = MathF.Max(tagsHeight, btnHeight);
|
||||||
@@ -311,7 +311,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
DrawPagination(totalPages);
|
DrawPagination(totalPages);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawSyncshellGrid(List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)> cardData)
|
private void DrawSyncshellGrid(List<(GroupJoinDto Shell, string BroadcasterName)> cardData)
|
||||||
{
|
{
|
||||||
const int shellsPerPage = 4;
|
const int shellsPerPage = 4;
|
||||||
var totalPages = (int)Math.Ceiling(cardData.Count / (float)shellsPerPage);
|
var totalPages = (int)Math.Ceiling(cardData.Count / (float)shellsPerPage);
|
||||||
@@ -336,10 +336,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
for (int index = firstIndex; index < lastExclusive; index++)
|
for (int index = firstIndex; index < lastExclusive; index++)
|
||||||
{
|
{
|
||||||
var localIndex = index - firstIndex;
|
var localIndex = index - firstIndex;
|
||||||
var (shell, broadcasterName, isSelfBroadcast) = cardData[index];
|
var (shell, broadcasterName) = cardData[index];
|
||||||
var broadcasterLabel = string.IsNullOrEmpty(broadcasterName)
|
|
||||||
? (isSelfBroadcast ? "You" : string.Empty)
|
|
||||||
: (isSelfBroadcast ? $"{broadcasterName} (You)" : broadcasterName);
|
|
||||||
|
|
||||||
if (localIndex % 2 != 0)
|
if (localIndex % 2 != 0)
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
@@ -376,17 +373,17 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
float maxBroadcasterWidth = regionRightX - minBroadcasterX;
|
float maxBroadcasterWidth = regionRightX - minBroadcasterX;
|
||||||
|
|
||||||
string broadcasterToShow = broadcasterLabel;
|
string broadcasterToShow = broadcasterName;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(broadcasterLabel) && maxBroadcasterWidth > 0f)
|
if (!string.IsNullOrEmpty(broadcasterName) && maxBroadcasterWidth > 0f)
|
||||||
{
|
{
|
||||||
float bcFullWidth = ImGui.CalcTextSize(broadcasterLabel).X;
|
float bcFullWidth = ImGui.CalcTextSize(broadcasterName).X;
|
||||||
string toolTip;
|
string toolTip;
|
||||||
|
|
||||||
if (bcFullWidth > maxBroadcasterWidth)
|
if (bcFullWidth > maxBroadcasterWidth)
|
||||||
{
|
{
|
||||||
broadcasterToShow = TruncateTextToWidth(broadcasterLabel, maxBroadcasterWidth);
|
broadcasterToShow = TruncateTextToWidth(broadcasterName, maxBroadcasterWidth);
|
||||||
toolTip = broadcasterLabel + Environment.NewLine + Environment.NewLine + "Broadcaster of the syncshell.";
|
toolTip = broadcasterName + Environment.NewLine + Environment.NewLine + "Broadcaster of the syncshell.";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -446,7 +443,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
if (remainingY > 0)
|
if (remainingY > 0)
|
||||||
ImGui.Dummy(new Vector2(0, remainingY));
|
ImGui.Dummy(new Vector2(0, remainingY));
|
||||||
|
|
||||||
DrawJoinButton(shell, isSelfBroadcast);
|
DrawJoinButton(shell);
|
||||||
|
|
||||||
ImGui.EndChild();
|
ImGui.EndChild();
|
||||||
ImGui.EndGroup();
|
ImGui.EndGroup();
|
||||||
@@ -492,7 +489,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawJoinButton(GroupJoinDto shell, bool isSelfBroadcast)
|
private void DrawJoinButton(dynamic shell)
|
||||||
{
|
{
|
||||||
const string visibleLabel = "Join";
|
const string visibleLabel = "Join";
|
||||||
var label = $"{visibleLabel}##{shell.Group.GID}";
|
var label = $"{visibleLabel}##{shell.Group.GID}";
|
||||||
@@ -520,7 +517,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
buttonSize = new Vector2(-1, 0);
|
buttonSize = new Vector2(-1, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAlreadyMember && !isRecentlyJoined && !isSelfBroadcast)
|
if (!isAlreadyMember && !isRecentlyJoined)
|
||||||
{
|
{
|
||||||
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen"));
|
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen"));
|
||||||
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f));
|
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f));
|
||||||
@@ -570,9 +567,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
ImGui.Button(label, buttonSize);
|
ImGui.Button(label, buttonSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
UiSharedService.AttachToolTip(isSelfBroadcast
|
UiSharedService.AttachToolTip("Already a member or owner of this Syncshell.");
|
||||||
? "This is your own Syncshell."
|
|
||||||
: "Already a member or owner of this Syncshell.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.PopStyleColor(3);
|
ImGui.PopStyleColor(3);
|
||||||
|
|||||||
@@ -440,7 +440,7 @@ public class TopTabMenu
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var myCidHash = _dalamudUtilService.GetCID().ToString().GetHash256();
|
var myCidHash = (await _dalamudUtilService.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
|
||||||
await _apiController.TryPairWithContentId(request.HashedCid).ConfigureAwait(false);
|
await _apiController.TryPairWithContentId(request.HashedCid).ConfigureAwait(false);
|
||||||
_pairRequestService.RemoveRequest(request.HashedCid);
|
_pairRequestService.RemoveRequest(request.HashedCid);
|
||||||
|
|
||||||
@@ -781,8 +781,7 @@ public class TopTabMenu
|
|||||||
{
|
{
|
||||||
var buttonX = (availableWidth - (spacingX)) / 2f;
|
var buttonX = (availableWidth - (spacingX)) / 2f;
|
||||||
|
|
||||||
var lightFinderLabel = GetLightfinderFinderLabel();
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCirclePlus, "Lightfinder", buttonX, center: true))
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCirclePlus, lightFinderLabel, buttonX, center: true))
|
|
||||||
{
|
{
|
||||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
|
_lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
|
||||||
}
|
}
|
||||||
@@ -796,28 +795,27 @@ public class TopTabMenu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetLightfinderFinderLabel()
|
|
||||||
{
|
|
||||||
string label = "Lightfinder";
|
|
||||||
|
|
||||||
if (_lightFinderService.IsBroadcasting)
|
|
||||||
{
|
|
||||||
var hashExclude = _dalamudUtilService.GetCID().ToString().GetHash256();
|
|
||||||
var nearbyCount = _lightFinderScannerService.GetActiveBroadcasts(hashExclude).Count;
|
|
||||||
return $"{label} ({nearbyCount})";
|
|
||||||
}
|
|
||||||
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetSyncshellFinderLabel()
|
private string GetSyncshellFinderLabel()
|
||||||
{
|
{
|
||||||
if (!_lightFinderService.IsBroadcasting)
|
if (!_lightFinderService.IsBroadcasting)
|
||||||
return "Syncshell Finder";
|
return "Syncshell Finder";
|
||||||
|
|
||||||
|
string? myHashedCid = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cid = _dalamudUtilService.GetCID();
|
||||||
|
myHashedCid = cid.ToString().GetHash256();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Couldnt get own CID, log and return default table
|
||||||
|
}
|
||||||
|
|
||||||
var nearbyCount = _lightFinderScannerService
|
var nearbyCount = _lightFinderScannerService
|
||||||
.GetActiveSyncshellBroadcasts(excludeLocal: true)
|
.GetActiveSyncshellBroadcasts()
|
||||||
.Where(b => !string.IsNullOrEmpty(b.GID))
|
.Where(b =>
|
||||||
|
!string.IsNullOrEmpty(b.GID) &&
|
||||||
|
!string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal))
|
||||||
.Select(b => b.GID!)
|
.Select(b => b.GID!)
|
||||||
.Distinct(StringComparer.Ordinal)
|
.Distinct(StringComparer.Ordinal)
|
||||||
.Count();
|
.Count();
|
||||||
|
|||||||
@@ -947,16 +947,13 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_discordOAuthCheck != null && _discordOAuthCheck.IsCompleted && _discordOAuthCheck.Result != null)
|
if (_discordOAuthCheck != null && _discordOAuthCheck.IsCompleted)
|
||||||
{
|
{
|
||||||
if (_discordOAuthGetCode == null)
|
if (IconTextButton(FontAwesomeIcon.ArrowRight, "Authenticate with Server"))
|
||||||
{
|
{
|
||||||
if (IconTextButton(FontAwesomeIcon.ArrowRight, "Authenticate with Server"))
|
_discordOAuthGetCode = _serverConfigurationManager.GetDiscordOAuthToken(_discordOAuthCheck.Result!, selectedServer.ServerUri, _discordOAuthGetCts.Token);
|
||||||
{
|
|
||||||
_discordOAuthGetCode = _serverConfigurationManager.GetDiscordOAuthToken(_discordOAuthCheck.Result, selectedServer.ServerUri, _discordOAuthGetCts.Token);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (!_discordOAuthGetCode.IsCompleted)
|
else if (_discordOAuthGetCode != null && !_discordOAuthGetCode.IsCompleted)
|
||||||
{
|
{
|
||||||
TextWrapped("A browser window has been opened, follow it to authenticate. Click the button below if you accidentally closed the window and need to restart the authentication.");
|
TextWrapped("A browser window has been opened, follow it to authenticate. Click the button below if you accidentally closed the window and need to restart the authentication.");
|
||||||
if (IconTextButton(FontAwesomeIcon.Ban, "Cancel Authentication"))
|
if (IconTextButton(FontAwesomeIcon.Ban, "Cancel Authentication"))
|
||||||
@@ -965,7 +962,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
_discordOAuthGetCode = null;
|
_discordOAuthGetCode = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else if (_discordOAuthGetCode != null && _discordOAuthGetCode.IsCompleted)
|
||||||
{
|
{
|
||||||
TextWrapped("Discord OAuth is completed, status: ");
|
TextWrapped("Discord OAuth is completed, status: ");
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,10 @@
|
|||||||
namespace LightlessSync.UtilsEnum.Enum
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace LightlessSync.UtilsEnum.Enum
|
||||||
{
|
{
|
||||||
public enum LabelAlignment
|
public enum LabelAlignment
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace LightlessSync.UtilsEnum.Enum
|
|
||||||
{
|
|
||||||
public enum LightfinderLabelRenderer
|
|
||||||
{
|
|
||||||
Pictomancy,
|
|
||||||
SignatureHook,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -60,6 +60,16 @@ public static class VariousExtensions
|
|||||||
CharacterData? oldData, ILogger logger, IPairPerformanceSubject cachedPlayer, bool forceApplyCustomization, bool forceApplyMods)
|
CharacterData? oldData, ILogger logger, IPairPerformanceSubject cachedPlayer, bool forceApplyCustomization, bool forceApplyMods)
|
||||||
{
|
{
|
||||||
oldData ??= new();
|
oldData ??= new();
|
||||||
|
static bool FileReplacementsEquivalent(ICollection<FileReplacementData> left, ICollection<FileReplacementData> right)
|
||||||
|
{
|
||||||
|
if (left.Count != right.Count)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var comparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer.Instance;
|
||||||
|
return !left.Except(right, comparer).Any() && !right.Except(left, comparer).Any();
|
||||||
|
}
|
||||||
|
|
||||||
var charaDataToUpdate = new Dictionary<ObjectKind, HashSet<PlayerChanges>>();
|
var charaDataToUpdate = new Dictionary<ObjectKind, HashSet<PlayerChanges>>();
|
||||||
foreach (ObjectKind objectKind in Enum.GetValues<ObjectKind>())
|
foreach (ObjectKind objectKind in Enum.GetValues<ObjectKind>())
|
||||||
@@ -95,7 +105,7 @@ public static class VariousExtensions
|
|||||||
{
|
{
|
||||||
var oldList = oldData.FileReplacements[objectKind];
|
var oldList = oldData.FileReplacements[objectKind];
|
||||||
var newList = newData.FileReplacements[objectKind];
|
var newList = newData.FileReplacements[objectKind];
|
||||||
var listsAreEqual = oldList.SequenceEqual(newList, PlayerData.Data.FileReplacementDataComparer.Instance);
|
var listsAreEqual = FileReplacementsEquivalent(oldList, newList);
|
||||||
if (!listsAreEqual || forceApplyMods)
|
if (!listsAreEqual || forceApplyMods)
|
||||||
{
|
{
|
||||||
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (FileReplacements not equal) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModFiles);
|
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (FileReplacements not equal) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModFiles);
|
||||||
@@ -118,9 +128,9 @@ public static class VariousExtensions
|
|||||||
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
var newTail = newFileReplacements.Where(g => g.GamePaths.Any(p => p.Contains("/tail/", StringComparison.OrdinalIgnoreCase)))
|
var newTail = newFileReplacements.Where(g => g.GamePaths.Any(p => p.Contains("/tail/", StringComparison.OrdinalIgnoreCase)))
|
||||||
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
var existingTransients = existingFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl") && !g.EndsWith("tex") && !g.EndsWith("mtrl")))
|
var existingTransients = existingFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl", StringComparison.OrdinalIgnoreCase) && !g.EndsWith("tex", StringComparison.OrdinalIgnoreCase) && !g.EndsWith("mtrl", StringComparison.OrdinalIgnoreCase)))
|
||||||
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
var newTransients = newFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl") && !g.EndsWith("tex") && !g.EndsWith("mtrl")))
|
var newTransients = newFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl", StringComparison.OrdinalIgnoreCase) && !g.EndsWith("tex", StringComparison.OrdinalIgnoreCase) && !g.EndsWith("mtrl", StringComparison.OrdinalIgnoreCase)))
|
||||||
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
|
||||||
logger.LogTrace("[BASE-{appbase}] ExistingFace: {of}, NewFace: {fc}; ExistingHair: {eh}, NewHair: {nh}; ExistingTail: {et}, NewTail: {nt}; ExistingTransient: {etr}, NewTransient: {ntr}", applicationBase,
|
logger.LogTrace("[BASE-{appbase}] ExistingFace: {of}, NewFace: {fc}; ExistingHair: {eh}, NewHair: {nh}; ExistingTail: {et}, NewTail: {nt}; ExistingTransient: {etr}, NewTransient: {ntr}", applicationBase,
|
||||||
@@ -167,7 +177,8 @@ public static class VariousExtensions
|
|||||||
if (objectKind != ObjectKind.Player) continue;
|
if (objectKind != ObjectKind.Player) continue;
|
||||||
|
|
||||||
bool manipDataDifferent = !string.Equals(oldData.ManipulationData, newData.ManipulationData, StringComparison.Ordinal);
|
bool manipDataDifferent = !string.Equals(oldData.ManipulationData, newData.ManipulationData, StringComparison.Ordinal);
|
||||||
if (manipDataDifferent || forceApplyMods)
|
var hasManipulationData = !string.IsNullOrEmpty(newData.ManipulationData);
|
||||||
|
if (manipDataDifferent || (forceApplyMods && hasManipulationData))
|
||||||
{
|
{
|
||||||
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff manip data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModManip);
|
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff manip data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModManip);
|
||||||
charaDataToUpdate[objectKind].Add(PlayerChanges.ModManip);
|
charaDataToUpdate[objectKind].Add(PlayerChanges.ModManip);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -18,72 +18,56 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
|||||||
private readonly LightlessConfigService _lightlessConfig;
|
private readonly LightlessConfigService _lightlessConfig;
|
||||||
private readonly object _semaphoreModificationLock = new();
|
private readonly object _semaphoreModificationLock = new();
|
||||||
private readonly TokenProvider _tokenProvider;
|
private readonly TokenProvider _tokenProvider;
|
||||||
|
|
||||||
private int _availableDownloadSlots;
|
private int _availableDownloadSlots;
|
||||||
private SemaphoreSlim _downloadSemaphore;
|
private SemaphoreSlim _downloadSemaphore;
|
||||||
|
|
||||||
private int CurrentlyUsedDownloadSlots => _availableDownloadSlots - _downloadSemaphore.CurrentCount;
|
private int CurrentlyUsedDownloadSlots => _availableDownloadSlots - _downloadSemaphore.CurrentCount;
|
||||||
|
|
||||||
public FileTransferOrchestrator(
|
public FileTransferOrchestrator(ILogger<FileTransferOrchestrator> logger, LightlessConfigService lightlessConfig,
|
||||||
ILogger<FileTransferOrchestrator> logger,
|
LightlessMediator mediator, TokenProvider tokenProvider, HttpClient httpClient) : base(logger, mediator)
|
||||||
LightlessConfigService lightlessConfig,
|
|
||||||
LightlessMediator mediator,
|
|
||||||
TokenProvider tokenProvider,
|
|
||||||
HttpClient httpClient) : base(logger, mediator)
|
|
||||||
{
|
{
|
||||||
_lightlessConfig = lightlessConfig;
|
_lightlessConfig = lightlessConfig;
|
||||||
_tokenProvider = tokenProvider;
|
_tokenProvider = tokenProvider;
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
|
|
||||||
var ver = Assembly.GetExecutingAssembly().GetName().Version;
|
var ver = Assembly.GetExecutingAssembly().GetName().Version;
|
||||||
_httpClient.DefaultRequestHeaders.UserAgent.Add(
|
_httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("LightlessSync", ver!.Major + "." + ver!.Minor + "." + ver!.Build));
|
||||||
new ProductInfoHeaderValue("LightlessSync", $"{ver!.Major}.{ver.Minor}.{ver.Build}"));
|
|
||||||
|
|
||||||
_availableDownloadSlots = Math.Max(1, lightlessConfig.Current.ParallelDownloads);
|
_availableDownloadSlots = lightlessConfig.Current.ParallelDownloads;
|
||||||
_downloadSemaphore = new SemaphoreSlim(_availableDownloadSlots, _availableDownloadSlots);
|
_downloadSemaphore = new(_availableDownloadSlots, _availableDownloadSlots);
|
||||||
|
|
||||||
Mediator.Subscribe<ConnectedMessage>(this, msg => FilesCdnUri = msg.Connection.ServerInfo.FileServerAddress);
|
Mediator.Subscribe<ConnectedMessage>(this, (msg) =>
|
||||||
Mediator.Subscribe<DisconnectedMessage>(this, _ => FilesCdnUri = null);
|
{
|
||||||
Mediator.Subscribe<DownloadReadyMessage>(this, msg => _downloadReady[msg.RequestId] = true);
|
FilesCdnUri = msg.Connection.ServerInfo.FileServerAddress;
|
||||||
|
});
|
||||||
|
|
||||||
|
Mediator.Subscribe<DisconnectedMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
FilesCdnUri = null;
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<DownloadReadyMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
_downloadReady[msg.RequestId] = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Files CDN Uri from server
|
|
||||||
/// </summary>
|
|
||||||
public Uri? FilesCdnUri { private set; get; }
|
public Uri? FilesCdnUri { private set; get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Forbidden file transfers given by server
|
|
||||||
/// </summary>
|
|
||||||
public List<FileTransfer> ForbiddenTransfers { get; } = [];
|
public List<FileTransfer> ForbiddenTransfers { get; } = [];
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Is the FileTransferOrchestrator initialized
|
|
||||||
/// </summary>
|
|
||||||
public bool IsInitialized => FilesCdnUri != null;
|
public bool IsInitialized => FilesCdnUri != null;
|
||||||
|
|
||||||
/// <summary>
|
public void ClearDownloadRequest(Guid guid)
|
||||||
/// Configured parallel downloads in settings (ParallelDownloads)
|
{
|
||||||
/// </summary>
|
_downloadReady.Remove(guid, out _);
|
||||||
public int ConfiguredParallelDownloads => Math.Max(1, _lightlessConfig.Current.ParallelDownloads);
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clears the download request for the given guid
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="guid">Guid of download request</param>
|
|
||||||
public void ClearDownloadRequest(Guid guid) => _downloadReady.Remove(guid, out _);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Is the download ready for the given guid
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="guid">Guid of download request</param>
|
|
||||||
/// <returns>Completion of the download</returns>
|
|
||||||
public bool IsDownloadReady(Guid guid)
|
public bool IsDownloadReady(Guid guid)
|
||||||
=> _downloadReady.TryGetValue(guid, out bool isReady) && isReady;
|
{
|
||||||
|
if (_downloadReady.TryGetValue(guid, out bool isReady) && isReady)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Release a download slot after download is complete
|
|
||||||
/// </summary>
|
|
||||||
public void ReleaseDownloadSlot()
|
public void ReleaseDownloadSlot()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -97,26 +81,60 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public async Task<HttpResponseMessage> SendRequestAsync(HttpMethod method, Uri uri,
|
||||||
/// Wait for an available download slot asyncronously
|
CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead,
|
||||||
/// </summary>
|
bool withToken = true)
|
||||||
/// <param name="token">Cancellation Token</param>
|
{
|
||||||
/// <returns>Task of the slot</returns>
|
return await SendRequestInternalAsync(() => new HttpRequestMessage(method, uri),
|
||||||
|
ct, httpCompletionOption, withToken, allowRetry: true).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<HttpResponseMessage> SendRequestAsync<T>(HttpMethod method, Uri uri, T content, CancellationToken ct,
|
||||||
|
bool withToken = true) where T : class
|
||||||
|
{
|
||||||
|
return await SendRequestInternalAsync(() =>
|
||||||
|
{
|
||||||
|
var requestMessage = new HttpRequestMessage(method, uri);
|
||||||
|
if (content is not ByteArrayContent byteArrayContent)
|
||||||
|
{
|
||||||
|
requestMessage.Content = JsonContent.Create(content);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var clonedContent = new ByteArrayContent(byteArrayContent.ReadAsByteArrayAsync().GetAwaiter().GetResult());
|
||||||
|
foreach (var header in byteArrayContent.Headers)
|
||||||
|
{
|
||||||
|
clonedContent.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||||
|
}
|
||||||
|
requestMessage.Content = clonedContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestMessage;
|
||||||
|
}, ct, HttpCompletionOption.ResponseContentRead, withToken,
|
||||||
|
allowRetry: content is not HttpContent || content is ByteArrayContent).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<HttpResponseMessage> SendRequestStreamAsync(HttpMethod method, Uri uri, ProgressableStreamContent content,
|
||||||
|
CancellationToken ct, bool withToken = true)
|
||||||
|
{
|
||||||
|
return await SendRequestInternalAsync(() =>
|
||||||
|
{
|
||||||
|
var requestMessage = new HttpRequestMessage(method, uri)
|
||||||
|
{
|
||||||
|
Content = content
|
||||||
|
};
|
||||||
|
return requestMessage;
|
||||||
|
}, ct, HttpCompletionOption.ResponseContentRead, withToken, allowRetry: false).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task WaitForDownloadSlotAsync(CancellationToken token)
|
public async Task WaitForDownloadSlotAsync(CancellationToken token)
|
||||||
{
|
{
|
||||||
lock (_semaphoreModificationLock)
|
lock (_semaphoreModificationLock)
|
||||||
{
|
{
|
||||||
var desired = Math.Max(1, _lightlessConfig.Current.ParallelDownloads);
|
if (_availableDownloadSlots != _lightlessConfig.Current.ParallelDownloads && _availableDownloadSlots == _downloadSemaphore.CurrentCount)
|
||||||
|
|
||||||
if (_availableDownloadSlots != desired &&
|
|
||||||
_availableDownloadSlots == _downloadSemaphore.CurrentCount)
|
|
||||||
{
|
{
|
||||||
_availableDownloadSlots = desired;
|
_availableDownloadSlots = _lightlessConfig.Current.ParallelDownloads;
|
||||||
|
_downloadSemaphore = new(_availableDownloadSlots, _availableDownloadSlots);
|
||||||
var old = _downloadSemaphore;
|
|
||||||
_downloadSemaphore = new SemaphoreSlim(_availableDownloadSlots, _availableDownloadSlots);
|
|
||||||
|
|
||||||
try { old.Dispose(); } catch { /* ignore */ }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,15 +142,10 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
|||||||
Mediator.Publish(new DownloadLimitChangedMessage());
|
Mediator.Publish(new DownloadLimitChangedMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Download limit per slot in bytes
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Bytes of the download limit</returns>
|
|
||||||
public long DownloadLimitPerSlot()
|
public long DownloadLimitPerSlot()
|
||||||
{
|
{
|
||||||
var limit = _lightlessConfig.Current.DownloadSpeedLimitInBytes;
|
var limit = _lightlessConfig.Current.DownloadSpeedLimitInBytes;
|
||||||
if (limit <= 0) return 0;
|
if (limit <= 0) return 0;
|
||||||
|
|
||||||
limit = _lightlessConfig.Current.DownloadSpeedType switch
|
limit = _lightlessConfig.Current.DownloadSpeedType switch
|
||||||
{
|
{
|
||||||
LightlessConfiguration.Models.DownloadSpeeds.Bps => limit,
|
LightlessConfiguration.Models.DownloadSpeeds.Bps => limit,
|
||||||
@@ -140,113 +153,22 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
|||||||
LightlessConfiguration.Models.DownloadSpeeds.MBps => limit * 1024 * 1024,
|
LightlessConfiguration.Models.DownloadSpeeds.MBps => limit * 1024 * 1024,
|
||||||
_ => limit,
|
_ => limit,
|
||||||
};
|
};
|
||||||
|
var currentUsedDlSlots = CurrentlyUsedDownloadSlots;
|
||||||
var usedSlots = CurrentlyUsedDownloadSlots;
|
var avaialble = _availableDownloadSlots;
|
||||||
var divided = limit / (usedSlots <= 0 ? 1 : usedSlots);
|
var currentCount = _downloadSemaphore.CurrentCount;
|
||||||
|
var dividedLimit = limit / (currentUsedDlSlots == 0 ? 1 : currentUsedDlSlots);
|
||||||
if (divided < 0)
|
if (dividedLimit < 0)
|
||||||
{
|
{
|
||||||
Logger.LogWarning(
|
Logger.LogWarning("Calculated Bandwidth Limit is negative, returning Infinity: {value}, CurrentlyUsedDownloadSlots is {currentSlots}, " +
|
||||||
"Calculated Bandwidth Limit is negative, returning Infinity: {value}, usedSlots={usedSlots}, limit={limit}, avail={avail}, currentCount={count}",
|
"DownloadSpeedLimit is {limit}, available slots: {avail}, current count: {count}", dividedLimit, currentUsedDlSlots, limit, avaialble, currentCount);
|
||||||
divided, usedSlots, limit, _availableDownloadSlots, _downloadSemaphore.CurrentCount);
|
|
||||||
return long.MaxValue;
|
return long.MaxValue;
|
||||||
}
|
}
|
||||||
|
return Math.Clamp(dividedLimit, 1, long.MaxValue);
|
||||||
return Math.Clamp(divided, 1, long.MaxValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private async Task<HttpResponseMessage> SendRequestInternalAsync(Func<HttpRequestMessage> requestFactory,
|
||||||
/// sends an HTTP request without content serialization
|
CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead,
|
||||||
/// </summary>
|
bool withToken = true, bool allowRetry = true)
|
||||||
/// <param name="method">HttpMethod for the request</param>
|
|
||||||
/// <param name="uri">Uri for the request</param>
|
|
||||||
/// <param name="ct">Cancellation Token</param>
|
|
||||||
/// <param name="httpCompletionOption">Enum of HttpCollectionOption</param>
|
|
||||||
/// <param name="withToken">Include Cancellation Token</param>
|
|
||||||
/// <returns>Http response of the request</returns>
|
|
||||||
public async Task<HttpResponseMessage> SendRequestAsync(
|
|
||||||
HttpMethod method,
|
|
||||||
Uri uri,
|
|
||||||
CancellationToken? ct = null,
|
|
||||||
HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead,
|
|
||||||
bool withToken = true)
|
|
||||||
{
|
|
||||||
return await SendRequestInternalAsync(
|
|
||||||
() => new HttpRequestMessage(method, uri),
|
|
||||||
ct,
|
|
||||||
httpCompletionOption,
|
|
||||||
withToken,
|
|
||||||
allowRetry: true).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sends an HTTP request with JSON content serialization
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">HttpResponseMessage</typeparam>
|
|
||||||
/// <param name="method">Http method</param>
|
|
||||||
/// <param name="uri">Url of the direct download link</param>
|
|
||||||
/// <param name="content">content of the request</param>
|
|
||||||
/// <param name="ct">cancellation token</param>
|
|
||||||
/// <param name="withToken">include cancellation token</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<HttpResponseMessage> SendRequestAsync<T>(
|
|
||||||
HttpMethod method,
|
|
||||||
Uri uri,
|
|
||||||
T content,
|
|
||||||
CancellationToken ct,
|
|
||||||
bool withToken = true) where T : class
|
|
||||||
{
|
|
||||||
return await SendRequestInternalAsync(() =>
|
|
||||||
{
|
|
||||||
var requestMessage = new HttpRequestMessage(method, uri);
|
|
||||||
|
|
||||||
if (content is ByteArrayContent byteArrayContent)
|
|
||||||
{
|
|
||||||
var bytes = byteArrayContent.ReadAsByteArrayAsync(ct).GetAwaiter().GetResult();
|
|
||||||
var cloned = new ByteArrayContent(bytes);
|
|
||||||
foreach (var header in byteArrayContent.Headers)
|
|
||||||
cloned.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
|
||||||
|
|
||||||
requestMessage.Content = cloned;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
requestMessage.Content = JsonContent.Create(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
return requestMessage;
|
|
||||||
}, ct, HttpCompletionOption.ResponseContentRead, withToken,
|
|
||||||
allowRetry: content is not HttpContent || content is ByteArrayContent).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<HttpResponseMessage> SendRequestStreamAsync(
|
|
||||||
HttpMethod method,
|
|
||||||
Uri uri,
|
|
||||||
ProgressableStreamContent content,
|
|
||||||
CancellationToken ct,
|
|
||||||
bool withToken = true)
|
|
||||||
{
|
|
||||||
return await SendRequestInternalAsync(() =>
|
|
||||||
{
|
|
||||||
return new HttpRequestMessage(method, uri) { Content = content };
|
|
||||||
}, ct, HttpCompletionOption.ResponseContentRead, withToken, allowRetry: false).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// sends an HTTP request with optional retry logic for transient network errors
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="requestFactory">Request factory</param>
|
|
||||||
/// <param name="ct">Cancellation Token</param>
|
|
||||||
/// <param name="httpCompletionOption">Http Options</param>
|
|
||||||
/// <param name="withToken">With cancellation token</param>
|
|
||||||
/// <param name="allowRetry">Allows retry of request</param>
|
|
||||||
/// <returns>Response message of request</returns>
|
|
||||||
private async Task<HttpResponseMessage> SendRequestInternalAsync(
|
|
||||||
Func<HttpRequestMessage> requestFactory,
|
|
||||||
CancellationToken? ct = null,
|
|
||||||
HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead,
|
|
||||||
bool withToken = true,
|
|
||||||
bool allowRetry = true)
|
|
||||||
{
|
{
|
||||||
const int maxAttempts = 2;
|
const int maxAttempts = 2;
|
||||||
var attempt = 0;
|
var attempt = 0;
|
||||||
@@ -262,11 +184,8 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
|||||||
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestMessage.Content != null &&
|
if (requestMessage.Content != null && requestMessage.Content is not StreamContent && requestMessage.Content is not ByteArrayContent)
|
||||||
requestMessage.Content is not StreamContent &&
|
|
||||||
requestMessage.Content is not ByteArrayContent)
|
|
||||||
{
|
{
|
||||||
// log content for debugging
|
|
||||||
var content = await ((JsonContent)requestMessage.Content).ReadAsStringAsync().ConfigureAwait(false);
|
var content = await ((JsonContent)requestMessage.Content).ReadAsStringAsync().ConfigureAwait(false);
|
||||||
Logger.LogDebug("Sending {method} to {uri} (Content: {content})", requestMessage.Method, requestMessage.RequestUri, content);
|
Logger.LogDebug("Sending {method} to {uri} (Content: {content})", requestMessage.Method, requestMessage.RequestUri, content);
|
||||||
}
|
}
|
||||||
@@ -277,10 +196,9 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// send request
|
if (ct != null)
|
||||||
return ct != null
|
return await _httpClient.SendAsync(requestMessage, httpCompletionOption, ct.Value).ConfigureAwait(false);
|
||||||
? await _httpClient.SendAsync(requestMessage, httpCompletionOption, ct.Value).ConfigureAwait(false)
|
return await _httpClient.SendAsync(requestMessage, httpCompletionOption).ConfigureAwait(false);
|
||||||
: await _httpClient.SendAsync(requestMessage, httpCompletionOption).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
catch (TaskCanceledException)
|
catch (TaskCanceledException)
|
||||||
{
|
{
|
||||||
@@ -290,11 +208,14 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
Logger.LogWarning(ex, "Transient error during SendRequestInternal for {uri}, retrying attempt {attempt}/{maxAttempts}",
|
Logger.LogWarning(ex, "Transient error during SendRequestInternal for {uri}, retrying attempt {attempt}/{maxAttempts}",
|
||||||
requestMessage.RequestUri, attempt, maxAttempts);
|
requestMessage.RequestUri, attempt, maxAttempts);
|
||||||
|
|
||||||
if (ct.HasValue)
|
if (ct.HasValue)
|
||||||
|
{
|
||||||
await Task.Delay(TimeSpan.FromMilliseconds(200), ct.Value).ConfigureAwait(false);
|
await Task.Delay(TimeSpan.FromMilliseconds(200), ct.Value).ConfigureAwait(false);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
await Task.Delay(TimeSpan.FromMilliseconds(200)).ConfigureAwait(false);
|
await Task.Delay(TimeSpan.FromMilliseconds(200)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -304,11 +225,6 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Is the exception a transient network exception
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="ex">expection</param>
|
|
||||||
/// <returns>Is transient network expection</returns>
|
|
||||||
private static bool IsTransientNetworkException(Exception ex)
|
private static bool IsTransientNetworkException(Exception ex)
|
||||||
{
|
{
|
||||||
var current = ex;
|
var current = ex;
|
||||||
@@ -316,13 +232,12 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (current is SocketException socketEx)
|
if (current is SocketException socketEx)
|
||||||
{
|
{
|
||||||
return socketEx.SocketErrorCode is
|
return socketEx.SocketErrorCode is SocketError.ConnectionReset or SocketError.ConnectionAborted or SocketError.TimedOut;
|
||||||
SocketError.ConnectionReset or
|
|
||||||
SocketError.ConnectionAborted or
|
|
||||||
SocketError.TimedOut;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
current = current.InnerException;
|
current = current.InnerException;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,7 +193,7 @@ public partial class ApiController
|
|||||||
CensusDataDto? censusDto = null;
|
CensusDataDto? censusDto = null;
|
||||||
if (_serverManager.SendCensusData && _lastCensus != null)
|
if (_serverManager.SendCensusData && _lastCensus != null)
|
||||||
{
|
{
|
||||||
var world = _dalamudUtil.GetWorldId();
|
var world = await _dalamudUtil.GetWorldIdAsync().ConfigureAwait(false);
|
||||||
censusDto = new((ushort)world, _lastCensus.RaceId, _lastCensus.TribeId, _lastCensus.Gender);
|
censusDto = new((ushort)world, _lastCensus.RaceId, _lastCensus.TribeId, _lastCensus.Gender);
|
||||||
Logger.LogDebug("Attaching Census Data: {data}", censusDto);
|
Logger.LogDebug("Attaching Census Data: {data}", censusDto);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -418,7 +418,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
|
|
||||||
public Task CyclePauseAsync(PairUniqueIdentifier ident)
|
public Task CyclePauseAsync(PairUniqueIdentifier ident)
|
||||||
{
|
{
|
||||||
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
var token = timeoutCts.Token;
|
var token = timeoutCts.Token;
|
||||||
@@ -430,19 +430,20 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var targetPermissions = entry.SelfPermissions;
|
var originalPermissions = entry.SelfPermissions;
|
||||||
targetPermissions.SetPaused(paused: true);
|
var targetPermissions = originalPermissions;
|
||||||
|
targetPermissions.SetPaused(!originalPermissions.IsPaused());
|
||||||
|
|
||||||
await UserSetPairPermissions(new UserPermissionsDto(entry.User, targetPermissions)).ConfigureAwait(false);
|
await UserSetPairPermissions(new UserPermissionsDto(entry.User, targetPermissions)).ConfigureAwait(false);
|
||||||
|
|
||||||
var pauseApplied = false;
|
var applied = false;
|
||||||
while (!token.IsCancellationRequested)
|
while (!token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
if (_pairCoordinator.Ledger.TryGetEntry(ident, out var updated) && updated is not null)
|
if (_pairCoordinator.Ledger.TryGetEntry(ident, out var updated) && updated is not null)
|
||||||
{
|
{
|
||||||
if (updated.SelfPermissions == targetPermissions)
|
if (updated.SelfPermissions == targetPermissions)
|
||||||
{
|
{
|
||||||
pauseApplied = true;
|
applied = true;
|
||||||
entry = updated;
|
entry = updated;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -452,16 +453,13 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
Logger.LogTrace("Waiting for permissions change for {uid}", ident.UserId);
|
Logger.LogTrace("Waiting for permissions change for {uid}", ident.UserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pauseApplied)
|
if (!applied)
|
||||||
{
|
{
|
||||||
Logger.LogWarning("CyclePauseAsync timed out waiting for pause acknowledgement for {uid}", ident.UserId);
|
Logger.LogWarning("CyclePauseAsync timed out waiting for pause acknowledgement for {uid}", ident.UserId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
targetPermissions.SetPaused(paused: false);
|
Logger.LogDebug("CyclePauseAsync toggled paused for {uid} to {state}", ident.UserId, targetPermissions.IsPaused());
|
||||||
await UserSetPairPermissions(new UserPermissionsDto(entry.User, targetPermissions)).ConfigureAwait(false);
|
|
||||||
|
|
||||||
Logger.LogDebug("CyclePauseAsync completed pause cycle for {uid}", ident.UserId);
|
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -481,26 +479,16 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task PauseAsync(UserData userData)
|
public async Task PauseAsync(UserData userData)
|
||||||
{
|
|
||||||
await SetPausedStateAsync(userData, paused: true).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task UnpauseAsync(UserData userData)
|
|
||||||
{
|
|
||||||
await SetPausedStateAsync(userData, paused: false).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SetPausedStateAsync(UserData userData, bool paused)
|
|
||||||
{
|
{
|
||||||
var pairIdent = new PairUniqueIdentifier(userData.UID);
|
var pairIdent = new PairUniqueIdentifier(userData.UID);
|
||||||
if (!_pairCoordinator.Ledger.TryGetEntry(pairIdent, out var entry) || entry is null)
|
if (!_pairCoordinator.Ledger.TryGetEntry(pairIdent, out var entry) || entry is null)
|
||||||
{
|
{
|
||||||
Logger.LogWarning("SetPausedStateAsync: pair {uid} not found in ledger", userData.UID);
|
Logger.LogWarning("PauseAsync: pair {uid} not found in ledger", userData.UID);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var permissions = entry.SelfPermissions;
|
var permissions = entry.SelfPermissions;
|
||||||
permissions.SetPaused(paused);
|
permissions.SetPaused(paused: true);
|
||||||
await UserSetPairPermissions(new UserPermissionsDto(userData, permissions)).ConfigureAwait(false);
|
await UserSetPairPermissions(new UserPermissionsDto(userData, permissions)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,8 +532,8 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
|
|
||||||
private void DalamudUtilOnLogIn()
|
private void DalamudUtilOnLogIn()
|
||||||
{
|
{
|
||||||
var charaName = _dalamudUtil.GetPlayerName();
|
var charaName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult();
|
||||||
var worldId = _dalamudUtil.GetHomeWorldId();
|
var worldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult();
|
||||||
var auth = _serverManager.CurrentServer.Authentications.Find(f => string.Equals(f.CharacterName, charaName, StringComparison.Ordinal) && f.WorldId == worldId);
|
var auth = _serverManager.CurrentServer.Authentications.Find(f => string.Equals(f.CharacterName, charaName, StringComparison.Ordinal) && f.WorldId == worldId);
|
||||||
if (auth?.AutoLogin ?? false)
|
if (auth?.AutoLogin ?? false)
|
||||||
{
|
{
|
||||||
@@ -653,7 +641,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
CensusDataDto? dto = null;
|
CensusDataDto? dto = null;
|
||||||
if (_serverManager.SendCensusData && _lastCensus != null)
|
if (_serverManager.SendCensusData && _lastCensus != null)
|
||||||
{
|
{
|
||||||
var world = _dalamudUtil.GetWorldId();
|
var world = await _dalamudUtil.GetWorldIdAsync().ConfigureAwait(false);
|
||||||
dto = new((ushort)world, _lastCensus.RaceId, _lastCensus.TribeId, _lastCensus.Gender);
|
dto = new((ushort)world, _lastCensus.RaceId, _lastCensus.TribeId, _lastCensus.Gender);
|
||||||
Logger.LogDebug("Attaching Census Data: {data}", dto);
|
Logger.LogDebug("Attaching Census Data: {data}", dto);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber
|
|||||||
result = await _httpClient.PostAsync(tokenUri, new FormUrlEncodedContent(
|
result = await _httpClient.PostAsync(tokenUri, new FormUrlEncodedContent(
|
||||||
[
|
[
|
||||||
new KeyValuePair<string, string>("auth", auth),
|
new KeyValuePair<string, string>("auth", auth),
|
||||||
new KeyValuePair<string, string>("charaIdent", _dalamudUtil.GetPlayerNameHashed()),
|
new KeyValuePair<string, string>("charaIdent", await _dalamudUtil.GetPlayerNameHashedAsync().ConfigureAwait(false)),
|
||||||
]), ct).ConfigureAwait(false);
|
]), ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -152,7 +152,7 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber
|
|||||||
JwtIdentifier jwtIdentifier;
|
JwtIdentifier jwtIdentifier;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var playerIdentifier = _dalamudUtil.GetPlayerNameHashed();
|
var playerIdentifier = await _dalamudUtil.GetPlayerNameHashedAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(playerIdentifier))
|
if (string.IsNullOrEmpty(playerIdentifier))
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user