Compare commits
1 Commits
pair-adapt
...
2.0.0.69-D
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48a276a349 |
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
|
|
||||||
|
|||||||
@@ -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,18 +51,12 @@ 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.ObjectDescriptors)
|
||||||
@@ -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.ObjectDescriptors)
|
||||||
|
{
|
||||||
|
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.2</Version>
|
<Version>2.0.0.69</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++)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -46,7 +46,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;
|
||||||
@@ -65,7 +64,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
private int _lastMissingCriticalMods;
|
private int _lastMissingCriticalMods;
|
||||||
private int _lastMissingNonCriticalMods;
|
private int _lastMissingNonCriticalMods;
|
||||||
private int _lastMissingForbiddenMods;
|
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();
|
||||||
@@ -183,8 +181,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;
|
||||||
@@ -202,7 +199,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()
|
||||||
@@ -425,7 +422,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 +454,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 +553,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 +696,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;
|
||||||
@@ -1423,7 +1415,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
private Task _visibilityGraceTask;
|
private Task _visibilityGraceTask;
|
||||||
|
|
||||||
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData,
|
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData,
|
||||||
bool updateModdedPaths, bool updateManip, Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths, CancellationToken downloadToken)
|
bool updateModdedPaths, bool updateManip, Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths, CancellationToken downloadToken)
|
||||||
{
|
{
|
||||||
var concurrencyLease = await _pairProcessingLimiter.AcquireAsync(downloadToken).ConfigureAwait(false);
|
var concurrencyLease = await _pairProcessingLimiter.AcquireAsync(downloadToken).ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
@@ -1577,37 +1569,24 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
RecordFailure("Handler not available for application", "HandlerUnavailable");
|
RecordFailure("Handler not available for application", "HandlerUnavailable");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
|
|
||||||
|
|
||||||
if (_applicationTask != null && !_applicationTask.IsCompleted)
|
var appToken = _applicationCancellationTokenSource?.Token;
|
||||||
|
while ((!_applicationTask?.IsCompleted ?? false)
|
||||||
|
&& !downloadToken.IsCancellationRequested
|
||||||
|
&& (!appToken?.IsCancellationRequested ?? false))
|
||||||
{
|
{
|
||||||
Logger.LogDebug("[BASE-{appBase}] Cancelling current data application (Id: {id}) for player ({handler})", applicationBase, _applicationId, PlayerName);
|
Logger.LogDebug("[BASE-{appBase}] Waiting for current data application (Id: {id}) for player ({handler}) to finish", applicationBase, _applicationId, PlayerName);
|
||||||
|
await Task.Delay(250).ConfigureAwait(false);
|
||||||
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
||||||
var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(downloadToken, timeoutCts.Token);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _applicationTask.WaitAsync(combinedCts.Token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
Logger.LogWarning("[BASE-{appBase}] Timeout waiting for application task {id} to complete, proceeding anyway", applicationBase, _applicationId);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
timeoutCts.Dispose();
|
|
||||||
combinedCts.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (downloadToken.IsCancellationRequested)
|
if (downloadToken.IsCancellationRequested || (appToken?.IsCancellationRequested ?? false))
|
||||||
{
|
{
|
||||||
_forceFullReapply = true;
|
_forceFullReapply = true;
|
||||||
RecordFailure("Application cancelled", "Cancellation");
|
RecordFailure("Application cancelled", "Cancellation");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_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, wantsModApply, pendingModReapply, token);
|
||||||
|
|||||||
@@ -31,7 +31,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 +48,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,7 +66,6 @@ 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)
|
||||||
@@ -94,7 +91,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));
|
||||||
@@ -219,7 +214,6 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
gameData,
|
gameData,
|
||||||
targetManager,
|
targetManager,
|
||||||
gameConfig,
|
gameConfig,
|
||||||
playerState,
|
|
||||||
sp.GetRequiredService<ActorObjectService>(),
|
sp.GetRequiredService<ActorObjectService>(),
|
||||||
sp.GetRequiredService<BlockedCharacterHandler>(),
|
sp.GetRequiredService<BlockedCharacterHandler>(),
|
||||||
sp.GetRequiredService<LightlessMediator>(),
|
sp.GetRequiredService<LightlessMediator>(),
|
||||||
@@ -282,22 +276,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 +299,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 +460,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 +529,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 +550,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,6 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using Dalamud.Game.ClientState.Conditions;
|
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 +10,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;
|
||||||
|
|
||||||
@@ -42,6 +41,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
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 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 GposeSnapshot _gposeSnapshot = GposeSnapshot.Empty;
|
||||||
|
|
||||||
@@ -151,16 +151,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -317,6 +316,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
_actorsByHash.Clear();
|
_actorsByHash.Clear();
|
||||||
_actorsByName.Clear();
|
_actorsByName.Clear();
|
||||||
_pendingHashResolutions.Clear();
|
_pendingHashResolutions.Clear();
|
||||||
|
_ownedTracker.Reset();
|
||||||
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
|
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
|
||||||
Volatile.Write(ref _gposeSnapshot, GposeSnapshot.Empty);
|
Volatile.Write(ref _gposeSnapshot, GposeSnapshot.Empty);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -481,196 +481,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)
|
||||||
@@ -764,8 +618,11 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
private void ReplaceDescriptor(ActorDescriptor existing, ActorDescriptor updated)
|
private void ReplaceDescriptor(ActorDescriptor existing, ActorDescriptor updated)
|
||||||
{
|
{
|
||||||
RemoveDescriptorFromIndexes(existing);
|
RemoveDescriptorFromIndexes(existing);
|
||||||
|
_ownedTracker.OnDescriptorRemoved(existing);
|
||||||
|
|
||||||
_activePlayers[updated.Address] = updated;
|
_activePlayers[updated.Address] = updated;
|
||||||
IndexDescriptor(updated);
|
IndexDescriptor(updated);
|
||||||
|
_ownedTracker.OnDescriptorAdded(updated);
|
||||||
UpdatePendingHashResolutions(updated);
|
UpdatePendingHashResolutions(updated);
|
||||||
PublishSnapshot();
|
PublishSnapshot();
|
||||||
}
|
}
|
||||||
@@ -833,6 +690,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
{
|
{
|
||||||
_activePlayers[descriptor.Address] = descriptor;
|
_activePlayers[descriptor.Address] = descriptor;
|
||||||
IndexDescriptor(descriptor);
|
IndexDescriptor(descriptor);
|
||||||
|
_ownedTracker.OnDescriptorAdded(descriptor);
|
||||||
UpdatePendingHashResolutions(descriptor);
|
UpdatePendingHashResolutions(descriptor);
|
||||||
PublishSnapshot();
|
PublishSnapshot();
|
||||||
}
|
}
|
||||||
@@ -840,6 +698,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
private void RemoveDescriptor(ActorDescriptor descriptor)
|
private void RemoveDescriptor(ActorDescriptor descriptor)
|
||||||
{
|
{
|
||||||
RemoveDescriptorFromIndexes(descriptor);
|
RemoveDescriptorFromIndexes(descriptor);
|
||||||
|
_ownedTracker.OnDescriptorRemoved(descriptor);
|
||||||
_pendingHashResolutions.TryRemove(descriptor.Address, out _);
|
_pendingHashResolutions.TryRemove(descriptor.Address, out _);
|
||||||
PublishSnapshot();
|
PublishSnapshot();
|
||||||
}
|
}
|
||||||
@@ -863,90 +722,17 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
|
|
||||||
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 ownedDescriptors = _activePlayers.Values
|
||||||
|
.Where(descriptor => descriptor.OwnedKind is not null)
|
||||||
|
.ToArray();
|
||||||
|
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, ownedDescriptors, playerAddresses, ownedSnapshot, nextGeneration);
|
||||||
Volatile.Write(ref _snapshot, snapshot);
|
Volatile.Write(ref _snapshot, snapshot);
|
||||||
@@ -1169,6 +955,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,
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ using LightlessSync.API.Dto.Chat;
|
|||||||
using LightlessSync.API.Data.Extensions;
|
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 +25,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,7 +37,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 List<ChatChannelSnapshot>? _cachedChannelSnapshots;
|
||||||
private bool _channelsSnapshotDirty = true;
|
private bool _channelsSnapshotDirty = true;
|
||||||
|
|
||||||
@@ -57,8 +54,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 +62,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;
|
||||||
@@ -576,7 +571,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 +697,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)
|
||||||
@@ -781,7 +776,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 +791,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 +816,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1024,14 +1013,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1161,7 +1149,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 +1159,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1309,12 +1288,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 +1301,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 +1315,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;
|
||||||
@@ -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;
|
||||||
@@ -210,18 +198,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 +217,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 +226,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,27 +80,21 @@ 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());
|
||||||
});
|
});
|
||||||
|
var clientLanguage = _clientState.ClientLanguage;
|
||||||
TerritoryData = new(() => BuildTerritoryData(clientLanguage));
|
TerritoryData = new(() => BuildTerritoryData(clientLanguage));
|
||||||
TerritoryDataEnglish = new(() => BuildTerritoryData(Dalamud.Game.ClientLanguage.English));
|
TerritoryDataEnglish = new(() => BuildTerritoryData(Dalamud.Game.ClientLanguage.English));
|
||||||
MapData = new(() => BuildMapData(clientLanguage));
|
MapData = new(() => BuildMapData(clientLanguage));
|
||||||
ContentFinderData = new Lazy<Dictionary<uint, string>>(() =>
|
|
||||||
{
|
|
||||||
return _gameData.GetExcelSheet<TerritoryType>()!
|
|
||||||
.Where(w => w.RowId != 0 && !string.IsNullOrEmpty(w.ContentFinderCondition.ValueNullable?.Name.ToString()))
|
|
||||||
.ToDictionary(w => w.RowId, w => w.ContentFinderCondition.Value.Name.ToString());
|
|
||||||
});
|
|
||||||
mediator.Subscribe<TargetPairMessage>(this, (msg) =>
|
mediator.Subscribe<TargetPairMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
if (clientState.IsPvP) return;
|
if (clientState.IsPvP) return;
|
||||||
@@ -273,7 +265,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];
|
||||||
@@ -288,7 +279,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
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, 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; }
|
||||||
|
|
||||||
@@ -382,7 +372,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()
|
||||||
@@ -443,22 +433,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 +470,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 +517,34 @@ 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();
|
||||||
|
|
||||||
|
if (playerChar == null || playerChar.Address == IntPtr.Zero)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
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 +583,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 +642,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);
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -23,7 +23,6 @@ using Pictomancy;
|
|||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using Task = System.Threading.Tasks.Task;
|
using Task = System.Threading.Tasks.Task;
|
||||||
|
|
||||||
@@ -42,7 +41,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly PairUiService _pairUiService;
|
private readonly PairUiService _pairUiService;
|
||||||
private readonly LightlessMediator _mediator;
|
private readonly LightlessMediator _mediator;
|
||||||
|
|
||||||
public LightlessMediator Mediator => _mediator;
|
public LightlessMediator Mediator => _mediator;
|
||||||
|
|
||||||
private readonly IUiBuilder _uiBuilder;
|
private readonly IUiBuilder _uiBuilder;
|
||||||
@@ -53,7 +51,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
private readonly Lock _labelLock = new();
|
private readonly Lock _labelLock = new();
|
||||||
private readonly NameplateBuffers _buffers = new();
|
private readonly NameplateBuffers _buffers = new();
|
||||||
private int _labelRenderCount;
|
private int _labelRenderCount;
|
||||||
private LightfinderLabelRenderer _lastRenderer;
|
|
||||||
|
|
||||||
private const string _defaultLabelText = "LightFinder";
|
private const string _defaultLabelText = "LightFinder";
|
||||||
private const SeIconChar _defaultIcon = SeIconChar.Hyadelyn;
|
private const SeIconChar _defaultIcon = SeIconChar.Hyadelyn;
|
||||||
@@ -63,24 +60,16 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
|
|
||||||
// / Overlay window flags
|
// / Overlay window flags
|
||||||
private const ImGuiWindowFlags _overlayFlags =
|
private const ImGuiWindowFlags _overlayFlags =
|
||||||
ImGuiWindowFlags.NoDecoration |
|
ImGuiWindowFlags.NoDecoration |
|
||||||
ImGuiWindowFlags.NoBackground |
|
ImGuiWindowFlags.NoBackground |
|
||||||
ImGuiWindowFlags.NoMove |
|
ImGuiWindowFlags.NoMove |
|
||||||
ImGuiWindowFlags.NoSavedSettings |
|
ImGuiWindowFlags.NoSavedSettings |
|
||||||
ImGuiWindowFlags.NoNav |
|
ImGuiWindowFlags.NoNav |
|
||||||
ImGuiWindowFlags.NoInputs;
|
ImGuiWindowFlags.NoInputs;
|
||||||
|
|
||||||
private readonly List<RectF> _uiRects = new(128);
|
private readonly List<RectF> _uiRects = new(128);
|
||||||
private ImmutableHashSet<string> _activeBroadcastingCids = [];
|
private ImmutableHashSet<string> _activeBroadcastingCids = [];
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
// Debug controls
|
|
||||||
|
|
||||||
// Debug counters (read-only from UI)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
private bool IsPictomancyRenderer => _configService.Current.LightfinderLabelRenderer == LightfinderLabelRenderer.Pictomancy;
|
|
||||||
|
|
||||||
public LightFinderPlateHandler(
|
public LightFinderPlateHandler(
|
||||||
ILogger<LightFinderPlateHandler> logger,
|
ILogger<LightFinderPlateHandler> logger,
|
||||||
IAddonLifecycle addonLifecycle,
|
IAddonLifecycle addonLifecycle,
|
||||||
@@ -103,26 +92,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
_pairUiService = pairUiService;
|
_pairUiService = pairUiService;
|
||||||
_uiBuilder = pluginInterface.UiBuilder ?? throw new ArgumentNullException(nameof(pluginInterface));
|
_uiBuilder = pluginInterface.UiBuilder ?? throw new ArgumentNullException(nameof(pluginInterface));
|
||||||
_ = pictomancyService ?? throw new ArgumentNullException(nameof(pictomancyService));
|
_ = pictomancyService ?? throw new ArgumentNullException(nameof(pictomancyService));
|
||||||
_lastRenderer = _configService.Current.LightfinderLabelRenderer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RefreshRendererState()
|
|
||||||
{
|
|
||||||
var renderer = _configService.Current.LightfinderLabelRenderer;
|
|
||||||
if (renderer == _lastRenderer)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_lastRenderer = renderer;
|
|
||||||
|
|
||||||
if (renderer == LightfinderLabelRenderer.Pictomancy)
|
|
||||||
{
|
|
||||||
FlagRefresh();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ClearNameplateCaches();
|
|
||||||
_lastNamePlateDrawFrame = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void Init()
|
internal void Init()
|
||||||
@@ -194,26 +164,10 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Draw detour for nameplate addon.
|
/// Draw detour for nameplate addon.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="type"></param>
|
||||||
|
/// <param name="args"></param>
|
||||||
private void NameplateDrawDetour(AddonEvent type, AddonArgs args)
|
private void NameplateDrawDetour(AddonEvent type, AddonArgs args)
|
||||||
{
|
{
|
||||||
RefreshRendererState();
|
|
||||||
if (!IsPictomancyRenderer)
|
|
||||||
{
|
|
||||||
ClearLabelBuffer();
|
|
||||||
_lastNamePlateDrawFrame = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide our overlay when the user hides the entire game UI (ScrollLock).
|
|
||||||
if (_gameGui.GameUiHidden)
|
|
||||||
{
|
|
||||||
ClearLabelBuffer();
|
|
||||||
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
|
|
||||||
_lastNamePlateDrawFrame = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// gpose: do not draw.
|
|
||||||
if (_clientState.IsGPosing)
|
if (_clientState.IsGPosing)
|
||||||
{
|
{
|
||||||
ClearLabelBuffer();
|
ClearLabelBuffer();
|
||||||
@@ -233,10 +187,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
if (fw != null)
|
if (fw != null)
|
||||||
_lastNamePlateDrawFrame = fw->FrameCounter;
|
_lastNamePlateDrawFrame = fw->FrameCounter;
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
DebugLastNameplateFrame = _lastNamePlateDrawFrame;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
var pNameplateAddon = (AddonNamePlate*)args.Addon.Address;
|
var pNameplateAddon = (AddonNamePlate*)args.Addon.Address;
|
||||||
|
|
||||||
if (_mpNameplateAddon != pNameplateAddon)
|
if (_mpNameplateAddon != pNameplateAddon)
|
||||||
@@ -253,13 +203,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void UpdateNameplateNodes()
|
private void UpdateNameplateNodes()
|
||||||
{
|
{
|
||||||
// If the user has hidden the UI, don't compute any labels.
|
|
||||||
if (_gameGui.GameUiHidden)
|
|
||||||
{
|
|
||||||
ClearLabelBuffer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentHandle = _gameGui.GetAddonByName("NamePlate");
|
var currentHandle = _gameGui.GetAddonByName("NamePlate");
|
||||||
if (currentHandle.Address == nint.Zero)
|
if (currentHandle.Address == nint.Zero)
|
||||||
{
|
{
|
||||||
@@ -323,6 +266,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
|
|
||||||
for (int i = 0; i < safeCount; ++i)
|
for (int i = 0; i < safeCount; ++i)
|
||||||
{
|
{
|
||||||
|
|
||||||
var objectInfoPtr = vec[i];
|
var objectInfoPtr = vec[i];
|
||||||
if (objectInfoPtr == null)
|
if (objectInfoPtr == null)
|
||||||
continue;
|
continue;
|
||||||
@@ -339,7 +283,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player)
|
if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// CID gating - only show for active broadcasters
|
// CID gating
|
||||||
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject);
|
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject);
|
||||||
if (cid == null || !_activeBroadcastingCids.Contains(cid))
|
if (cid == null || !_activeBroadcastingCids.Contains(cid))
|
||||||
continue;
|
continue;
|
||||||
@@ -375,12 +319,12 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
if (!currentConfig.LightfinderLabelShowHidden && !isNameplateVisible)
|
if (!currentConfig.LightfinderLabelShowHidden && !isNameplateVisible)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Prepare label content and scaling factors
|
// Prepare label content and scaling
|
||||||
var scaleMultiplier = Math.Clamp(currentConfig.LightfinderLabelScale, 0.5f, 2.0f);
|
var scaleMultiplier = System.Math.Clamp(currentConfig.LightfinderLabelScale, 0.5f, 2.0f);
|
||||||
var baseScale = currentConfig.LightfinderLabelUseIcon ? 1.0f : 0.5f;
|
var baseScale = currentConfig.LightfinderLabelUseIcon ? 1.0f : 0.5f;
|
||||||
var effectiveScale = baseScale * scaleMultiplier;
|
var effectiveScale = baseScale * scaleMultiplier;
|
||||||
var baseFontSize = currentConfig.LightfinderLabelUseIcon ? 36f : 24f;
|
var baseFontSize = currentConfig.LightfinderLabelUseIcon ? 36f : 24f;
|
||||||
var targetFontSize = (int)Math.Round(baseFontSize * scaleMultiplier);
|
var targetFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier);
|
||||||
var labelContent = currentConfig.LightfinderLabelUseIcon
|
var labelContent = currentConfig.LightfinderLabelUseIcon
|
||||||
? NormalizeIconGlyph(currentConfig.LightfinderLabelIconGlyph)
|
? NormalizeIconGlyph(currentConfig.LightfinderLabelIconGlyph)
|
||||||
: _defaultLabelText;
|
: _defaultLabelText;
|
||||||
@@ -388,8 +332,8 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
if (!currentConfig.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal)))
|
if (!currentConfig.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal)))
|
||||||
labelContent = _defaultLabelText;
|
labelContent = _defaultLabelText;
|
||||||
|
|
||||||
var nodeWidth = (int)Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
var nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
||||||
var nodeHeight = (int)Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
|
var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
|
||||||
AlignmentType alignment;
|
AlignmentType alignment;
|
||||||
|
|
||||||
var textScaleY = nameText->AtkResNode.ScaleY;
|
var textScaleY = nameText->AtkResNode.ScaleY;
|
||||||
@@ -399,7 +343,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
var blockHeight = ResolveCache(
|
var blockHeight = ResolveCache(
|
||||||
_buffers.TextHeights,
|
_buffers.TextHeights,
|
||||||
nameplateIndex,
|
nameplateIndex,
|
||||||
Math.Abs((int)nameplateObject.TextH),
|
System.Math.Abs((int)nameplateObject.TextH),
|
||||||
() => GetScaledTextHeight(nameText),
|
() => GetScaledTextHeight(nameText),
|
||||||
nodeHeight);
|
nodeHeight);
|
||||||
|
|
||||||
@@ -409,7 +353,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
(int)nameContainer->Height,
|
(int)nameContainer->Height,
|
||||||
() =>
|
() =>
|
||||||
{
|
{
|
||||||
var computed = blockHeight + (int)Math.Round(8 * textScaleY);
|
var computed = blockHeight + (int)System.Math.Round(8 * textScaleY);
|
||||||
return computed <= blockHeight ? blockHeight + 1 : computed;
|
return computed <= blockHeight ? blockHeight + 1 : computed;
|
||||||
},
|
},
|
||||||
blockHeight + 1);
|
blockHeight + 1);
|
||||||
@@ -417,7 +361,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
var blockTop = containerHeight - blockHeight;
|
var blockTop = containerHeight - blockHeight;
|
||||||
if (blockTop < 0)
|
if (blockTop < 0)
|
||||||
blockTop = 0;
|
blockTop = 0;
|
||||||
var verticalPadding = (int)Math.Round(4 * effectiveScale);
|
var verticalPadding = (int)System.Math.Round(4 * effectiveScale);
|
||||||
|
|
||||||
var positionY = blockTop - verticalPadding;
|
var positionY = blockTop - verticalPadding;
|
||||||
|
|
||||||
@@ -425,14 +369,21 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
var textWidth = ResolveCache(
|
var textWidth = ResolveCache(
|
||||||
_buffers.TextWidths,
|
_buffers.TextWidths,
|
||||||
nameplateIndex,
|
nameplateIndex,
|
||||||
Math.Abs(rawTextWidth),
|
System.Math.Abs(rawTextWidth),
|
||||||
() => GetScaledTextWidth(nameText),
|
() => GetScaledTextWidth(nameText),
|
||||||
nodeWidth);
|
nodeWidth);
|
||||||
|
|
||||||
// Text offset caching
|
// Text offset caching
|
||||||
var textOffset = (int)Math.Round(nameText->AtkResNode.X);
|
var textOffset = (int)System.Math.Round(nameText->AtkResNode.X);
|
||||||
var hasValidOffset = TryCacheTextOffset(nameplateIndex, rawTextWidth, textOffset);
|
var hasValidOffset = TryCacheTextOffset(nameplateIndex, rawTextWidth, textOffset);
|
||||||
|
|
||||||
|
if (nameContainer == null)
|
||||||
|
{
|
||||||
|
if (_logger.IsEnabled(LogLevel.Debug))
|
||||||
|
_logger.LogDebug("Nameplate {Index} container became unavailable during update, skipping.", nameplateIndex);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var res = nameContainer;
|
var res = nameContainer;
|
||||||
|
|
||||||
// X scale
|
// X scale
|
||||||
@@ -468,7 +419,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
|
|
||||||
var offsetXScreen = currentConfig.LightfinderLabelOffsetX * worldScaleX;
|
var offsetXScreen = currentConfig.LightfinderLabelOffsetX * worldScaleX;
|
||||||
|
|
||||||
// alignment based on config setting
|
// alignment based on config
|
||||||
switch (currentConfig.LabelAlignment)
|
switch (currentConfig.LabelAlignment)
|
||||||
{
|
{
|
||||||
case LabelAlignment.Left:
|
case LabelAlignment.Left:
|
||||||
@@ -487,7 +438,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// manual X positioning with optional cached offset
|
// manual X positioning
|
||||||
var cachedTextOffset = _buffers.TextOffsets[nameplateIndex];
|
var cachedTextOffset = _buffers.TextOffsets[nameplateIndex];
|
||||||
var hasCachedOffset = cachedTextOffset != int.MinValue;
|
var hasCachedOffset = cachedTextOffset != int.MinValue;
|
||||||
var baseOffsetXLocal = (!currentConfig.LightfinderLabelUseIcon && hasValidOffset && hasCachedOffset)
|
var baseOffsetXLocal = (!currentConfig.LightfinderLabelUseIcon && hasValidOffset && hasCachedOffset)
|
||||||
@@ -507,16 +458,16 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
|
|
||||||
// final position before smoothing
|
// final position before smoothing
|
||||||
var finalPosition = new Vector2(finalX, res->ScreenY + positionYScreen);
|
var finalPosition = new Vector2(finalX, res->ScreenY + positionYScreen);
|
||||||
var dpiScale = ImGui.GetIO().DisplayFramebufferScale.X;
|
var dpiScale = ImGui.GetIO().DisplayFramebufferScale.X; // often same for Y
|
||||||
var fw = Framework.Instance();
|
var fw = Framework.Instance();
|
||||||
float dt = fw->RealFrameDeltaTime;
|
float dt = fw->RealFrameDeltaTime;
|
||||||
|
|
||||||
//smoothing.. snap.. smooth.. snap
|
//smoothing..
|
||||||
finalPosition = SnapToPixels(finalPosition, dpiScale);
|
finalPosition = SnapToPixels(finalPosition, dpiScale);
|
||||||
finalPosition = SmoothPosition(nameplateIndex, finalPosition, dt);
|
finalPosition = SmoothPosition(nameplateIndex, finalPosition, dt);
|
||||||
finalPosition = SnapToPixels(finalPosition, dpiScale);
|
finalPosition = SnapToPixels(finalPosition, dpiScale);
|
||||||
|
|
||||||
// prepare label info for rendering
|
// prepare label info
|
||||||
var pivot = (currentConfig.LightfinderAutoAlign || currentConfig.LightfinderLabelUseIcon)
|
var pivot = (currentConfig.LightfinderAutoAlign || currentConfig.LightfinderLabelUseIcon)
|
||||||
? AlignmentToPivot(alignment)
|
? AlignmentToPivot(alignment)
|
||||||
: _defaultPivot;
|
: _defaultPivot;
|
||||||
@@ -552,10 +503,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void OnUiBuilderDraw()
|
private void OnUiBuilderDraw()
|
||||||
{
|
{
|
||||||
RefreshRendererState();
|
|
||||||
if (!IsPictomancyRenderer)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!_mEnabled)
|
if (!_mEnabled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -563,23 +510,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
if (fw == null)
|
if (fw == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// If UI is hidden, do not render.
|
// Frame skip check
|
||||||
if (_gameGui.GameUiHidden)
|
|
||||||
{
|
|
||||||
ClearLabelBuffer();
|
|
||||||
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
|
|
||||||
_lastNamePlateDrawFrame = 0;
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
DebugLabelCountLastFrame = 0;
|
|
||||||
DebugUiRectCountLastFrame = 0;
|
|
||||||
DebugOccludedCountLastFrame = 0;
|
|
||||||
DebugLastNameplateFrame = 0;
|
|
||||||
#endif
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Frame skip check - skip if more than 1 frame has passed since last nameplate draw.
|
|
||||||
var frame = fw->FrameCounter;
|
var frame = fw->FrameCounter;
|
||||||
|
|
||||||
if (_lastNamePlateDrawFrame == 0 || (frame - _lastNamePlateDrawFrame) > 1)
|
if (_lastNamePlateDrawFrame == 0 || (frame - _lastNamePlateDrawFrame) > 1)
|
||||||
@@ -587,62 +518,34 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
ClearLabelBuffer();
|
ClearLabelBuffer();
|
||||||
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
|
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
DebugLabelCountLastFrame = 0;
|
|
||||||
DebugUiRectCountLastFrame = 0;
|
|
||||||
DebugOccludedCountLastFrame = 0;
|
|
||||||
DebugLastNameplateFrame = _lastNamePlateDrawFrame;
|
|
||||||
#endif
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gpose Check - do not render.
|
//Gpose Check
|
||||||
if (_clientState.IsGPosing)
|
if (_clientState.IsGPosing)
|
||||||
{
|
{
|
||||||
ClearLabelBuffer();
|
ClearLabelBuffer();
|
||||||
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
|
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
|
||||||
_lastNamePlateDrawFrame = 0;
|
_lastNamePlateDrawFrame = 0;
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
DebugLabelCountLastFrame = 0;
|
|
||||||
DebugUiRectCountLastFrame = 0;
|
|
||||||
DebugOccludedCountLastFrame = 0;
|
|
||||||
DebugLastNameplateFrame = 0;
|
|
||||||
#endif
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If nameplate addon is not visible, skip rendering entirely.
|
// If nameplate addon is not visible, skip rendering
|
||||||
if (!IsNamePlateAddonVisible())
|
if (!IsNamePlateAddonVisible())
|
||||||
{
|
|
||||||
#if DEBUG
|
|
||||||
DebugLabelCountLastFrame = 0;
|
|
||||||
DebugUiRectCountLastFrame = 0;
|
|
||||||
DebugOccludedCountLastFrame = 0;
|
|
||||||
DebugLastNameplateFrame = _lastNamePlateDrawFrame;
|
|
||||||
#endif
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
int copyCount;
|
int copyCount;
|
||||||
lock (_labelLock)
|
lock (_labelLock)
|
||||||
{
|
{
|
||||||
copyCount = _labelRenderCount;
|
copyCount = _labelRenderCount;
|
||||||
if (copyCount == 0)
|
if (copyCount == 0)
|
||||||
{
|
|
||||||
#if DEBUG
|
|
||||||
DebugLabelCountLastFrame = 0;
|
|
||||||
DebugUiRectCountLastFrame = 0;
|
|
||||||
DebugOccludedCountLastFrame = 0;
|
|
||||||
DebugLastNameplateFrame = _lastNamePlateDrawFrame;
|
|
||||||
#endif
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
Array.Copy(_buffers.LabelRender, _buffers.LabelCopy, copyCount);
|
Array.Copy(_buffers.LabelRender, _buffers.LabelCopy, copyCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
var uiModule = fw->GetUIModule();
|
var uiModule = fw != null ? fw->GetUIModule() : null;
|
||||||
|
|
||||||
if (uiModule != null)
|
if (uiModule != null)
|
||||||
{
|
{
|
||||||
var rapture = uiModule->GetRaptureAtkModule();
|
var rapture = uiModule->GetRaptureAtkModule();
|
||||||
@@ -661,6 +564,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
var vpPos = vp.Pos;
|
var vpPos = vp.Pos;
|
||||||
|
|
||||||
ImGuiHelpers.ForceNextWindowMainViewport();
|
ImGuiHelpers.ForceNextWindowMainViewport();
|
||||||
|
|
||||||
ImGui.SetNextWindowPos(vp.Pos);
|
ImGui.SetNextWindowPos(vp.Pos);
|
||||||
ImGui.SetNextWindowSize(vp.Size);
|
ImGui.SetNextWindowSize(vp.Size);
|
||||||
|
|
||||||
@@ -671,118 +575,54 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
|
|
||||||
ImGui.PopStyleVar(2);
|
ImGui.PopStyleVar(2);
|
||||||
|
|
||||||
// Debug flags
|
using var drawList = PictoService.Draw();
|
||||||
bool dbgEnabled = false;
|
if (drawList == null)
|
||||||
bool dbgDisableOcc = false;
|
|
||||||
bool dbgDrawUiRects = false;
|
|
||||||
bool dbgDrawLabelRects = false;
|
|
||||||
#if DEBUG
|
|
||||||
dbgEnabled = DebugEnabled;
|
|
||||||
dbgDisableOcc = DebugDisableOcclusion;
|
|
||||||
dbgDrawUiRects = DebugDrawUiRects;
|
|
||||||
dbgDrawLabelRects = DebugDrawLabelRects;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
int occludedThisFrame = 0;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var drawList = PictoService.Draw();
|
|
||||||
if (drawList == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Debug drawing uses the window drawlist (so it always draws in the correct viewport).
|
|
||||||
var dbgDl = ImGui.GetWindowDrawList();
|
|
||||||
var useViewportOffset = ImGui.GetIO().ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable);
|
|
||||||
|
|
||||||
for (int i = 0; i < copyCount; ++i)
|
|
||||||
{
|
|
||||||
ref var info = ref _buffers.LabelCopy[i];
|
|
||||||
|
|
||||||
// final draw position with viewport offset (only when viewports are enabled)
|
|
||||||
var drawPos = info.ScreenPosition;
|
|
||||||
if (useViewportOffset)
|
|
||||||
drawPos += vpPos;
|
|
||||||
|
|
||||||
var font = default(ImFontPtr);
|
|
||||||
if (info.UseIcon)
|
|
||||||
{
|
|
||||||
var ioFonts = ImGui.GetIO().Fonts;
|
|
||||||
font = ioFonts.Fonts.Size > 1 ? new ImFontPtr(ioFonts.Fonts[1]) : ImGui.GetFont();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
font = ImGui.GetFont();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!font.IsNull)
|
|
||||||
ImGui.PushFont(font);
|
|
||||||
|
|
||||||
// calculate size for occlusion checking
|
|
||||||
var baseSize = ImGui.CalcTextSize(info.Text);
|
|
||||||
var baseFontSize = ImGui.GetFontSize();
|
|
||||||
|
|
||||||
if (!font.IsNull)
|
|
||||||
ImGui.PopFont();
|
|
||||||
|
|
||||||
// scale size based on font size
|
|
||||||
var scale = baseFontSize > 0 ? (info.FontSize / baseFontSize) : 1f;
|
|
||||||
var size = baseSize * scale;
|
|
||||||
|
|
||||||
var topLeft = info.ScreenPosition - new Vector2(size.X * info.Pivot.X, size.Y * info.Pivot.Y);
|
|
||||||
var labelRect = new RectF(topLeft.X, topLeft.Y, topLeft.X + size.X, topLeft.Y + size.Y);
|
|
||||||
|
|
||||||
bool wouldOcclude = IsOccludedByAnyUi(labelRect);
|
|
||||||
if (wouldOcclude)
|
|
||||||
occludedThisFrame++;
|
|
||||||
|
|
||||||
// Debug: draw label rects
|
|
||||||
if (dbgEnabled && dbgDrawLabelRects)
|
|
||||||
{
|
|
||||||
var tl = new Vector2(labelRect.L, labelRect.T);
|
|
||||||
var br = new Vector2(labelRect.R, labelRect.B);
|
|
||||||
|
|
||||||
if (useViewportOffset) { tl += vpPos; br += vpPos; }
|
|
||||||
|
|
||||||
// green = visible, red = would be occluded (even if forced)
|
|
||||||
var col = wouldOcclude
|
|
||||||
? ImGui.GetColorU32(new Vector4(1f, 0f, 0f, 0.6f))
|
|
||||||
: ImGui.GetColorU32(new Vector4(0f, 1f, 0f, 0.6f));
|
|
||||||
|
|
||||||
dbgDl.AddRect(tl, br, col);
|
|
||||||
}
|
|
||||||
|
|
||||||
// occlusion check (allow debug to disable)
|
|
||||||
if (!dbgDisableOcc && wouldOcclude)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
drawList.AddScreenText(drawPos, info.Text, info.TextColor, info.FontSize, info.Pivot, info.EdgeColor, font);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debug: draw UI rects if any
|
|
||||||
if (dbgEnabled && dbgDrawUiRects && _uiRects.Count > 0)
|
|
||||||
{
|
|
||||||
var useOff = useViewportOffset ? vpPos : Vector2.Zero;
|
|
||||||
var col = ImGui.GetColorU32(new Vector4(1f, 1f, 1f, 0.35f));
|
|
||||||
|
|
||||||
for (int i = 0; i < _uiRects.Count; i++)
|
|
||||||
{
|
|
||||||
var r = _uiRects[i];
|
|
||||||
dbgDl.AddRect(new Vector2(r.L, r.T) + useOff, new Vector2(r.R, r.B) + useOff, col);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
{
|
||||||
ImGui.End();
|
ImGui.End();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
for (int i = 0; i < copyCount; ++i)
|
||||||
DebugLabelCountLastFrame = copyCount;
|
{
|
||||||
DebugUiRectCountLastFrame = _uiRects.Count;
|
ref var info = ref _buffers.LabelCopy[i];
|
||||||
DebugOccludedCountLastFrame = occludedThisFrame;
|
|
||||||
DebugLastNameplateFrame = _lastNamePlateDrawFrame;
|
// final draw position with viewport offset
|
||||||
#endif
|
var drawPos = info.ScreenPosition + vpPos;
|
||||||
|
var font = default(ImFontPtr);
|
||||||
|
if (info.UseIcon)
|
||||||
|
{
|
||||||
|
var ioFonts = ImGui.GetIO().Fonts;
|
||||||
|
font = ioFonts.Fonts.Size > 1 ? new ImFontPtr(ioFonts.Fonts[1]) : ImGui.GetFont();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
font = ImGui.GetFont();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!font.IsNull)
|
||||||
|
ImGui.PushFont(font);
|
||||||
|
|
||||||
|
// calculate size for occlusion checking
|
||||||
|
var baseSize = ImGui.CalcTextSize(info.Text);
|
||||||
|
var baseFontSize = ImGui.GetFontSize();
|
||||||
|
|
||||||
|
if (!font.IsNull)
|
||||||
|
ImGui.PopFont();
|
||||||
|
|
||||||
|
// scale size based on font size
|
||||||
|
var scale = baseFontSize > 0 ? (info.FontSize / baseFontSize) : 1f;
|
||||||
|
var size = baseSize * scale;
|
||||||
|
|
||||||
|
// label rect for occlusion checking
|
||||||
|
var topLeft = info.ScreenPosition - new Vector2(size.X * info.Pivot.X, size.Y * info.Pivot.Y);
|
||||||
|
var labelRect = new RectF(topLeft.X, topLeft.Y, topLeft.X + size.X, topLeft.Y + size.Y);
|
||||||
|
|
||||||
|
// occlusion check
|
||||||
|
if (IsOccludedByAnyUi(labelRect))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
drawList.AddScreenText(drawPos, info.Text, info.TextColor, info.FontSize, info.Pivot, info.EdgeColor, font);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Vector2 AlignmentToPivot(AlignmentType alignment) => alignment switch
|
private static Vector2 AlignmentToPivot(AlignmentType alignment) => alignment switch
|
||||||
@@ -830,8 +670,8 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
if (scale <= 0f)
|
if (scale <= 0f)
|
||||||
scale = 1f;
|
scale = 1f;
|
||||||
|
|
||||||
var computed = (int)Math.Round(rawHeight * scale);
|
var computed = (int)System.Math.Round(rawHeight * scale);
|
||||||
return Math.Max(1, computed);
|
return System.Math.Max(1, computed);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static unsafe int GetScaledTextWidth(AtkTextNode* node)
|
private static unsafe int GetScaledTextWidth(AtkTextNode* node)
|
||||||
@@ -855,6 +695,12 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves a cached value for the given index.
|
/// Resolves a cached value for the given index.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="cache"></param>
|
||||||
|
/// <param name="index"></param>
|
||||||
|
/// <param name="rawValue"></param>
|
||||||
|
/// <param name="fallback"></param>
|
||||||
|
/// <param name="fallbackWhenZero"></param>
|
||||||
|
/// <returns></returns>
|
||||||
private static int ResolveCache(
|
private static int ResolveCache(
|
||||||
int[] cache,
|
int[] cache,
|
||||||
int index,
|
int index,
|
||||||
@@ -894,6 +740,9 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Snapping a position to pixel grid based on DPI scale.
|
/// Snapping a position to pixel grid based on DPI scale.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="p">Position</param>
|
||||||
|
/// <param name="dpiScale">DPI Scale</param>
|
||||||
|
/// <returns></returns>
|
||||||
private static Vector2 SnapToPixels(Vector2 p, float dpiScale)
|
private static Vector2 SnapToPixels(Vector2 p, float dpiScale)
|
||||||
{
|
{
|
||||||
// snap to pixel grid
|
// snap to pixel grid
|
||||||
@@ -902,9 +751,15 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
return new Vector2(x, y);
|
return new Vector2(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Smooths the position using exponential smoothing.
|
/// Smooths the position using exponential smoothing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="idx">Nameplate Index</param>
|
||||||
|
/// <param name="target">Final position</param>
|
||||||
|
/// <param name="dt">Delta Time</param>
|
||||||
|
/// <param name="responsiveness">How responssive the smooting should be</param>
|
||||||
|
/// <returns></returns>
|
||||||
private Vector2 SmoothPosition(int idx, Vector2 target, float dt, float responsiveness = 24f)
|
private Vector2 SmoothPosition(int idx, Vector2 target, float dt, float responsiveness = 24f)
|
||||||
{
|
{
|
||||||
// exponential smoothing
|
// exponential smoothing
|
||||||
@@ -931,193 +786,73 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
return cur;
|
return cur;
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
/// <summary>
|
||||||
private static bool IsFinite(float f) => !(float.IsNaN(f) || float.IsInfinity(f));
|
/// Tries to get a valid screen rect for the given addon.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="addon">Addon UI</param>
|
||||||
|
/// <param name="screen">Screen positioning/param>
|
||||||
|
/// <param name="rect">RectF of Addon</param>
|
||||||
|
/// <returns></returns>
|
||||||
private static bool TryGetAddonRect(AtkUnitBase* addon, Vector2 screen, out RectF rect)
|
private static bool TryGetAddonRect(AtkUnitBase* addon, Vector2 screen, out RectF rect)
|
||||||
{
|
{
|
||||||
|
// Addon existence
|
||||||
rect = default;
|
rect = default;
|
||||||
if (addon == null)
|
if (addon == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Addon must be visible
|
// Visibility check
|
||||||
if (!addon->IsVisible)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Root must be visible
|
|
||||||
var root = addon->RootNode;
|
var root = addon->RootNode;
|
||||||
if (root == null || !root->IsVisible())
|
if (root == null || !root->IsVisible())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Must have multiple nodes to be useful
|
// Size check
|
||||||
var nodeCount = addon->UldManager.NodeListCount;
|
float w = root->Width;
|
||||||
var nodeList = addon->UldManager.NodeList;
|
float h = root->Height;
|
||||||
if (nodeCount <= 1 || nodeList == null)
|
if (w <= 0 || h <= 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
float rsx = GetWorldScaleX(root);
|
// Local scale
|
||||||
float rsy = GetWorldScaleY(root);
|
float sx = root->ScaleX; if (sx <= 0f) sx = 1f;
|
||||||
if (!IsFinite(rsx) || rsx <= 0f) rsx = 1f;
|
float sy = root->ScaleY; if (sy <= 0f) sy = 1f;
|
||||||
if (!IsFinite(rsy) || rsy <= 0f) rsy = 1f;
|
|
||||||
|
|
||||||
// clamp insane root scales (rare but prevents explosions)
|
// World/composed scale from Transform
|
||||||
rsx = MathF.Min(rsx, 6f);
|
float wsx = GetWorldScaleX(root);
|
||||||
rsy = MathF.Min(rsy, 6f);
|
float wsy = GetWorldScaleY(root);
|
||||||
|
if (wsx <= 0f) wsx = 1f;
|
||||||
|
if (wsy <= 0f) wsy = 1f;
|
||||||
|
|
||||||
float rw = root->Width * rsx;
|
// World scale may include parent scaling; use it if meaningfully different.
|
||||||
float rh = root->Height * rsy;
|
float useX = MathF.Abs(wsx - sx) > 0.01f ? wsx : sx;
|
||||||
if (!IsFinite(rw) || !IsFinite(rh) || rw <= 2f || rh <= 2f)
|
float useY = MathF.Abs(wsy - sy) > 0.01f ? wsy : sy;
|
||||||
|
|
||||||
|
w *= useX;
|
||||||
|
h *= useY;
|
||||||
|
|
||||||
|
if (w < 4f || h < 4f)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
float rl = root->ScreenX;
|
// Screen coords
|
||||||
float rt = root->ScreenY;
|
float l = root->ScreenX;
|
||||||
if (!IsFinite(rl) || !IsFinite(rt))
|
float t = root->ScreenY;
|
||||||
|
float r = l + w;
|
||||||
|
float b = t + h;
|
||||||
|
|
||||||
|
// Drop fullscreen-ish / insane rects
|
||||||
|
if (w >= screen.X * 0.98f && h >= screen.Y * 0.98f)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
float rr = rl + rw;
|
// Drop offscreen rects
|
||||||
float rb = rt + rh;
|
if (l < -screen.X || t < -screen.Y || r > screen.X * 2f || b > screen.Y * 2f)
|
||||||
|
|
||||||
// If root is basically fullscreen, it<69>s not a useful occluder for our purpose.
|
|
||||||
if (rw >= screen.X * 0.98f && rh >= screen.Y * 0.98f)
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Clip root to screen so it stays sane
|
|
||||||
float rootL = MathF.Max(0f, rl);
|
|
||||||
float rootT = MathF.Max(0f, rt);
|
|
||||||
float rootR = MathF.Min(screen.X, rr);
|
|
||||||
float rootB = MathF.Min(screen.Y, rb);
|
|
||||||
if (rootR <= rootL || rootB <= rootT)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Root dimensions
|
|
||||||
var rootW = rootR - rootL;
|
|
||||||
var rootH = rootB - rootT;
|
|
||||||
|
|
||||||
// Find union of all probably-drawable nodes intersecting root
|
|
||||||
bool any = false;
|
|
||||||
float l = float.MaxValue, t = float.MaxValue, r = float.MinValue, b = float.MinValue;
|
|
||||||
|
|
||||||
// Allow a small bleed outside root; some addons draw small bits outside their root container.
|
|
||||||
const float rootPad = 24f;
|
|
||||||
float padL = rootL - rootPad;
|
|
||||||
float padT = rootT - rootPad;
|
|
||||||
float padR = rootR + rootPad;
|
|
||||||
float padB = rootB + rootPad;
|
|
||||||
|
|
||||||
for (int i = 1; i < nodeCount; i++)
|
|
||||||
{
|
|
||||||
var n = nodeList[i];
|
|
||||||
if (!IsProbablyDrawableNode(n))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
float w = n->Width;
|
|
||||||
float h = n->Height;
|
|
||||||
if (!IsFinite(w) || !IsFinite(h) || w <= 1f || h <= 1f)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
float sx = GetWorldScaleX(n);
|
|
||||||
float sy = GetWorldScaleY(n);
|
|
||||||
|
|
||||||
if (!IsFinite(sx) || sx <= 0f) sx = 1f;
|
|
||||||
if (!IsFinite(sy) || sy <= 0f) sy = 1f;
|
|
||||||
|
|
||||||
sx = MathF.Min(sx, 6f);
|
|
||||||
sy = MathF.Min(sy, 6f);
|
|
||||||
|
|
||||||
w *= sx;
|
|
||||||
h *= sy;
|
|
||||||
|
|
||||||
if (!IsFinite(w) || !IsFinite(h) || w < 2f || h < 2f)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
float nl = n->ScreenX;
|
|
||||||
float nt = n->ScreenY;
|
|
||||||
if (!IsFinite(nl) || !IsFinite(nt))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
float nr = nl + w;
|
|
||||||
float nb = nt + h;
|
|
||||||
|
|
||||||
// Must intersect root (with padding). This is the big mitigation.
|
|
||||||
if (nr <= padL || nb <= padT || nl >= padR || nt >= padB)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Reject nodes that are wildly larger than the root (common on targeting).
|
|
||||||
if (w > rootW * 2.0f || h > rootH * 2.0f)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Clip node to root and then to screen (prevents offscreen junk stretching union)
|
|
||||||
float cl = MathF.Max(rootL, nl);
|
|
||||||
float ct = MathF.Max(rootT, nt);
|
|
||||||
float cr = MathF.Min(rootR, nr);
|
|
||||||
float cb = MathF.Min(rootB, nb);
|
|
||||||
|
|
||||||
cl = MathF.Max(0f, cl);
|
|
||||||
ct = MathF.Max(0f, ct);
|
|
||||||
cr = MathF.Min(screen.X, cr);
|
|
||||||
cb = MathF.Min(screen.Y, cb);
|
|
||||||
|
|
||||||
if (cr <= cl || cb <= ct)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
any = true;
|
|
||||||
if (cl < l) l = cl;
|
|
||||||
if (ct < t) t = ct;
|
|
||||||
if (cr > r) r = cr;
|
|
||||||
if (cb > b) b = cb;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If nothing usable, fallback to root rect (still a sane occluder)
|
|
||||||
if (!any)
|
|
||||||
{
|
|
||||||
rect = new RectF(rootL, rootT, rootR, rootB);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate final union rect
|
|
||||||
var uw = r - l;
|
|
||||||
var uh = b - t;
|
|
||||||
if (uw < 4f || uh < 4f)
|
|
||||||
{
|
|
||||||
rect = new RectF(rootL, rootT, rootR, rootB);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If union is excessively larger than root, fallback to root rect
|
|
||||||
if (uw > rootW * 1.35f || uh > rootH * 1.35f)
|
|
||||||
{
|
|
||||||
rect = new RectF(rootL, rootT, rootR, rootB);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
rect = new RectF(l, t, r, b);
|
rect = new RectF(l, t, r, b);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsProbablyDrawableNode(AtkResNode* n)
|
|
||||||
{
|
|
||||||
if (n == null || !n->IsVisible())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Check alpha
|
|
||||||
if (n->Color.A == 16)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Check node type
|
|
||||||
return n->Type switch
|
|
||||||
{
|
|
||||||
NodeType.Text => true,
|
|
||||||
NodeType.Image => true,
|
|
||||||
NodeType.NineGrid => true,
|
|
||||||
NodeType.Counter => true,
|
|
||||||
NodeType.Component => true,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Refreshes the cached UI rects for occlusion checking.
|
/// Refreshes the cached UI rects for occlusion checking.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="unitMgr">Unit Manager</param>
|
||||||
private void RefreshUiRects(RaptureAtkUnitManager* unitMgr)
|
private void RefreshUiRects(RaptureAtkUnitManager* unitMgr)
|
||||||
{
|
{
|
||||||
_uiRects.Clear();
|
_uiRects.Clear();
|
||||||
@@ -1141,15 +876,13 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
if (TryGetAddonRect(addon, screen, out var r))
|
if (TryGetAddonRect(addon, screen, out var r))
|
||||||
_uiRects.Add(r);
|
_uiRects.Add(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
DebugUiRectCountLastFrame = _uiRects.Count;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Is the given label rect occluded by any UI rects?
|
/// Is the given label rect occluded by any UI rects?
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="labelRect">UI/Label Rect</param>
|
||||||
|
/// <returns>Is occluded or not</returns>
|
||||||
private bool IsOccludedByAnyUi(RectF labelRect)
|
private bool IsOccludedByAnyUi(RectF labelRect)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < _uiRects.Count; i++)
|
for (int i = 0; i < _uiRects.Count; i++)
|
||||||
@@ -1163,6 +896,8 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the world scale X of the given node.
|
/// Gets the world scale X of the given node.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="n">Node</param>
|
||||||
|
/// <returns>World Scale of node</returns>
|
||||||
private static float GetWorldScaleX(AtkResNode* n)
|
private static float GetWorldScaleX(AtkResNode* n)
|
||||||
{
|
{
|
||||||
var t = n->Transform;
|
var t = n->Transform;
|
||||||
@@ -1172,6 +907,8 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the world scale Y of the given node.
|
/// Gets the world scale Y of the given node.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="n">Node</param>
|
||||||
|
/// <returns>World Scale of node</returns>
|
||||||
private static float GetWorldScaleY(AtkResNode* n)
|
private static float GetWorldScaleY(AtkResNode* n)
|
||||||
{
|
{
|
||||||
var t = n->Transform;
|
var t = n->Transform;
|
||||||
@@ -1181,6 +918,8 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Normalize an icon glyph input into a valid string.
|
/// Normalize an icon glyph input into a valid string.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="rawInput">Raw glyph input</param>
|
||||||
|
/// <returns>Normalized glyph input</returns>
|
||||||
internal static string NormalizeIconGlyph(string? rawInput)
|
internal static string NormalizeIconGlyph(string? rawInput)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(rawInput))
|
if (string.IsNullOrWhiteSpace(rawInput))
|
||||||
@@ -1208,6 +947,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Is the nameplate addon visible?
|
/// Is the nameplate addon visible?
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <returns>Is it visible?</returns>
|
||||||
private bool IsNamePlateAddonVisible()
|
private bool IsNamePlateAddonVisible()
|
||||||
{
|
{
|
||||||
if (_mpNameplateAddon == null)
|
if (_mpNameplateAddon == null)
|
||||||
@@ -1217,6 +957,20 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
return root != null && root->IsVisible();
|
return root != null && root->IsVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts raw icon glyph input into an icon editor string.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="rawInput">Raw icon glyph input</param>
|
||||||
|
/// <returns>Icon editor string</returns>
|
||||||
|
internal static string ToIconEditorString(string? rawInput)
|
||||||
|
{
|
||||||
|
var normalized = NormalizeIconGlyph(rawInput);
|
||||||
|
var runeEnumerator = normalized.EnumerateRunes();
|
||||||
|
return runeEnumerator.MoveNext()
|
||||||
|
? runeEnumerator.Current.Value.ToString("X4", CultureInfo.InvariantCulture)
|
||||||
|
: _defaultIconGlyph;
|
||||||
|
}
|
||||||
|
|
||||||
private readonly struct NameplateLabelInfo
|
private readonly struct NameplateLabelInfo
|
||||||
{
|
{
|
||||||
public NameplateLabelInfo(
|
public NameplateLabelInfo(
|
||||||
@@ -1254,15 +1008,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
||||||
.Select(u => (ulong)u.PlayerCharacterId)];
|
.Select(u => (ulong)u.PlayerCharacterId)];
|
||||||
|
|
||||||
public int DebugLabelCountLastFrame { get; set; }
|
|
||||||
public int DebugUiRectCountLastFrame { get; set; }
|
|
||||||
public int DebugOccludedCountLastFrame { get; set; }
|
|
||||||
public uint DebugLastNameplateFrame { get; set; }
|
|
||||||
public bool DebugDrawUiRects { get; set; }
|
|
||||||
public bool DebugDrawLabelRects { get; set; } = true;
|
|
||||||
public bool DebugDisableOcclusion { get; set; }
|
|
||||||
public bool DebugEnabled { get; set; }
|
|
||||||
|
|
||||||
public void FlagRefresh()
|
public void FlagRefresh()
|
||||||
{
|
{
|
||||||
_needsLabelRefresh = true;
|
_needsLabelRefresh = true;
|
||||||
@@ -1270,12 +1015,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
|
|
||||||
public void OnTick(PriorityFrameworkUpdateMessage _)
|
public void OnTick(PriorityFrameworkUpdateMessage _)
|
||||||
{
|
{
|
||||||
if (!IsPictomancyRenderer)
|
|
||||||
{
|
|
||||||
_needsLabelRefresh = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_needsLabelRefresh)
|
if (_needsLabelRefresh)
|
||||||
{
|
{
|
||||||
UpdateNameplateNodes();
|
UpdateNameplateNodes();
|
||||||
@@ -1286,6 +1025,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update the active broadcasting CIDs.
|
/// Update the active broadcasting CIDs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="cids">Inbound new CIDs</param>
|
||||||
public void UpdateBroadcastingCids(IEnumerable<string> cids)
|
public void UpdateBroadcastingCids(IEnumerable<string> cids)
|
||||||
{
|
{
|
||||||
var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal);
|
var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal);
|
||||||
@@ -1315,7 +1055,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
public NameplateBuffers()
|
public NameplateBuffers()
|
||||||
{
|
{
|
||||||
TextOffsets = new int[AddonNamePlate.NumNamePlateObjects];
|
TextOffsets = new int[AddonNamePlate.NumNamePlateObjects];
|
||||||
Array.Fill(TextOffsets, int.MinValue);
|
System.Array.Fill(TextOffsets, int.MinValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int[] TextWidths { get; } = new int[AddonNamePlate.NumNamePlateObjects];
|
public int[] TextWidths { get; } = new int[AddonNamePlate.NumNamePlateObjects];
|
||||||
@@ -1327,20 +1067,23 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
public NameplateLabelInfo[] LabelCopy { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects];
|
public NameplateLabelInfo[] LabelCopy { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects];
|
||||||
|
|
||||||
public Vector2[] SmoothedPos = new Vector2[AddonNamePlate.NumNamePlateObjects];
|
public Vector2[] SmoothedPos = new Vector2[AddonNamePlate.NumNamePlateObjects];
|
||||||
|
|
||||||
public bool[] HasSmoothed = new bool[AddonNamePlate.NumNamePlateObjects];
|
public bool[] HasSmoothed = new bool[AddonNamePlate.NumNamePlateObjects];
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
Array.Clear(TextWidths, 0, TextWidths.Length);
|
System.Array.Clear(TextWidths, 0, TextWidths.Length);
|
||||||
Array.Clear(TextHeights, 0, TextHeights.Length);
|
System.Array.Clear(TextHeights, 0, TextHeights.Length);
|
||||||
Array.Clear(ContainerHeights, 0, ContainerHeights.Length);
|
System.Array.Clear(ContainerHeights, 0, ContainerHeights.Length);
|
||||||
Array.Fill(TextOffsets, int.MinValue);
|
System.Array.Fill(TextOffsets, int.MinValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts the LightFinder Plate Handler.
|
/// Starts the LightFinder Plate Handler.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation Token</param>
|
||||||
|
/// <returns>Task Completed</returns>
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Init();
|
Init();
|
||||||
@@ -1350,6 +1093,8 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stops the LightFinder Plate Handler.
|
/// Stops the LightFinder Plate Handler.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation Token</param>
|
||||||
|
/// <returns>Task Completed</returns>
|
||||||
public Task StopAsync(CancellationToken cancellationToken)
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Uninit();
|
Uninit();
|
||||||
|
|||||||
@@ -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,45 +140,9 @@ 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()
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,7 +105,6 @@ public class UiFactory
|
|||||||
groupData: groupData,
|
groupData: groupData,
|
||||||
isLightfinderContext: isLightfinderContext,
|
isLightfinderContext: isLightfinderContext,
|
||||||
lightfinderCid: lightfinderCid,
|
lightfinderCid: lightfinderCid,
|
||||||
performanceCollector: _performanceCollectorService,
|
performanceCollector: _performanceCollectorService);
|
||||||
_apiController);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -757,19 +757,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);
|
||||||
|
|||||||
@@ -84,8 +84,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
private bool _pairDiagnosticsEnabled;
|
private bool _pairDiagnosticsEnabled;
|
||||||
private string? _selectedPairDebugUid = null;
|
private string? _selectedPairDebugUid = null;
|
||||||
private string _lightfinderIconInput = string.Empty;
|
private string _lightfinderIconInput = string.Empty;
|
||||||
private bool _showLightfinderRendererWarning = false;
|
|
||||||
private LightfinderLabelRenderer _pendingLightfinderRenderer = LightfinderLabelRenderer.Pictomancy;
|
|
||||||
private bool _lightfinderIconInputInitialized = false;
|
private bool _lightfinderIconInputInitialized = false;
|
||||||
private int _lightfinderIconPresetIndex = -1;
|
private int _lightfinderIconPresetIndex = -1;
|
||||||
private static readonly LightlessConfig DefaultConfig = new();
|
private static readonly LightlessConfig DefaultConfig = new();
|
||||||
@@ -2370,78 +2368,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
|
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
|
||||||
|
|
||||||
ImGui.TextUnformatted("Nameplate Label Rendering");
|
|
||||||
var labelRenderer = _configService.Current.LightfinderLabelRenderer;
|
|
||||||
var labelRendererLabel = labelRenderer switch
|
|
||||||
{
|
|
||||||
LightfinderLabelRenderer.SignatureHook => "Native Nameplate Rendering",
|
|
||||||
_ => "ImGui Overlay",
|
|
||||||
};
|
|
||||||
|
|
||||||
if (ImGui.BeginCombo("Render mode", labelRendererLabel))
|
|
||||||
{
|
|
||||||
foreach (var option in Enum.GetValues<LightfinderLabelRenderer>())
|
|
||||||
{
|
|
||||||
var optionLabel = option switch
|
|
||||||
{
|
|
||||||
LightfinderLabelRenderer.SignatureHook => "Native Nameplate Rendering",
|
|
||||||
_ => "ImGui Overlay",
|
|
||||||
};
|
|
||||||
|
|
||||||
var selected = option == labelRenderer;
|
|
||||||
if (ImGui.Selectable(optionLabel, selected))
|
|
||||||
{
|
|
||||||
if (option == LightfinderLabelRenderer.SignatureHook)
|
|
||||||
{
|
|
||||||
_pendingLightfinderRenderer = option;
|
|
||||||
_showLightfinderRendererWarning = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_configService.Current.LightfinderLabelRenderer = option;
|
|
||||||
_configService.Save();
|
|
||||||
_nameplateService.RequestRedraw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (selected)
|
|
||||||
ImGui.SetItemDefaultFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.EndCombo();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_showLightfinderRendererWarning)
|
|
||||||
{
|
|
||||||
ImGui.SetNextWindowSize(new Vector2(450f, 0f), ImGuiCond.Appearing);
|
|
||||||
ImGui.OpenPopup("Nameplate Warning");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui.BeginPopupModal("Nameplate Warning", ref _showLightfinderRendererWarning, ImGuiWindowFlags.AlwaysAutoResize))
|
|
||||||
{
|
|
||||||
ImGui.TextColored(UIColors.Get("DimRed"), "USE AT YOUR RISK!");
|
|
||||||
ImGui.Spacing();
|
|
||||||
ImGui.TextWrapped("Writing on to the native Nameplates is known to be unstable and MAY cause crashes. DO NOT REPORT THOSE CRASHES TO DALAMUD. We will also not be supporting Nameplate crashes. You have been warned.");
|
|
||||||
ImGui.Spacing();
|
|
||||||
ImGui.TextWrapped("By accepting this warning, you understand that you are using this feature at risk of crashing.");
|
|
||||||
ImGui.Spacing();
|
|
||||||
|
|
||||||
var buttonWidth = ImGui.GetContentRegionAvail().X;
|
|
||||||
if (ImGui.Button("I Understand", new Vector2(buttonWidth, 0)))
|
|
||||||
{
|
|
||||||
_configService.Current.LightfinderLabelRenderer = _pendingLightfinderRenderer;
|
|
||||||
_configService.Save();
|
|
||||||
_nameplateService.RequestRedraw();
|
|
||||||
_showLightfinderRendererWarning = false;
|
|
||||||
ImGui.CloseCurrentPopup();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.EndPopup();
|
|
||||||
}
|
|
||||||
|
|
||||||
_uiShared.DrawHelpText("Choose how Lightfinder labels render: the default ImGui overlay or native nameplate nodes via signature hook.");
|
|
||||||
|
|
||||||
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
|
|
||||||
|
|
||||||
ImGui.TextUnformatted("Alignment");
|
ImGui.TextUnformatted("Alignment");
|
||||||
ImGui.BeginDisabled(autoAlign);
|
ImGui.BeginDisabled(autoAlign);
|
||||||
if (ImGui.SliderInt("Label Offset X", ref offsetX, -200, 200))
|
if (ImGui.SliderInt("Label Offset X", ref offsetX, -200, 200))
|
||||||
@@ -2624,7 +2550,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
var selected = i == _lightfinderIconPresetIndex;
|
var selected = i == _lightfinderIconPresetIndex;
|
||||||
if (ImGui.Selectable(preview, selected))
|
if (ImGui.Selectable(preview, selected))
|
||||||
{
|
{
|
||||||
_lightfinderIconInput = LightFinderPlateHandler.NormalizeIconGlyph(optionGlyph);
|
_lightfinderIconInput = LightFinderPlateHandler.ToIconEditorString(optionGlyph);
|
||||||
_lightfinderIconPresetIndex = i;
|
_lightfinderIconPresetIndex = i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4100,7 +4026,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
private void RefreshLightfinderIconState()
|
private void RefreshLightfinderIconState()
|
||||||
{
|
{
|
||||||
var normalized = LightFinderPlateHandler.NormalizeIconGlyph(_configService.Current.LightfinderLabelIconGlyph);
|
var normalized = LightFinderPlateHandler.NormalizeIconGlyph(_configService.Current.LightfinderLabelIconGlyph);
|
||||||
_lightfinderIconInput = LightFinderPlateHandler.NormalizeIconGlyph(normalized);
|
_lightfinderIconInput = LightFinderPlateHandler.ToIconEditorString(normalized);
|
||||||
_lightfinderIconInputInitialized = true;
|
_lightfinderIconInputInitialized = true;
|
||||||
|
|
||||||
_lightfinderIconPresetIndex = -1;
|
_lightfinderIconPresetIndex = -1;
|
||||||
@@ -4118,7 +4044,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
_configService.Current.LightfinderLabelIconGlyph = normalizedGlyph;
|
_configService.Current.LightfinderLabelIconGlyph = normalizedGlyph;
|
||||||
_configService.Save();
|
_configService.Save();
|
||||||
_lightfinderIconInput = LightFinderPlateHandler.NormalizeIconGlyph(normalizedGlyph);
|
_lightfinderIconInput = LightFinderPlateHandler.ToIconEditorString(normalizedGlyph);
|
||||||
_lightfinderIconPresetIndex = presetIndex;
|
_lightfinderIconPresetIndex = presetIndex;
|
||||||
_lightfinderIconInputInitialized = true;
|
_lightfinderIconInputInitialized = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ using LightlessSync.Services.ServerConfiguration;
|
|||||||
using LightlessSync.UI.Services;
|
using LightlessSync.UI.Services;
|
||||||
using LightlessSync.UI.Tags;
|
using LightlessSync.UI.Tags;
|
||||||
using LightlessSync.Utils;
|
using LightlessSync.Utils;
|
||||||
using LightlessSync.WebAPI;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
@@ -23,7 +22,6 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
|
|||||||
private readonly PairUiService _pairUiService;
|
private readonly PairUiService _pairUiService;
|
||||||
private readonly ServerConfigurationManager _serverManager;
|
private readonly ServerConfigurationManager _serverManager;
|
||||||
private readonly ProfileTagService _profileTagService;
|
private readonly ProfileTagService _profileTagService;
|
||||||
private readonly ApiController _apiController;
|
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
private readonly UserData? _userData;
|
private readonly UserData? _userData;
|
||||||
private readonly GroupData? _groupData;
|
private readonly GroupData? _groupData;
|
||||||
@@ -62,8 +60,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
|
|||||||
GroupData? groupData,
|
GroupData? groupData,
|
||||||
bool isLightfinderContext,
|
bool isLightfinderContext,
|
||||||
string? lightfinderCid,
|
string? lightfinderCid,
|
||||||
PerformanceCollectorService performanceCollector,
|
PerformanceCollectorService performanceCollector)
|
||||||
ApiController apiController)
|
|
||||||
: base(logger, mediator, BuildWindowTitle(
|
: base(logger, mediator, BuildWindowTitle(
|
||||||
userData,
|
userData,
|
||||||
groupData,
|
groupData,
|
||||||
@@ -97,7 +94,6 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
|
|||||||
.Apply();
|
.Apply();
|
||||||
|
|
||||||
IsOpen = true;
|
IsOpen = true;
|
||||||
_apiController = apiController;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Pair? Pair { get; }
|
public Pair? Pair { get; }
|
||||||
@@ -252,33 +248,19 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
|
|||||||
ResetBannerTexture();
|
ResetBannerTexture();
|
||||||
_lastBannerPicture = bannerBytes;
|
_lastBannerPicture = bannerBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
string? noteText = null;
|
string? noteText = null;
|
||||||
|
string statusLabel = _isLightfinderContext ? "Exploring" : "Offline";
|
||||||
var isSelfProfile = !_isLightfinderContext
|
|
||||||
&& _userData is not null
|
|
||||||
&& !string.IsNullOrEmpty(_apiController.UID)
|
|
||||||
&& string.Equals(_userData.UID, _apiController.UID, StringComparison.Ordinal);
|
|
||||||
|
|
||||||
string statusLabel = _isLightfinderContext
|
|
||||||
? "Exploring"
|
|
||||||
: isSelfProfile ? "Online" : "Offline";
|
|
||||||
|
|
||||||
string? visiblePlayerName = null;
|
string? visiblePlayerName = null;
|
||||||
bool directPair = false;
|
bool directPair = false;
|
||||||
bool youPaused = false;
|
bool youPaused = false;
|
||||||
bool theyPaused = false;
|
bool theyPaused = false;
|
||||||
List<string> syncshellLines = [];
|
List<string> syncshellLines = [];
|
||||||
|
|
||||||
if (!_isLightfinderContext)
|
|
||||||
{
|
|
||||||
noteText = _serverManager.GetNoteForUid(_userData!.UID);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_isLightfinderContext && Pair != null)
|
if (!_isLightfinderContext && Pair != null)
|
||||||
{
|
{
|
||||||
var snapshot = _pairUiService.GetSnapshot();
|
var snapshot = _pairUiService.GetSnapshot();
|
||||||
noteText = _serverManager.GetNoteForUid(Pair.UserData.UID);
|
noteText = _serverManager.GetNoteForUid(Pair.UserData.UID);
|
||||||
|
|
||||||
statusLabel = Pair.IsVisible ? "Visible" : (Pair.IsOnline ? "Online" : "Offline");
|
statusLabel = Pair.IsVisible ? "Visible" : (Pair.IsOnline ? "Online" : "Offline");
|
||||||
visiblePlayerName = Pair.IsVisible ? Pair.PlayerName : null;
|
visiblePlayerName = Pair.IsVisible ? Pair.PlayerName : null;
|
||||||
|
|
||||||
@@ -300,15 +282,11 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
|
|||||||
var groupLabel = snapshot.GroupsByGid.TryGetValue(gid, out var groupInfo)
|
var groupLabel = snapshot.GroupsByGid.TryGetValue(gid, out var groupInfo)
|
||||||
? groupInfo.GroupAliasOrGID
|
? groupInfo.GroupAliasOrGID
|
||||||
: gid;
|
: gid;
|
||||||
|
|
||||||
var groupNote = _serverManager.GetNoteForGid(gid);
|
var groupNote = _serverManager.GetNoteForGid(gid);
|
||||||
syncshellLines.Add(string.IsNullOrEmpty(groupNote) ? groupLabel : $"{groupNote} ({groupLabel})");
|
syncshellLines.Add(string.IsNullOrEmpty(groupNote) ? groupLabel : $"{groupNote} ({groupLabel})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSelfProfile)
|
|
||||||
statusLabel = "Online";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var presenceTokens = new List<PresenceToken>
|
var presenceTokens = new List<PresenceToken>
|
||||||
|
|||||||
@@ -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,20 +795,6 @@ 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)
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Data.Extensions;
|
|
||||||
using LightlessSync.API.Data.Enum;
|
|
||||||
using Dalamud.Bindings.ImGui;
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using LightlessSync.API.Dto.Chat;
|
using LightlessSync.API.Dto.Chat;
|
||||||
using LightlessSync.API.Dto.Group;
|
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.LightlessConfiguration.Models;
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
@@ -17,17 +14,13 @@ using LightlessSync.Services.Chat;
|
|||||||
using LightlessSync.Services.LightFinder;
|
using LightlessSync.Services.LightFinder;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Services.ServerConfiguration;
|
using LightlessSync.Services.ServerConfiguration;
|
||||||
using LightlessSync.UI.Models;
|
|
||||||
using LightlessSync.UI.Services;
|
using LightlessSync.UI.Services;
|
||||||
using LightlessSync.UI.Style;
|
using LightlessSync.UI.Style;
|
||||||
using LightlessSync.Utils;
|
using LightlessSync.Utils;
|
||||||
using Dalamud.Interface.Textures.TextureWraps;
|
|
||||||
using OtterGui.Text;
|
using OtterGui.Text;
|
||||||
using LightlessSync.WebAPI;
|
using LightlessSync.WebAPI;
|
||||||
using LightlessSync.WebAPI.SignalR.Utils;
|
using LightlessSync.WebAPI.SignalR.Utils;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using LightlessSync.PlayerData.Factories;
|
|
||||||
using LightlessSync.PlayerData.Pairs;
|
|
||||||
|
|
||||||
namespace LightlessSync.UI;
|
namespace LightlessSync.UI;
|
||||||
|
|
||||||
@@ -38,8 +31,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
private const string SettingsPopupId = "zone_chat_settings_popup";
|
private const string SettingsPopupId = "zone_chat_settings_popup";
|
||||||
private const string ReportPopupId = "Report Message##zone_chat_report_popup";
|
private const string ReportPopupId = "Report Message##zone_chat_report_popup";
|
||||||
private const string ChannelDragPayloadId = "zone_chat_channel_drag";
|
private const string ChannelDragPayloadId = "zone_chat_channel_drag";
|
||||||
private const string EmotePickerPopupId = "zone_chat_emote_picker";
|
|
||||||
private const int EmotePickerColumns = 10;
|
|
||||||
private const float DefaultWindowOpacity = .97f;
|
private const float DefaultWindowOpacity = .97f;
|
||||||
private const float DefaultUnfocusedWindowOpacity = 0.6f;
|
private const float DefaultUnfocusedWindowOpacity = 0.6f;
|
||||||
private const float MinWindowOpacity = 0.05f;
|
private const float MinWindowOpacity = 0.05f;
|
||||||
@@ -55,8 +46,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
private readonly ZoneChatService _zoneChatService;
|
private readonly ZoneChatService _zoneChatService;
|
||||||
private readonly PairUiService _pairUiService;
|
private readonly PairUiService _pairUiService;
|
||||||
private readonly PairFactory _pairFactory;
|
|
||||||
private readonly ChatEmoteService _chatEmoteService;
|
|
||||||
private readonly LightFinderService _lightFinderService;
|
private readonly LightFinderService _lightFinderService;
|
||||||
private readonly LightlessProfileManager _profileManager;
|
private readonly LightlessProfileManager _profileManager;
|
||||||
private readonly ApiController _apiController;
|
private readonly ApiController _apiController;
|
||||||
@@ -65,7 +54,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
private readonly DalamudUtilService _dalamudUtilService;
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
private readonly IUiBuilder _uiBuilder;
|
private readonly IUiBuilder _uiBuilder;
|
||||||
private readonly Dictionary<string, string> _draftMessages = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, string> _draftMessages = new(StringComparer.Ordinal);
|
||||||
private readonly Dictionary<string, List<string>> _pendingDraftClears = new(StringComparer.Ordinal);
|
|
||||||
private readonly ImGuiWindowFlags _unpinnedWindowFlags;
|
private readonly ImGuiWindowFlags _unpinnedWindowFlags;
|
||||||
private float _currentWindowOpacity = DefaultWindowOpacity;
|
private float _currentWindowOpacity = DefaultWindowOpacity;
|
||||||
private float _baseWindowOpacity = DefaultWindowOpacity;
|
private float _baseWindowOpacity = DefaultWindowOpacity;
|
||||||
@@ -93,8 +81,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
private ChatReportResult? _reportSubmissionResult;
|
private ChatReportResult? _reportSubmissionResult;
|
||||||
private string? _dragChannelKey;
|
private string? _dragChannelKey;
|
||||||
private string? _dragHoverKey;
|
private string? _dragHoverKey;
|
||||||
private bool _openEmotePicker;
|
|
||||||
private string _emoteFilter = string.Empty;
|
|
||||||
private bool _HideStateActive;
|
private bool _HideStateActive;
|
||||||
private bool _HideStateWasOpen;
|
private bool _HideStateWasOpen;
|
||||||
private bool _pushedStyle;
|
private bool _pushedStyle;
|
||||||
@@ -105,8 +91,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
UiSharedService uiSharedService,
|
UiSharedService uiSharedService,
|
||||||
ZoneChatService zoneChatService,
|
ZoneChatService zoneChatService,
|
||||||
PairUiService pairUiService,
|
PairUiService pairUiService,
|
||||||
PairFactory pairFactory,
|
|
||||||
ChatEmoteService chatEmoteService,
|
|
||||||
LightFinderService lightFinderService,
|
LightFinderService lightFinderService,
|
||||||
LightlessProfileManager profileManager,
|
LightlessProfileManager profileManager,
|
||||||
ChatConfigService chatConfigService,
|
ChatConfigService chatConfigService,
|
||||||
@@ -120,8 +104,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
_uiSharedService = uiSharedService;
|
_uiSharedService = uiSharedService;
|
||||||
_zoneChatService = zoneChatService;
|
_zoneChatService = zoneChatService;
|
||||||
_pairUiService = pairUiService;
|
_pairUiService = pairUiService;
|
||||||
_pairFactory = pairFactory;
|
|
||||||
_chatEmoteService = chatEmoteService;
|
|
||||||
_lightFinderService = lightFinderService;
|
_lightFinderService = lightFinderService;
|
||||||
_profileManager = profileManager;
|
_profileManager = profileManager;
|
||||||
_chatConfigService = chatConfigService;
|
_chatConfigService = chatConfigService;
|
||||||
@@ -206,7 +188,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
private void ApplyUiVisibilitySettings()
|
private void ApplyUiVisibilitySettings()
|
||||||
{
|
{
|
||||||
var config = _chatConfigService.Current;
|
var config = _chatConfigService.Current;
|
||||||
_uiBuilder.DisableUserUiHide = true;
|
_uiBuilder.DisableAutomaticUiHide = config.ShowWhenUiHidden;
|
||||||
_uiBuilder.DisableCutsceneUiHide = config.ShowInCutscenes;
|
_uiBuilder.DisableCutsceneUiHide = config.ShowInCutscenes;
|
||||||
_uiBuilder.DisableGposeUiHide = config.ShowInGpose;
|
_uiBuilder.DisableGposeUiHide = config.ShowInGpose;
|
||||||
}
|
}
|
||||||
@@ -215,11 +197,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
var config = _chatConfigService.Current;
|
var config = _chatConfigService.Current;
|
||||||
|
|
||||||
if (!config.ShowWhenUiHidden && _dalamudUtilService.IsGameUiHidden)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.HideInCombat && _dalamudUtilService.IsInCombat)
|
if (config.HideInCombat && _dalamudUtilService.IsInCombat)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
@@ -409,9 +386,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
bottomColor);
|
bottomColor);
|
||||||
|
|
||||||
var showTimestamps = _chatConfigService.Current.ShowMessageTimestamps;
|
var showTimestamps = _chatConfigService.Current.ShowMessageTimestamps;
|
||||||
_chatEmoteService.EnsureGlobalEmotesLoaded();
|
|
||||||
PairUiSnapshot? pairSnapshot = null;
|
|
||||||
var itemSpacing = ImGui.GetStyle().ItemSpacing.X;
|
|
||||||
|
|
||||||
if (channel.Messages.Count == 0)
|
if (channel.Messages.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -449,109 +423,16 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
timestampText = $"[{message.ReceivedAtUtc.ToLocalTime().ToString("HH:mm", CultureInfo.InvariantCulture)}] ";
|
timestampText = $"[{message.ReceivedAtUtc.ToLocalTime().ToString("HH:mm", CultureInfo.InvariantCulture)}] ";
|
||||||
}
|
}
|
||||||
var color = message.FromSelf ? UIColors.Get("LightlessBlue") : ImGuiColors.DalamudWhite;
|
var color = message.FromSelf ? UIColors.Get("LightlessBlue") : ImGuiColors.DalamudWhite;
|
||||||
var showRoleIcons = false;
|
|
||||||
var isOwner = false;
|
|
||||||
var isModerator = false;
|
|
||||||
var isPinned = false;
|
|
||||||
|
|
||||||
if (channel.Type == ChatChannelType.Group
|
|
||||||
&& payload.Sender.Kind == ChatSenderKind.IdentifiedUser
|
|
||||||
&& payload.Sender.User is not null)
|
|
||||||
{
|
|
||||||
pairSnapshot ??= _pairUiService.GetSnapshot();
|
|
||||||
var groupId = channel.Descriptor.CustomKey;
|
|
||||||
if (!string.IsNullOrWhiteSpace(groupId)
|
|
||||||
&& pairSnapshot.GroupsByGid.TryGetValue(groupId, out var groupInfo))
|
|
||||||
{
|
|
||||||
var senderUid = payload.Sender.User.UID;
|
|
||||||
isOwner = string.Equals(senderUid, groupInfo.OwnerUID, StringComparison.Ordinal);
|
|
||||||
if (groupInfo.GroupPairUserInfos.TryGetValue(senderUid, out var info))
|
|
||||||
{
|
|
||||||
isModerator = info.IsModerator();
|
|
||||||
isPinned = info.IsPinned();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showRoleIcons = isOwner || isModerator || isPinned;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.BeginGroup();
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.Text, color);
|
ImGui.PushStyleColor(ImGuiCol.Text, color);
|
||||||
if (showRoleIcons)
|
ImGui.TextWrapped($"{timestampText}{message.DisplayName}: {payload.Message}");
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(timestampText))
|
|
||||||
{
|
|
||||||
ImGui.TextUnformatted(timestampText);
|
|
||||||
ImGui.SameLine(0f, 0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasIcon = false;
|
|
||||||
if (isModerator)
|
|
||||||
{
|
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.UserShield, UIColors.Get("LightlessPurple"));
|
|
||||||
UiSharedService.AttachToolTip("Moderator");
|
|
||||||
hasIcon = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isOwner)
|
|
||||||
{
|
|
||||||
if (hasIcon)
|
|
||||||
{
|
|
||||||
ImGui.SameLine(0f, itemSpacing);
|
|
||||||
}
|
|
||||||
|
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.Crown, UIColors.Get("LightlessYellow"));
|
|
||||||
UiSharedService.AttachToolTip("Owner");
|
|
||||||
hasIcon = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPinned)
|
|
||||||
{
|
|
||||||
if (hasIcon)
|
|
||||||
{
|
|
||||||
ImGui.SameLine(0f, itemSpacing);
|
|
||||||
}
|
|
||||||
|
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.Thumbtack, UIColors.Get("LightlessBlue"));
|
|
||||||
UiSharedService.AttachToolTip("Pinned");
|
|
||||||
hasIcon = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasIcon)
|
|
||||||
{
|
|
||||||
ImGui.SameLine(0f, itemSpacing);
|
|
||||||
}
|
|
||||||
|
|
||||||
var messageStartX = ImGui.GetCursorPosX();
|
|
||||||
DrawChatMessageWithEmotes($"{message.DisplayName}: ", payload.Message, messageStartX);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var messageStartX = ImGui.GetCursorPosX();
|
|
||||||
DrawChatMessageWithEmotes($"{timestampText}{message.DisplayName}: ", payload.Message, messageStartX);
|
|
||||||
}
|
|
||||||
ImGui.PopStyleColor();
|
ImGui.PopStyleColor();
|
||||||
ImGui.EndGroup();
|
|
||||||
|
|
||||||
ImGui.SetNextWindowSizeConstraints(
|
|
||||||
new Vector2(190f * ImGuiHelpers.GlobalScale, 0f),
|
|
||||||
new Vector2(float.MaxValue, float.MaxValue));
|
|
||||||
if (ImGui.BeginPopupContextItem($"chat_msg_ctx##{channel.Key}_{i}"))
|
if (ImGui.BeginPopupContextItem($"chat_msg_ctx##{channel.Key}_{i}"))
|
||||||
{
|
{
|
||||||
var contextLocalTimestamp = payload.SentAtUtc.ToLocalTime();
|
var contextLocalTimestamp = payload.SentAtUtc.ToLocalTime();
|
||||||
var contextTimestampText = contextLocalTimestamp.ToString("yyyy-MM-dd HH:mm:ss 'UTC'z", CultureInfo.InvariantCulture);
|
var contextTimestampText = contextLocalTimestamp.ToString("yyyy-MM-dd HH:mm:ss 'UTC'z", CultureInfo.InvariantCulture);
|
||||||
ImGui.TextDisabled(contextTimestampText);
|
ImGui.TextDisabled(contextTimestampText);
|
||||||
if (channel.Type == ChatChannelType.Group
|
|
||||||
&& payload.Sender.Kind == ChatSenderKind.IdentifiedUser
|
|
||||||
&& payload.Sender.User is not null)
|
|
||||||
{
|
|
||||||
var aliasOrUid = payload.Sender.User.AliasOrUID;
|
|
||||||
if (!string.IsNullOrWhiteSpace(aliasOrUid)
|
|
||||||
&& !string.Equals(message.DisplayName, aliasOrUid, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
ImGui.TextDisabled(aliasOrUid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
|
|
||||||
var actionIndex = 0;
|
var actionIndex = 0;
|
||||||
@@ -580,335 +461,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawChatMessageWithEmotes(string prefix, string message, float lineStartX)
|
|
||||||
{
|
|
||||||
var segments = BuildChatSegments(prefix, message);
|
|
||||||
var firstOnLine = true;
|
|
||||||
var emoteSize = new Vector2(ImGui.GetTextLineHeight());
|
|
||||||
var remainingWidth = ImGui.GetContentRegionAvail().X;
|
|
||||||
|
|
||||||
foreach (var segment in segments)
|
|
||||||
{
|
|
||||||
if (segment.IsLineBreak)
|
|
||||||
{
|
|
||||||
if (firstOnLine)
|
|
||||||
{
|
|
||||||
ImGui.NewLine();
|
|
||||||
}
|
|
||||||
ImGui.SetCursorPosX(lineStartX);
|
|
||||||
firstOnLine = true;
|
|
||||||
remainingWidth = ImGui.GetContentRegionAvail().X;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (segment.IsWhitespace && firstOnLine)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var segmentWidth = segment.IsEmote ? emoteSize.X : ImGui.CalcTextSize(segment.Text).X;
|
|
||||||
if (!firstOnLine)
|
|
||||||
{
|
|
||||||
if (segmentWidth > remainingWidth)
|
|
||||||
{
|
|
||||||
ImGui.SetCursorPosX(lineStartX);
|
|
||||||
firstOnLine = true;
|
|
||||||
remainingWidth = ImGui.GetContentRegionAvail().X;
|
|
||||||
if (segment.IsWhitespace)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui.SameLine(0f, 0f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (segment.IsEmote && segment.Texture is not null)
|
|
||||||
{
|
|
||||||
ImGui.Image(segment.Texture.Handle, emoteSize);
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
{
|
|
||||||
DrawEmoteTooltip(segment.EmoteName ?? string.Empty, segment.Texture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui.TextUnformatted(segment.Text);
|
|
||||||
}
|
|
||||||
|
|
||||||
remainingWidth -= segmentWidth;
|
|
||||||
firstOnLine = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawEmotePickerPopup(ref string draft, string channelKey)
|
|
||||||
{
|
|
||||||
if (_openEmotePicker)
|
|
||||||
{
|
|
||||||
ImGui.OpenPopup(EmotePickerPopupId);
|
|
||||||
_openEmotePicker = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var style = ImGui.GetStyle();
|
|
||||||
var scale = ImGuiHelpers.GlobalScale;
|
|
||||||
var emoteSize = 32f * scale;
|
|
||||||
var itemWidth = emoteSize + (style.FramePadding.X * 2f);
|
|
||||||
var gridWidth = (itemWidth * EmotePickerColumns) + (style.ItemSpacing.X * Math.Max(0, EmotePickerColumns - 1));
|
|
||||||
var scrollbarPadding = style.ScrollbarSize + (style.ItemSpacing.X * 2f) + (8f * scale);
|
|
||||||
var windowWidth = gridWidth + scrollbarPadding + (style.WindowPadding.X * 2f);
|
|
||||||
ImGui.SetNextWindowSize(new Vector2(windowWidth, 340f * scale), ImGuiCond.Always);
|
|
||||||
if (!ImGui.BeginPopup(EmotePickerPopupId))
|
|
||||||
return;
|
|
||||||
|
|
||||||
ImGui.TextUnformatted("Emotes");
|
|
||||||
ImGui.Separator();
|
|
||||||
|
|
||||||
ImGui.SetNextItemWidth(-1f);
|
|
||||||
ImGui.InputTextWithHint("##emote_filter", "Search Emotes", ref _emoteFilter, 50);
|
|
||||||
ImGui.Spacing();
|
|
||||||
|
|
||||||
var emotes = _chatEmoteService.GetEmoteNames();
|
|
||||||
var filter = _emoteFilter.Trim();
|
|
||||||
var hasFilter = filter.Length > 0;
|
|
||||||
|
|
||||||
using (var child = ImRaii.Child("emote_picker_list", new Vector2(-1f, 0f), true))
|
|
||||||
{
|
|
||||||
if (child)
|
|
||||||
{
|
|
||||||
var any = false;
|
|
||||||
var itemHeight = emoteSize + (style.FramePadding.Y * 2f);
|
|
||||||
var cellWidth = itemWidth + style.ItemSpacing.X;
|
|
||||||
var availableWidth = Math.Max(1f, ImGui.GetContentRegionAvail().X);
|
|
||||||
var maxColumns = Math.Max(1, (int)MathF.Floor((availableWidth + style.ItemSpacing.X) / cellWidth));
|
|
||||||
var columns = Math.Max(1, Math.Min(EmotePickerColumns, maxColumns));
|
|
||||||
var columnIndex = 0;
|
|
||||||
foreach (var emote in emotes)
|
|
||||||
{
|
|
||||||
if (hasFilter && !emote.Contains(filter, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
any = true;
|
|
||||||
IDalamudTextureWrap? texture = null;
|
|
||||||
_chatEmoteService.TryGetEmote(emote, out texture);
|
|
||||||
|
|
||||||
ImGui.PushID(emote);
|
|
||||||
var clicked = false;
|
|
||||||
if (texture is not null)
|
|
||||||
{
|
|
||||||
clicked = ImGui.ImageButton(texture.Handle, new Vector2(emoteSize));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
clicked = ImGui.Button("?", new Vector2(itemWidth, itemHeight));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
{
|
|
||||||
DrawEmoteTooltip(emote, texture);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.PopID();
|
|
||||||
|
|
||||||
if (clicked)
|
|
||||||
{
|
|
||||||
AppendEmoteToDraft(ref draft, emote);
|
|
||||||
_draftMessages[channelKey] = draft;
|
|
||||||
_refocusChatInput = true;
|
|
||||||
_refocusChatInputKey = channelKey;
|
|
||||||
ImGui.CloseCurrentPopup();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
columnIndex++;
|
|
||||||
if (columnIndex < columns)
|
|
||||||
{
|
|
||||||
ImGui.SameLine();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
columnIndex = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!any)
|
|
||||||
{
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
|
|
||||||
ImGui.TextUnformatted(emotes.Count == 0 ? "Loading emotes..." : "No emotes found.");
|
|
||||||
ImGui.PopStyleColor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.EndPopup();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AppendEmoteToDraft(ref string draft, string emote)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(draft))
|
|
||||||
{
|
|
||||||
draft = emote;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (char.IsWhiteSpace(draft[^1]))
|
|
||||||
{
|
|
||||||
draft += emote;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
draft += " " + emote;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<ChatSegment> BuildChatSegments(string prefix, string message)
|
|
||||||
{
|
|
||||||
var segments = new List<ChatSegment>(Math.Max(16, message.Length / 4));
|
|
||||||
AppendChatSegments(segments, prefix, allowEmotes: false);
|
|
||||||
AppendChatSegments(segments, message, allowEmotes: true);
|
|
||||||
return segments;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AppendChatSegments(List<ChatSegment> segments, string text, bool allowEmotes)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(text))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var index = 0;
|
|
||||||
while (index < text.Length)
|
|
||||||
{
|
|
||||||
if (text[index] == '\n')
|
|
||||||
{
|
|
||||||
segments.Add(ChatSegment.LineBreak());
|
|
||||||
index++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (text[index] == '\r')
|
|
||||||
{
|
|
||||||
index++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (char.IsWhiteSpace(text[index]))
|
|
||||||
{
|
|
||||||
var start = index;
|
|
||||||
while (index < text.Length && char.IsWhiteSpace(text[index]) && text[index] != '\n' && text[index] != '\r')
|
|
||||||
{
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
segments.Add(ChatSegment.FromText(text[start..index], isWhitespace: true));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var tokenStart = index;
|
|
||||||
while (index < text.Length && !char.IsWhiteSpace(text[index]))
|
|
||||||
{
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
var token = text[tokenStart..index];
|
|
||||||
if (allowEmotes && TrySplitToken(token, out var leading, out var core, out var trailing))
|
|
||||||
{
|
|
||||||
if (_chatEmoteService.TryGetEmote(core, out var texture) && texture is not null)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(leading))
|
|
||||||
{
|
|
||||||
segments.Add(ChatSegment.FromText(leading));
|
|
||||||
}
|
|
||||||
|
|
||||||
segments.Add(ChatSegment.Emote(texture, core));
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(trailing))
|
|
||||||
{
|
|
||||||
segments.Add(ChatSegment.FromText(trailing));
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
segments.Add(ChatSegment.FromText(token));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TrySplitToken(string token, out string leading, out string core, out string trailing)
|
|
||||||
{
|
|
||||||
leading = string.Empty;
|
|
||||||
core = string.Empty;
|
|
||||||
trailing = string.Empty;
|
|
||||||
|
|
||||||
var start = 0;
|
|
||||||
while (start < token.Length && !IsEmoteChar(token[start]))
|
|
||||||
{
|
|
||||||
start++;
|
|
||||||
}
|
|
||||||
|
|
||||||
var end = token.Length - 1;
|
|
||||||
while (end >= start && !IsEmoteChar(token[end]))
|
|
||||||
{
|
|
||||||
end--;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (start > end)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
leading = token[..start];
|
|
||||||
core = token[start..(end + 1)];
|
|
||||||
trailing = token[(end + 1)..];
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsEmoteChar(char value)
|
|
||||||
{
|
|
||||||
return char.IsLetterOrDigit(value) || value == '_' || value == '-' || value == '!';
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawEmoteTooltip(string name, IDalamudTextureWrap? texture)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(name) && texture is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.BeginTooltip();
|
|
||||||
ImGui.SetWindowFontScale(1f);
|
|
||||||
|
|
||||||
if (texture is not null)
|
|
||||||
{
|
|
||||||
var size = 48f * ImGuiHelpers.GlobalScale;
|
|
||||||
ImGui.Image(texture.Handle, new Vector2(size));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(name))
|
|
||||||
{
|
|
||||||
if (texture is not null)
|
|
||||||
{
|
|
||||||
ImGui.Spacing();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.TextUnformatted(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.EndTooltip();
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly record struct ChatSegment(string Text, IDalamudTextureWrap? Texture, string? EmoteName, bool IsEmote, bool IsWhitespace, bool IsLineBreak)
|
|
||||||
{
|
|
||||||
public static ChatSegment FromText(string text, bool isWhitespace = false) => new(text, null, null, false, isWhitespace, false);
|
|
||||||
public static ChatSegment Emote(IDalamudTextureWrap texture, string name) => new(string.Empty, texture, name, true, false, false);
|
|
||||||
public static ChatSegment LineBreak() => new(string.Empty, null, null, false, false, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawInput(ChatChannelSnapshot channel)
|
private void DrawInput(ChatChannelSnapshot channel)
|
||||||
{
|
{
|
||||||
const int MaxMessageLength = ZoneChatService.MaxOutgoingLength;
|
const int MaxMessageLength = ZoneChatService.MaxOutgoingLength;
|
||||||
@@ -917,10 +469,9 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
draft ??= string.Empty;
|
draft ??= string.Empty;
|
||||||
|
|
||||||
var style = ImGui.GetStyle();
|
var style = ImGui.GetStyle();
|
||||||
var sendButtonWidth = 70f * ImGuiHelpers.GlobalScale;
|
var sendButtonWidth = 100f * ImGuiHelpers.GlobalScale;
|
||||||
var emoteButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Comments).X;
|
|
||||||
var counterWidth = ImGui.CalcTextSize($"{MaxMessageLength}/{MaxMessageLength}").X;
|
var counterWidth = ImGui.CalcTextSize($"{MaxMessageLength}/{MaxMessageLength}").X;
|
||||||
var reservedWidth = sendButtonWidth + emoteButtonWidth + counterWidth + style.ItemSpacing.X * 3f;
|
var reservedWidth = sendButtonWidth + counterWidth + style.ItemSpacing.X * 2f;
|
||||||
|
|
||||||
ImGui.SetNextItemWidth(-reservedWidth);
|
ImGui.SetNextItemWidth(-reservedWidth);
|
||||||
var inputId = $"##chat-input-{channel.Key}";
|
var inputId = $"##chat-input-{channel.Key}";
|
||||||
@@ -931,7 +482,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
_refocusChatInputKey = null;
|
_refocusChatInputKey = null;
|
||||||
}
|
}
|
||||||
ImGui.InputText(inputId, ref draft, MaxMessageLength);
|
ImGui.InputText(inputId, ref draft, MaxMessageLength);
|
||||||
if (ImGui.IsItemActive())
|
if (ImGui.IsItemActive() || ImGui.IsItemFocused())
|
||||||
{
|
{
|
||||||
var drawList = ImGui.GetWindowDrawList();
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
var itemMin = ImGui.GetItemRectMin();
|
var itemMin = ImGui.GetItemRectMin();
|
||||||
@@ -953,22 +504,10 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
var buttonScreenPos = ImGui.GetCursorScreenPos();
|
var buttonScreenPos = ImGui.GetCursorScreenPos();
|
||||||
var rightEdgeScreen = ImGui.GetWindowPos().X + ImGui.GetWindowContentRegionMax().X;
|
var rightEdgeScreen = ImGui.GetWindowPos().X + ImGui.GetWindowContentRegionMax().X;
|
||||||
|
var desiredButtonX = rightEdgeScreen - sendButtonWidth;
|
||||||
var minButtonX = buttonScreenPos.X + style.ItemSpacing.X;
|
var minButtonX = buttonScreenPos.X + style.ItemSpacing.X;
|
||||||
var desiredSendX = rightEdgeScreen - sendButtonWidth;
|
var finalButtonX = MathF.Max(minButtonX, desiredButtonX);
|
||||||
var sendX = MathF.Max(minButtonX + emoteButtonWidth + style.ItemSpacing.X, desiredSendX);
|
ImGui.SetCursorScreenPos(new Vector2(finalButtonX, buttonScreenPos.Y));
|
||||||
var emoteX = sendX - style.ItemSpacing.X - emoteButtonWidth;
|
|
||||||
|
|
||||||
ImGui.SetCursorScreenPos(new Vector2(emoteX, buttonScreenPos.Y));
|
|
||||||
if (_uiSharedService.IconButton(FontAwesomeIcon.Comments))
|
|
||||||
{
|
|
||||||
_openEmotePicker = true;
|
|
||||||
}
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
{
|
|
||||||
ImGui.SetTooltip("Open Emotes");
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.SetCursorScreenPos(new Vector2(sendX, buttonScreenPos.Y));
|
|
||||||
var sendColor = UIColors.Get("LightlessPurpleDefault");
|
var sendColor = UIColors.Get("LightlessPurpleDefault");
|
||||||
var sendHovered = UIColors.Get("LightlessPurple");
|
var sendHovered = UIColors.Get("LightlessPurple");
|
||||||
var sendActive = UIColors.Get("LightlessPurpleActive");
|
var sendActive = UIColors.Get("LightlessPurpleActive");
|
||||||
@@ -979,7 +518,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
var sendClicked = false;
|
var sendClicked = false;
|
||||||
using (ImRaii.Disabled(!canSend))
|
using (ImRaii.Disabled(!canSend))
|
||||||
{
|
{
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PaperPlane, $"Send##chat-send-{channel.Key}", sendButtonWidth, center: true))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PaperPlane, $"Send##chat-send-{channel.Key}", 100f * ImGuiHelpers.GlobalScale, center: true))
|
||||||
{
|
{
|
||||||
sendClicked = true;
|
sendClicked = true;
|
||||||
}
|
}
|
||||||
@@ -987,71 +526,47 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.PopStyleVar();
|
ImGui.PopStyleVar();
|
||||||
ImGui.PopStyleColor(3);
|
ImGui.PopStyleColor(3);
|
||||||
|
|
||||||
DrawEmotePickerPopup(ref draft, channel.Key);
|
|
||||||
|
|
||||||
if (canSend && (enterPressed || sendClicked))
|
if (canSend && (enterPressed || sendClicked))
|
||||||
{
|
{
|
||||||
_refocusChatInput = true;
|
_refocusChatInput = true;
|
||||||
_refocusChatInputKey = channel.Key;
|
_refocusChatInputKey = channel.Key;
|
||||||
|
if (TrySendDraft(channel, draft))
|
||||||
var draftAtSend = draft;
|
|
||||||
var sanitized = SanitizeOutgoingDraft(draftAtSend);
|
|
||||||
|
|
||||||
if (sanitized is not null)
|
|
||||||
{
|
{
|
||||||
TrackPendingDraftClear(channel.Key, sanitized);
|
_draftMessages[channel.Key] = string.Empty;
|
||||||
draft = string.Empty;
|
|
||||||
_draftMessages[channel.Key] = draft;
|
|
||||||
_scrollToBottom = true;
|
_scrollToBottom = true;
|
||||||
|
|
||||||
_ = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var succeeded = await _zoneChatService.SendMessageAsync(channel.Descriptor, sanitized).ConfigureAwait(false);
|
|
||||||
if (!succeeded)
|
|
||||||
{
|
|
||||||
RemovePendingDraftClear(channel.Key, sanitized);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Failed to send chat message");
|
|
||||||
RemovePendingDraftClear(channel.Key, sanitized);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawRulesOverlay()
|
private void DrawRulesOverlay()
|
||||||
{
|
{
|
||||||
|
var windowPos = ImGui.GetWindowPos();
|
||||||
|
var windowSize = ImGui.GetWindowSize();
|
||||||
var parentContentMin = ImGui.GetWindowContentRegionMin();
|
var parentContentMin = ImGui.GetWindowContentRegionMin();
|
||||||
var parentContentMax = ImGui.GetWindowContentRegionMax();
|
var parentContentMax = ImGui.GetWindowContentRegionMax();
|
||||||
|
var overlayPos = windowPos + parentContentMin;
|
||||||
var overlaySize = parentContentMax - parentContentMin;
|
var overlaySize = parentContentMax - parentContentMin;
|
||||||
|
|
||||||
if (overlaySize.X <= 0f || overlaySize.Y <= 0f)
|
if (overlaySize.X <= 0f || overlaySize.Y <= 0f)
|
||||||
{
|
{
|
||||||
parentContentMin = Vector2.Zero;
|
overlayPos = windowPos;
|
||||||
overlaySize = ImGui.GetWindowSize();
|
overlaySize = windowSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
var previousCursor = ImGui.GetCursorPos();
|
ImGui.SetNextWindowFocus();
|
||||||
ImGui.SetCursorPos(parentContentMin);
|
ImGui.SetNextWindowPos(overlayPos);
|
||||||
|
ImGui.SetNextWindowSize(overlaySize);
|
||||||
var bgColor = ImGui.GetStyle().Colors[(int)ImGuiCol.WindowBg];
|
ImGui.SetNextWindowBgAlpha(0.86f);
|
||||||
bgColor.W = 0.86f;
|
ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 6f * ImGuiHelpers.GlobalScale);
|
||||||
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 6f * ImGuiHelpers.GlobalScale);
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.ChildBorderSize, 0f);
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.Border, Vector4.Zero);
|
ImGui.PushStyleColor(ImGuiCol.Border, Vector4.Zero);
|
||||||
ImGui.PushStyleColor(ImGuiCol.ChildBg, bgColor);
|
|
||||||
|
|
||||||
var overlayFlags = ImGuiWindowFlags.NoScrollbar
|
var overlayFlags = ImGuiWindowFlags.NoDecoration
|
||||||
| ImGuiWindowFlags.NoScrollWithMouse
|
| ImGuiWindowFlags.NoMove
|
||||||
|
| ImGuiWindowFlags.NoScrollbar
|
||||||
| ImGuiWindowFlags.NoSavedSettings;
|
| ImGuiWindowFlags.NoSavedSettings;
|
||||||
|
|
||||||
if (ImGui.BeginChild("##zone_chat_rules_overlay", overlaySize, false, overlayFlags))
|
var overlayOpen = true;
|
||||||
|
if (ImGui.Begin("##zone_chat_rules_overlay", ref overlayOpen, overlayFlags))
|
||||||
{
|
{
|
||||||
var contentMin = ImGui.GetWindowContentRegionMin();
|
var contentMin = ImGui.GetWindowContentRegionMin();
|
||||||
var contentMax = ImGui.GetWindowContentRegionMax();
|
var contentMax = ImGui.GetWindowContentRegionMax();
|
||||||
@@ -1171,12 +686,16 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
_showRulesOverlay = false;
|
_showRulesOverlay = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!overlayOpen)
|
||||||
|
{
|
||||||
|
_showRulesOverlay = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.EndChild();
|
ImGui.End();
|
||||||
ImGui.PopStyleColor(2);
|
ImGui.PopStyleColor();
|
||||||
ImGui.PopStyleVar(2);
|
ImGui.PopStyleVar();
|
||||||
ImGui.SetCursorPos(previousCursor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawReportPopup()
|
private void DrawReportPopup()
|
||||||
@@ -1424,15 +943,16 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
_reportPopupRequested = false;
|
_reportPopupRequested = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TrySendDraft(ChatChannelSnapshot channel, string sanitizedMessage)
|
private bool TrySendDraft(ChatChannelSnapshot channel, string draft)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(sanitizedMessage))
|
var trimmed = draft.Trim();
|
||||||
|
if (trimmed.Length == 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
bool succeeded;
|
bool succeeded;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
succeeded = _zoneChatService.SendMessageAsync(channel.Descriptor, sanitizedMessage).GetAwaiter().GetResult();
|
succeeded = _zoneChatService.SendMessageAsync(channel.Descriptor, trimmed).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -1467,21 +987,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
yield return reportAction;
|
yield return reportAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
var moderationActions = new List<ChatMessageContextAction>();
|
|
||||||
foreach (var action in GetSyncshellModerationActions(channel, message, payload))
|
|
||||||
{
|
|
||||||
moderationActions.Add(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (moderationActions.Count > 0)
|
|
||||||
{
|
|
||||||
yield return ChatMessageContextAction.Separator();
|
|
||||||
foreach (var action in moderationActions)
|
|
||||||
{
|
|
||||||
yield return action;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryCreateCopyMessageAction(ChatMessageEntry message, ChatMessageDto payload, out ChatMessageContextAction action)
|
private static bool TryCreateCopyMessageAction(ChatMessageEntry message, ChatMessageDto payload, out ChatMessageContextAction action)
|
||||||
@@ -1589,91 +1094,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<ChatMessageContextAction> GetSyncshellModerationActions(ChatChannelSnapshot channel, ChatMessageEntry message, ChatMessageDto payload)
|
|
||||||
{
|
|
||||||
if (channel.Type != ChatChannelType.Group)
|
|
||||||
yield break;
|
|
||||||
|
|
||||||
if (message.FromSelf)
|
|
||||||
yield break;
|
|
||||||
|
|
||||||
if (payload.Sender.Kind != ChatSenderKind.IdentifiedUser || payload.Sender.User is null)
|
|
||||||
yield break;
|
|
||||||
|
|
||||||
var groupId = channel.Descriptor.CustomKey;
|
|
||||||
if (string.IsNullOrWhiteSpace(groupId))
|
|
||||||
yield break;
|
|
||||||
|
|
||||||
var snapshot = _pairUiService.GetSnapshot();
|
|
||||||
if (!snapshot.GroupsByGid.TryGetValue(groupId, out var groupInfo))
|
|
||||||
yield break;
|
|
||||||
|
|
||||||
var sender = payload.Sender.User;
|
|
||||||
var senderUid = sender.UID;
|
|
||||||
if (string.IsNullOrWhiteSpace(senderUid))
|
|
||||||
yield break;
|
|
||||||
|
|
||||||
var selfIsOwner = string.Equals(groupInfo.OwnerUID, _apiController.UID, StringComparison.Ordinal);
|
|
||||||
var selfIsModerator = groupInfo.GroupUserInfo.IsModerator();
|
|
||||||
if (!selfIsOwner && !selfIsModerator)
|
|
||||||
yield break;
|
|
||||||
|
|
||||||
var senderInfo = groupInfo.GroupPairUserInfos.TryGetValue(senderUid, out var info) ? info : GroupPairUserInfo.None;
|
|
||||||
var userIsModerator = senderInfo.IsModerator();
|
|
||||||
var userIsPinned = senderInfo.IsPinned();
|
|
||||||
|
|
||||||
var showModeratorActions = selfIsOwner || (selfIsModerator && !userIsModerator);
|
|
||||||
if (!showModeratorActions)
|
|
||||||
yield break;
|
|
||||||
|
|
||||||
if (showModeratorActions)
|
|
||||||
{
|
|
||||||
var pinLabel = userIsPinned ? "Unpin user" : "Pin user";
|
|
||||||
yield return new ChatMessageContextAction(
|
|
||||||
FontAwesomeIcon.Thumbtack,
|
|
||||||
pinLabel,
|
|
||||||
true,
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
var updatedInfo = senderInfo;
|
|
||||||
updatedInfo.SetPinned(!userIsPinned);
|
|
||||||
_ = _apiController.GroupSetUserInfo(new GroupPairUserInfoDto(groupInfo.Group, sender, updatedInfo));
|
|
||||||
});
|
|
||||||
|
|
||||||
var removeEnabled = UiSharedService.CtrlPressed();
|
|
||||||
var removeLabel = removeEnabled ? "Remove user" : "Remove user (Hold CTRL)";
|
|
||||||
yield return new ChatMessageContextAction(
|
|
||||||
FontAwesomeIcon.Trash,
|
|
||||||
removeLabel,
|
|
||||||
removeEnabled,
|
|
||||||
() => _ = _apiController.GroupRemoveUser(new GroupPairDto(groupInfo.Group, sender)),
|
|
||||||
"Syncshell action: removes the user from the syncshell, not just chat.");
|
|
||||||
|
|
||||||
var banPair = ResolveBanPair(snapshot, senderUid, sender, groupInfo);
|
|
||||||
var banEnabled = UiSharedService.CtrlPressed();
|
|
||||||
var banLabel = banEnabled ? "Ban user" : "Ban user (Hold CTRL)";
|
|
||||||
yield return new ChatMessageContextAction(
|
|
||||||
FontAwesomeIcon.UserSlash,
|
|
||||||
banLabel,
|
|
||||||
banEnabled,
|
|
||||||
() => Mediator.Publish(new OpenBanUserPopupMessage(banPair!, groupInfo)),
|
|
||||||
"Hold CTRL to ban the user from the syncshell, not just chat.");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private Pair? ResolveBanPair(PairUiSnapshot snapshot, string senderUid, UserData sender, GroupFullInfoDto groupInfo)
|
|
||||||
{
|
|
||||||
if (snapshot.PairsByUid.TryGetValue(senderUid, out var pair))
|
|
||||||
{
|
|
||||||
return pair;
|
|
||||||
}
|
|
||||||
|
|
||||||
var connection = new PairConnection(sender);
|
|
||||||
var entry = new PairDisplayEntry(new PairUniqueIdentifier(senderUid), connection, new[] { groupInfo }, null);
|
|
||||||
return _pairFactory.Create(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task OpenStandardProfileAsync(UserData user)
|
private Task OpenStandardProfileAsync(UserData user)
|
||||||
{
|
{
|
||||||
_profileManager.GetLightlessProfile(user);
|
_profileManager.GetLightlessProfile(user);
|
||||||
@@ -1704,92 +1124,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
_scrollToBottom = true;
|
_scrollToBottom = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!message.Message.FromSelf || message.Message.Payload?.Message is not { Length: > 0 } payloadText)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var matchedPending = false;
|
|
||||||
if (_pendingDraftClears.TryGetValue(message.ChannelKey, out var pending))
|
|
||||||
{
|
|
||||||
var pendingIndex = pending.FindIndex(text => string.Equals(text, payloadText, StringComparison.Ordinal));
|
|
||||||
if (pendingIndex >= 0)
|
|
||||||
{
|
|
||||||
pending.RemoveAt(pendingIndex);
|
|
||||||
matchedPending = true;
|
|
||||||
if (pending.Count == 0)
|
|
||||||
{
|
|
||||||
_pendingDraftClears.Remove(message.ChannelKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matchedPending && _draftMessages.TryGetValue(message.ChannelKey, out var currentDraft))
|
|
||||||
{
|
|
||||||
var sanitizedCurrent = SanitizeOutgoingDraft(currentDraft);
|
|
||||||
if (sanitizedCurrent is not null && string.Equals(sanitizedCurrent, payloadText, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
_draftMessages[message.ChannelKey] = string.Empty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? SanitizeOutgoingDraft(string draft)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(draft))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sanitized = draft.Trim().ReplaceLineEndings(" ");
|
|
||||||
if (sanitized.Length == 0)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sanitized.Length > ZoneChatService.MaxOutgoingLength)
|
|
||||||
{
|
|
||||||
sanitized = sanitized[..ZoneChatService.MaxOutgoingLength];
|
|
||||||
}
|
|
||||||
|
|
||||||
return sanitized;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TrackPendingDraftClear(string channelKey, string message)
|
|
||||||
{
|
|
||||||
if (!_pendingDraftClears.TryGetValue(channelKey, out var pending))
|
|
||||||
{
|
|
||||||
pending = new List<string>();
|
|
||||||
_pendingDraftClears[channelKey] = pending;
|
|
||||||
}
|
|
||||||
|
|
||||||
pending.Add(message);
|
|
||||||
const int MaxPendingDrafts = 12;
|
|
||||||
if (pending.Count > MaxPendingDrafts)
|
|
||||||
{
|
|
||||||
pending.RemoveAt(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RemovePendingDraftClear(string channelKey, string message)
|
|
||||||
{
|
|
||||||
if (!_pendingDraftClears.TryGetValue(channelKey, out var pending))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var index = pending.FindIndex(text => string.Equals(text, message, StringComparison.Ordinal));
|
|
||||||
if (index < 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
pending.RemoveAt(index);
|
|
||||||
if (pending.Count == 0)
|
|
||||||
{
|
|
||||||
_pendingDraftClears.Remove(channelKey);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OpenLightfinderProfileInternalAsync(string hashedCid)
|
private async Task OpenLightfinderProfileInternalAsync(string hashedCid)
|
||||||
@@ -2073,17 +1407,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.SetTooltip("Toggles the timestamp prefix on messages.");
|
ImGui.SetTooltip("Toggles the timestamp prefix on messages.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var showNotesInSyncshellChat = chatConfig.ShowNotesInSyncshellChat;
|
|
||||||
if (ImGui.Checkbox("Show notes in syncshell chat", ref showNotesInSyncshellChat))
|
|
||||||
{
|
|
||||||
chatConfig.ShowNotesInSyncshellChat = showNotesInSyncshellChat;
|
|
||||||
_chatConfigService.Save();
|
|
||||||
}
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
{
|
|
||||||
ImGui.SetTooltip("When enabled, your notes replace user names in syncshell chat.");
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
ImGui.TextUnformatted("Chat Visibility");
|
ImGui.TextUnformatted("Chat Visibility");
|
||||||
|
|
||||||
@@ -2670,12 +1993,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
private void DrawContextMenuAction(ChatMessageContextAction action, int index)
|
private void DrawContextMenuAction(ChatMessageContextAction action, int index)
|
||||||
{
|
{
|
||||||
ImGui.PushID(index);
|
ImGui.PushID(index);
|
||||||
if (action.IsSeparator)
|
|
||||||
{
|
|
||||||
ImGui.Separator();
|
|
||||||
ImGui.PopID();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
using var disabled = ImRaii.Disabled(!action.IsEnabled);
|
using var disabled = ImRaii.Disabled(!action.IsEnabled);
|
||||||
|
|
||||||
var availableWidth = Math.Max(1f, ImGui.GetContentRegionAvail().X);
|
var availableWidth = Math.Max(1f, ImGui.GetContentRegionAvail().X);
|
||||||
@@ -2708,11 +2025,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
drawList.AddText(textPos, textColor, action.Label);
|
drawList.AddText(textPos, textColor, action.Label);
|
||||||
|
|
||||||
if (action.Tooltip is { Length: > 0 } && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
|
||||||
{
|
|
||||||
ImGui.SetTooltip(action.Tooltip);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clicked && action.IsEnabled)
|
if (clicked && action.IsEnabled)
|
||||||
{
|
{
|
||||||
ImGui.CloseCurrentPopup();
|
ImGui.CloseCurrentPopup();
|
||||||
@@ -2722,12 +2034,5 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.PopID();
|
ImGui.PopID();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void NoopContextAction()
|
private readonly record struct ChatMessageContextAction(FontAwesomeIcon? Icon, string Label, bool IsEnabled, Action Execute);
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly record struct ChatMessageContextAction(FontAwesomeIcon? Icon, string Label, bool IsEnabled, Action Execute, string? Tooltip = null, bool IsSeparator = false)
|
|
||||||
{
|
|
||||||
public static ChatMessageContextAction Separator() => new(null, string.Empty, false, ZoneChatUi.NoopContextAction, null, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -544,8 +544,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 +653,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