Merge remote-tracking branch 'origin/2.0.0-crashing-bugfixes' into 2.0.0-crashing-bugfixes

# Conflicts:
#	LightlessSync/Services/DalamudUtilService.cs
#	LightlessSync/UI/DtrEntry.cs
This commit is contained in:
choco
2025-12-28 16:56:06 +01:00
18 changed files with 1038 additions and 371 deletions

View File

@@ -1,11 +1,39 @@
tagline: "Lightless Sync v2.0.1" tagline: "Lightless Sync v2.0.1"
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. Youll 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 arent 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" - name: "v2.0.1"
tagline: "Some Fixes" tagline: "Some Fixes"
date: "December 23 2025" date: "December 23 2025"
# be sure to set this every new version
isCurrent: true
versions: versions:
- number: "Chat" - number: "Chat"
icon: "" icon: ""

View File

@@ -1,15 +1,23 @@
#nullable disable #nullable disable
using System.Text.Json.Serialization;
namespace LightlessSync.FileCache; namespace LightlessSync.FileCache;
public class FileCacheEntity public class FileCacheEntity
{ {
public FileCacheEntity(string hash, string path, string lastModifiedDateTicks, long? size = null, long? compressedSize = null) [JsonConstructor]
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 = path; PrefixedFilePath = prefixedFilePath;
LastModifiedDateTicks = lastModifiedDateTicks; LastModifiedDateTicks = lastModifiedDateTicks;
} }
@@ -23,7 +31,5 @@ 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);
}
} }

View File

@@ -7,6 +7,8 @@ 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;
@@ -31,6 +33,14 @@ 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;
@@ -45,6 +55,18 @@ 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))
@@ -111,6 +133,114 @@ 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;
@@ -318,9 +448,18 @@ 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;
return (fileHash, LZ4Wrapper.WrapHC(await File.ReadAllBytesAsync(fileCache, uploadToken).ConfigureAwait(false), 0, var raw = await File.ReadAllBytesAsync(fileCache, uploadToken).ConfigureAwait(false);
(int)new FileInfo(fileCache).Length)); var compressed = LZ4Wrapper.WrapHC(raw, 0, raw.Length);
UpdateSizeInfo(fileHash, original: raw.LongLength, compressed: compressed.LongLength);
return (fileHash, compressed);
} }
public FileCacheEntity? GetFileCacheByHash(string hash) public FileCacheEntity? GetFileCacheByHash(string hash)
@@ -891,6 +1030,14 @@ 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)

View File

@@ -352,6 +352,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
private void RefreshPlayerRelatedAddressMap() private void RefreshPlayerRelatedAddressMap()
{ {
_playerRelatedByAddress.Clear(); _playerRelatedByAddress.Clear();
var updatedFrameAddresses = new ConcurrentDictionary<nint, ObjectKind>();
lock (_playerRelatedLock) lock (_playerRelatedLock)
{ {
foreach (var handler in _playerRelatedPointers) foreach (var handler in _playerRelatedPointers)
@@ -360,9 +361,12 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
if (address != nint.Zero) if (address != nint.Zero)
{ {
_playerRelatedByAddress[address] = handler; _playerRelatedByAddress[address] = handler;
updatedFrameAddresses[address] = handler.ObjectKind;
} }
} }
} }
_cachedFrameAddresses = updatedFrameAddresses;
} }
private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor) private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor)
@@ -497,9 +501,16 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
{ {
var gameObjectAddress = msg.GameObject; var gameObjectAddress = msg.GameObject;
if (!_cachedFrameAddresses.TryGetValue(gameObjectAddress, out var objectKind)) if (!_cachedFrameAddresses.TryGetValue(gameObjectAddress, out var objectKind))
{
if (_actorObjectService.TryGetOwnedKind(gameObjectAddress, out var ownedKind))
{
objectKind = ownedKind;
}
else
{ {
return; return;
} }
}
var gamePath = NormalizeGamePath(msg.GamePath); var gamePath = NormalizeGamePath(msg.GamePath);
if (string.IsNullOrEmpty(gamePath)) if (string.IsNullOrEmpty(gamePath))

View File

@@ -95,6 +95,12 @@ 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);

View File

@@ -14,6 +14,8 @@ 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;
@@ -27,6 +29,8 @@ 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);
@@ -67,7 +71,13 @@ public sealed class PenumbraResource : PenumbraBase
return await _resolvePlayerPaths.Invoke(forwardPaths, reversePaths).ConfigureAwait(false); return await _resolvePlayerPaths.Invoke(forwardPaths, reversePaths).ConfigureAwait(false);
} }
private void HandleResourceLoaded(nint ptr, string resolvedPath, string gamePath) public string ResolveGameObjectPath(string gamePath, int gameObjectIndex)
=> IsAvailable ? _resolveGameObjectPath.Invoke(gamePath, gameObjectIndex) : gamePath;
public string[] ReverseResolveGameObjectPath(string moddedPath, int gameObjectIndex)
=> IsAvailable ? _reverseResolveGameObjectPath.Invoke(moddedPath, gameObjectIndex) : Array.Empty<string>();
private void HandleResourceLoaded(nint ptr, string gamePath, string resolvedPath)
{ {
if (ptr == nint.Zero) if (ptr == nint.Zero)
{ {
@@ -79,12 +89,12 @@ public sealed class PenumbraResource : PenumbraBase
return; return;
} }
if (string.Compare(resolvedPath, gamePath, StringComparison.OrdinalIgnoreCase) == 0) if (string.Compare(gamePath, resolvedPath, StringComparison.OrdinalIgnoreCase) == 0)
{ {
return; return;
} }
Mediator.Publish(new PenumbraResourceLoadMessage(ptr, resolvedPath, gamePath)); Mediator.Publish(new PenumbraResourceLoadMessage(ptr, gamePath, resolvedPath));
} }
protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current) protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current)

View File

@@ -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(transientPaths, new HashSet<string>(StringComparer.Ordinal)).ConfigureAwait(false); var resolvedTransientPaths = await GetFileReplacementsFromPaths(playerRelatedObject, transientPaths, new HashSet<string>(StringComparer.Ordinal)).ConfigureAwait(false);
if (logDebug) if (logDebug)
{ {
@@ -373,11 +373,73 @@ public class PlayerDataFactory
} }
} }
private async Task<IReadOnlyDictionary<string, string[]>> GetFileReplacementsFromPaths(HashSet<string> forwardResolve, HashSet<string> reverseResolve) private async Task<IReadOnlyDictionary<string, string[]>> GetFileReplacementsFromPaths(GameObjectHandler handler, 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++)
{ {

View File

@@ -484,7 +484,8 @@ public sealed class Plugin : IDalamudPlugin
sp.GetRequiredService<PairUiService>(), sp.GetRequiredService<PairUiService>(),
sp.GetRequiredService<DalamudUtilService>(), sp.GetRequiredService<DalamudUtilService>(),
sp.GetRequiredService<LightlessProfileManager>(), sp.GetRequiredService<LightlessProfileManager>(),
sp.GetRequiredService<ActorObjectService>())); sp.GetRequiredService<ActorObjectService>(),
sp.GetRequiredService<LightFinderPlateHandler>()));
services.AddScoped<IPopupHandler, BanUserPopupHandler>(); services.AddScoped<IPopupHandler, BanUserPopupHandler>();
services.AddScoped<IPopupHandler, CensusPopupHandler>(); services.AddScoped<IPopupHandler, CensusPopupHandler>();

View File

@@ -1,3 +1,4 @@
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;
@@ -98,11 +99,13 @@ 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>(
@@ -125,6 +128,7 @@ 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;
@@ -136,29 +140,47 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
{ {
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
var fileCacheEntries = (await _fileCacheManager.GetAllFileCachesByHashAsync(fileEntry.Hash, ignoreCacheEntries: true, validate: false, token).ConfigureAwait(false)).ToList(); var fileCacheEntries = (await _fileCacheManager
if (fileCacheEntries.Count == 0) continue; .GetAllFileCachesByHashAsync(fileEntry.Hash, ignoreCacheEntries: true, validate: false, token)
var filePath = fileCacheEntries[0].ResolvedFilepath; .ConfigureAwait(false))
FileInfo fi = new(filePath); .ToList();
string ext = "unk?";
try if (fileCacheEntries.Count == 0)
{ continue;
ext = fi.Extension[1..];
} var resolved = fileCacheEntries[0].ResolvedFilepath;
catch (Exception ex)
{ var extWithDot = Path.GetExtension(resolved);
Logger.LogWarning(ex, "Could not identify extension for {path}", filePath); var ext = string.IsNullOrEmpty(extWithDot) ? "unk?" : extWithDot.TrimStart('.');
}
var tris = await _xivDataAnalyzer.GetTrianglesByHash(fileEntry.Hash).ConfigureAwait(false); var tris = await _xivDataAnalyzer.GetTrianglesByHash(fileEntry.Hash).ConfigureAwait(false);
foreach (var entry in fileCacheEntries)
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))
{ {
data[fileEntry.Hash] = new FileDataEntry(fileEntry.Hash, ext, if (orig <= 0 && cached.Original > 0) orig = cached.Original;
[.. fileEntry.GamePaths], if (comp <= 0 && cached.Compressed > 0) comp = cached.Compressed;
[.. 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;
} }
@@ -167,6 +189,7 @@ 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>();
@@ -192,6 +215,7 @@ 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;
@@ -235,42 +259,79 @@ 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)
{ {
var compressedsize = await fileCacheManager.GetCompressedFileData(Hash, token).ConfigureAwait(false); Hash = hash;
var normalSize = new FileInfo(FilePaths[0]).Length; FileType = fileType;
var entries = await fileCacheManager.GetAllFileCachesByHashAsync(Hash, ignoreCacheEntries: true, validate: false, token).ConfigureAwait(false); GamePaths = gamePaths;
foreach (var entry in entries) FilePaths = filePaths;
{ OriginalSize = originalSize;
entry.Size = normalSize; CompressedSize = compressedSize;
entry.CompressedSize = compressedsize.Item2.LongLength; Triangles = triangles;
CacheEntries = cacheEntries;
} }
OriginalSize = normalSize;
CompressedSize = compressedsize.Item2.LongLength; public async Task ComputeSizes(FileCacheManager fileCacheManager, CancellationToken token, bool force = false)
{
if (!force && IsComputed)
return;
if (FilePaths.Count == 0 || string.IsNullOrWhiteSpace(FilePaths[0]))
return;
var path = FilePaths[0];
if (!File.Exists(path))
return;
var original = new FileInfo(path).Length;
var compressedLen = await fileCacheManager.GetCompressedSizeAsync(Hash, token).ConfigureAwait(false);
fileCacheManager.SetSizeInfo(Hash, original, compressedLen);
FileCacheManager.ApplySizesToEntries(CacheEntries, original, compressedLen);
OriginalSize = original;
CompressedSize = compressedLen;
if (string.Equals(FileType, "tex", StringComparison.OrdinalIgnoreCase))
RefreshFormat(); RefreshFormat();
} }
public long OriginalSize { get; private set; } = OriginalSize;
public long CompressedSize { get; private set; } = CompressedSize;
public long Triangles { get; private set; } = Triangles;
public Lazy<string> Format => _format ??= CreateFormatValue();
public Lazy<string> Format => _format ??= CreateFormatValue();
private Lazy<string>? _format; private Lazy<string>? _format;
public void RefreshFormat() public void RefreshFormat() => _format = CreateFormatValue();
{
_format = CreateFormatValue();
}
private Lazy<string> CreateFormatValue() private Lazy<string> CreateFormatValue()
=> new(() => => new(() =>
{ {
if (!string.Equals(FileType, "tex", StringComparison.Ordinal)) if (!string.Equals(FileType, "tex", StringComparison.OrdinalIgnoreCase))
{
return string.Empty; return string.Empty;
}
try try
{ {

View File

@@ -1,13 +1,11 @@
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.Text;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility; using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Control; using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using LightlessSync.API.Dto.CharaData; using LightlessSync.API.Dto.CharaData;
@@ -28,7 +26,6 @@ using System.Text;
using BattleNpcSubKind = FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSubKind; using BattleNpcSubKind = FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSubKind;
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
using Map = Lumina.Excel.Sheets.Map;
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags; using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
namespace LightlessSync.Services; namespace LightlessSync.Services;
@@ -85,18 +82,18 @@ 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>(Dalamud.Game.ClientLanguage.English)! return gameData.GetExcelSheet<Lumina.Excel.Sheets.World>(clientLanguage)!
.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>(Dalamud.Game.ClientLanguage.English)! return gameData.GetExcelSheet<ClassJob>(clientLanguage)!
.ToDictionary(k => k.RowId, k => k.NameEnglish.ToString()); .ToDictionary(k => k.RowId, k => k.Name.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));
@@ -662,7 +659,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
var location = new LocationInfo(); var location = new LocationInfo();
location.ServerId = _playerState.CurrentWorld.RowId; location.ServerId = _playerState.CurrentWorld.RowId;
location.InstanceId = UIState.Instance()->PublicInstance.InstanceId; //location.InstanceId = UIState.Instance()->PublicInstance.InstanceId; //TODO:Need API update first
location.TerritoryId = _clientState.TerritoryType; location.TerritoryId = _clientState.TerritoryType;
location.MapId = _clientState.MapId; location.MapId = _clientState.MapId;
if (houseMan != null) if (houseMan != null)
@@ -716,10 +713,10 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
str += $" - {MapData.Value[(ushort)location.MapId].MapName}"; str += $" - {MapData.Value[(ushort)location.MapId].MapName}";
} }
if (location.InstanceId is not 0) // if (location.InstanceId is not 0)
{ // {
str += ((SeIconChar)(57520 + location.InstanceId)).ToIconString(); // str += ((SeIconChar)(57520 + location.InstanceId)).ToIconString();
} // }
if (location.WardId is not 0) if (location.WardId is not 0)
{ {

View File

@@ -23,6 +23,7 @@ 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;
@@ -41,6 +42,7 @@ 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;
@@ -71,6 +73,12 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
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; private bool IsPictomancyRenderer => _configService.Current.LightfinderLabelRenderer == LightfinderLabelRenderer.Pictomancy;
public LightFinderPlateHandler( public LightFinderPlateHandler(
@@ -96,7 +104,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
_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; _lastRenderer = _configService.Current.LightfinderLabelRenderer;
} }
private void RefreshRendererState() private void RefreshRendererState()
@@ -187,8 +194,6 @@ 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(); RefreshRendererState();
@@ -199,6 +204,16 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
return; 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();
@@ -218,6 +233,10 @@ 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)
@@ -234,6 +253,13 @@ 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)
{ {
@@ -297,7 +323,6 @@ 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;
@@ -314,7 +339,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player) if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player)
continue; continue;
// CID gating // CID gating - only show for active broadcasters
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;
@@ -350,12 +375,12 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
if (!currentConfig.LightfinderLabelShowHidden && !isNameplateVisible) if (!currentConfig.LightfinderLabelShowHidden && !isNameplateVisible)
continue; continue;
// Prepare label content and scaling // Prepare label content and scaling factors
var scaleMultiplier = System.Math.Clamp(currentConfig.LightfinderLabelScale, 0.5f, 2.0f); var scaleMultiplier = 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)System.Math.Round(baseFontSize * scaleMultiplier); var targetFontSize = (int)Math.Round(baseFontSize * scaleMultiplier);
var labelContent = currentConfig.LightfinderLabelUseIcon var labelContent = currentConfig.LightfinderLabelUseIcon
? NormalizeIconGlyph(currentConfig.LightfinderLabelIconGlyph) ? NormalizeIconGlyph(currentConfig.LightfinderLabelIconGlyph)
: _defaultLabelText; : _defaultLabelText;
@@ -363,8 +388,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)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale); var nodeWidth = (int)Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale); var nodeHeight = (int)Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
AlignmentType alignment; AlignmentType alignment;
var textScaleY = nameText->AtkResNode.ScaleY; var textScaleY = nameText->AtkResNode.ScaleY;
@@ -374,7 +399,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
var blockHeight = ResolveCache( var blockHeight = ResolveCache(
_buffers.TextHeights, _buffers.TextHeights,
nameplateIndex, nameplateIndex,
System.Math.Abs((int)nameplateObject.TextH), Math.Abs((int)nameplateObject.TextH),
() => GetScaledTextHeight(nameText), () => GetScaledTextHeight(nameText),
nodeHeight); nodeHeight);
@@ -384,7 +409,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
(int)nameContainer->Height, (int)nameContainer->Height,
() => () =>
{ {
var computed = blockHeight + (int)System.Math.Round(8 * textScaleY); var computed = blockHeight + (int)Math.Round(8 * textScaleY);
return computed <= blockHeight ? blockHeight + 1 : computed; return computed <= blockHeight ? blockHeight + 1 : computed;
}, },
blockHeight + 1); blockHeight + 1);
@@ -392,7 +417,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)System.Math.Round(4 * effectiveScale); var verticalPadding = (int)Math.Round(4 * effectiveScale);
var positionY = blockTop - verticalPadding; var positionY = blockTop - verticalPadding;
@@ -400,21 +425,14 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
var textWidth = ResolveCache( var textWidth = ResolveCache(
_buffers.TextWidths, _buffers.TextWidths,
nameplateIndex, nameplateIndex,
System.Math.Abs(rawTextWidth), Math.Abs(rawTextWidth),
() => GetScaledTextWidth(nameText), () => GetScaledTextWidth(nameText),
nodeWidth); nodeWidth);
// Text offset caching // Text offset caching
var textOffset = (int)System.Math.Round(nameText->AtkResNode.X); var textOffset = (int)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
@@ -450,7 +468,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
var offsetXScreen = currentConfig.LightfinderLabelOffsetX * worldScaleX; var offsetXScreen = currentConfig.LightfinderLabelOffsetX * worldScaleX;
// alignment based on config // alignment based on config setting
switch (currentConfig.LabelAlignment) switch (currentConfig.LabelAlignment)
{ {
case LabelAlignment.Left: case LabelAlignment.Left:
@@ -469,7 +487,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
} }
else else
{ {
// manual X positioning // manual X positioning with optional cached offset
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)
@@ -489,16 +507,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; // often same for Y var dpiScale = ImGui.GetIO().DisplayFramebufferScale.X;
var fw = Framework.Instance(); var fw = Framework.Instance();
float dt = fw->RealFrameDeltaTime; float dt = fw->RealFrameDeltaTime;
//smoothing.. //smoothing.. snap.. smooth.. snap
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 // prepare label info for rendering
var pivot = (currentConfig.LightfinderAutoAlign || currentConfig.LightfinderLabelUseIcon) var pivot = (currentConfig.LightfinderAutoAlign || currentConfig.LightfinderLabelUseIcon)
? AlignmentToPivot(alignment) ? AlignmentToPivot(alignment)
: _defaultPivot; : _defaultPivot;
@@ -545,7 +563,23 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
if (fw == null) if (fw == null)
return; return;
// Frame skip check // If UI is hidden, do not render.
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)
@@ -553,34 +587,62 @@ 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 // Gpose Check - do not render.
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 // If nameplate addon is not visible, skip rendering entirely.
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 != null ? fw->GetUIModule() : null; var uiModule = fw->GetUIModule();
if (uiModule != null) if (uiModule != null)
{ {
var rapture = uiModule->GetRaptureAtkModule(); var rapture = uiModule->GetRaptureAtkModule();
@@ -599,7 +661,6 @@ 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);
@@ -610,19 +671,39 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
ImGui.PopStyleVar(2); ImGui.PopStyleVar(2);
// Debug flags
bool dbgEnabled = false;
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(); using var drawList = PictoService.Draw();
if (drawList == null) if (drawList == null)
{
ImGui.End();
return; 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) for (int i = 0; i < copyCount; ++i)
{ {
ref var info = ref _buffers.LabelCopy[i]; ref var info = ref _buffers.LabelCopy[i];
// final draw position with viewport offset // final draw position with viewport offset (only when viewports are enabled)
var drawPos = info.ScreenPosition + vpPos; var drawPos = info.ScreenPosition;
if (useViewportOffset)
drawPos += vpPos;
var font = default(ImFontPtr); var font = default(ImFontPtr);
if (info.UseIcon) if (info.UseIcon)
{ {
@@ -648,16 +729,60 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
var scale = baseFontSize > 0 ? (info.FontSize / baseFontSize) : 1f; var scale = baseFontSize > 0 ? (info.FontSize / baseFontSize) : 1f;
var size = baseSize * scale; 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 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); var labelRect = new RectF(topLeft.X, topLeft.Y, topLeft.X + size.X, topLeft.Y + size.Y);
// occlusion check bool wouldOcclude = IsOccludedByAnyUi(labelRect);
if (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; continue;
drawList.AddScreenText(drawPos, info.Text, info.TextColor, info.FontSize, info.Pivot, info.EdgeColor, font); 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();
}
#if DEBUG
DebugLabelCountLastFrame = copyCount;
DebugUiRectCountLastFrame = _uiRects.Count;
DebugOccludedCountLastFrame = occludedThisFrame;
DebugLastNameplateFrame = _lastNamePlateDrawFrame;
#endif
} }
private static Vector2 AlignmentToPivot(AlignmentType alignment) => alignment switch private static Vector2 AlignmentToPivot(AlignmentType alignment) => alignment switch
@@ -705,8 +830,8 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
if (scale <= 0f) if (scale <= 0f)
scale = 1f; scale = 1f;
var computed = (int)System.Math.Round(rawHeight * scale); var computed = (int)Math.Round(rawHeight * scale);
return System.Math.Max(1, computed); return Math.Max(1, computed);
} }
private static unsafe int GetScaledTextWidth(AtkTextNode* node) private static unsafe int GetScaledTextWidth(AtkTextNode* node)
@@ -730,12 +855,6 @@ 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,
@@ -775,9 +894,6 @@ 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
@@ -786,15 +902,9 @@ 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
@@ -821,73 +931,193 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
return cur; return cur;
} }
/// <summary> [MethodImpl(MethodImplOptions.AggressiveInlining)]
/// Tries to get a valid screen rect for the given addon. private static bool IsFinite(float f) => !(float.IsNaN(f) || float.IsInfinity(f));
/// </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;
// Visibility check // Addon must be visible
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;
// Size check // Must have multiple nodes to be useful
float w = root->Width; var nodeCount = addon->UldManager.NodeListCount;
float h = root->Height; var nodeList = addon->UldManager.NodeList;
if (w <= 0 || h <= 0) if (nodeCount <= 1 || nodeList == null)
return false; return false;
// Local scale float rsx = GetWorldScaleX(root);
float sx = root->ScaleX; if (sx <= 0f) sx = 1f; float rsy = GetWorldScaleY(root);
float sy = root->ScaleY; if (sy <= 0f) sy = 1f; if (!IsFinite(rsx) || rsx <= 0f) rsx = 1f;
if (!IsFinite(rsy) || rsy <= 0f) rsy = 1f;
// World/composed scale from Transform // clamp insane root scales (rare but prevents explosions)
float wsx = GetWorldScaleX(root); rsx = MathF.Min(rsx, 6f);
float wsy = GetWorldScaleY(root); rsy = MathF.Min(rsy, 6f);
if (wsx <= 0f) wsx = 1f;
if (wsy <= 0f) wsy = 1f;
// World scale may include parent scaling; use it if meaningfully different. float rw = root->Width * rsx;
float useX = MathF.Abs(wsx - sx) > 0.01f ? wsx : sx; float rh = root->Height * rsy;
float useY = MathF.Abs(wsy - sy) > 0.01f ? wsy : sy; if (!IsFinite(rw) || !IsFinite(rh) || rw <= 2f || rh <= 2f)
w *= useX;
h *= useY;
if (w < 4f || h < 4f)
return false; return false;
// Screen coords float rl = root->ScreenX;
float l = root->ScreenX; float rt = root->ScreenY;
float t = root->ScreenY; if (!IsFinite(rl) || !IsFinite(rt))
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;
// Drop offscreen rects float rr = rl + rw;
if (l < -screen.X || t < -screen.Y || r > screen.X * 2f || b > screen.Y * 2f) float rb = rt + rh;
// 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();
@@ -911,13 +1141,15 @@ 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++)
@@ -931,8 +1163,6 @@ 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;
@@ -942,8 +1172,6 @@ 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;
@@ -953,8 +1181,6 @@ 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))
@@ -982,7 +1208,6 @@ 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)
@@ -992,20 +1217,6 @@ 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(
@@ -1043,6 +1254,15 @@ 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;
@@ -1066,7 +1286,6 @@ 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);
@@ -1096,7 +1315,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
public NameplateBuffers() public NameplateBuffers()
{ {
TextOffsets = new int[AddonNamePlate.NumNamePlateObjects]; TextOffsets = new int[AddonNamePlate.NumNamePlateObjects];
System.Array.Fill(TextOffsets, int.MinValue); Array.Fill(TextOffsets, int.MinValue);
} }
public int[] TextWidths { get; } = new int[AddonNamePlate.NumNamePlateObjects]; public int[] TextWidths { get; } = new int[AddonNamePlate.NumNamePlateObjects];
@@ -1108,23 +1327,20 @@ 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()
{ {
System.Array.Clear(TextWidths, 0, TextWidths.Length); Array.Clear(TextWidths, 0, TextWidths.Length);
System.Array.Clear(TextHeights, 0, TextHeights.Length); Array.Clear(TextHeights, 0, TextHeights.Length);
System.Array.Clear(ContainerHeights, 0, ContainerHeights.Length); Array.Clear(ContainerHeights, 0, ContainerHeights.Length);
System.Array.Fill(TextOffsets, int.MinValue); 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();
@@ -1134,8 +1350,6 @@ 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();

View File

@@ -105,6 +105,7 @@ public class UiFactory
groupData: groupData, groupData: groupData,
isLightfinderContext: isLightfinderContext, isLightfinderContext: isLightfinderContext,
lightfinderCid: lightfinderCid, lightfinderCid: lightfinderCid,
performanceCollector: _performanceCollectorService); performanceCollector: _performanceCollectorService,
_apiController);
} }
} }

View File

@@ -968,20 +968,25 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>> source) Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>> source)
{ {
var clone = new Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>>(source.Count); var clone = new Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>>(source.Count);
foreach (var (objectKind, entries) in source) foreach (var (objectKind, entries) in source)
{ {
var entryClone = new Dictionary<string, CharacterAnalyzer.FileDataEntry>(entries.Count, entries.Comparer); var entryClone = new Dictionary<string, CharacterAnalyzer.FileDataEntry>(entries.Count, entries.Comparer);
foreach (var (hash, entry) in entries) foreach (var (hash, entry) in entries)
{ {
entryClone[hash] = new CharacterAnalyzer.FileDataEntry( entryClone[hash] = new CharacterAnalyzer.FileDataEntry(
hash, hash: hash,
entry.FileType, fileType: entry.FileType,
entry.GamePaths.ToList(), gamePaths: entry.GamePaths?.ToList() ?? [],
entry.FilePaths.ToList(), filePaths: entry.FilePaths?.ToList() ?? [],
entry.OriginalSize, originalSize: entry.OriginalSize,
entry.CompressedSize, compressedSize: entry.CompressedSize,
entry.Triangles); triangles: entry.Triangles,
cacheEntries: entry.CacheEntries
);
} }
clone[objectKind] = entryClone; clone[objectKind] = entryClone;
} }

View File

@@ -364,15 +364,22 @@ public sealed class DtrEntry : IDisposable, IHostedService
return; return;
} }
_ = Task.Run(async () =>
{
try try
{ {
var cid = _dalamudUtilService.GetCID(); var cid = _dalamudUtilService.GetCID();
var hashedCid = cid.ToString().GetHash256(); var hashedCid = cid.ToString().GetHash256();
lock (_localHashedCidLock)
{
_localHashedCid = hashedCid; _localHashedCid = hashedCid;
_localHashedCidFetchedAt = now; _localHashedCidFetchedAt = DateTime.UtcNow;
return hashedCid; }
} }
catch (Exception ex) catch (Exception ex)
{
var now = DateTime.UtcNow;
lock (_localHashedCidLock)
{ {
if (now >= _localHashedCidNextErrorLog) if (now >= _localHashedCidNextErrorLog)
{ {

View File

@@ -37,6 +37,7 @@ public class LightFinderUI : WindowMediatorSubscriberBase
private readonly LightFinderService _broadcastService; private readonly LightFinderService _broadcastService;
private readonly LightlessConfigService _configService; private readonly LightlessConfigService _configService;
private readonly LightlessProfileManager _lightlessProfileManager; private readonly LightlessProfileManager _lightlessProfileManager;
private readonly LightFinderPlateHandler _lightFinderPlateHandler;
private readonly PairUiService _pairUiService; private readonly PairUiService _pairUiService;
private readonly UiSharedService _uiSharedService; private readonly UiSharedService _uiSharedService;
@@ -100,7 +101,8 @@ public class LightFinderUI : WindowMediatorSubscriberBase
DalamudUtilService dalamudUtilService, DalamudUtilService dalamudUtilService,
LightlessProfileManager lightlessProfileManager, LightlessProfileManager lightlessProfileManager,
ActorObjectService actorObjectService ActorObjectService actorObjectService
) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService) ,
LightFinderPlateHandler lightFinderPlateHandler) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService)
{ {
_broadcastService = broadcastService; _broadcastService = broadcastService;
_uiSharedService = uiShared; _uiSharedService = uiShared;
@@ -126,6 +128,7 @@ public class LightFinderUI : WindowMediatorSubscriberBase
Mediator.Subscribe<BroadcastStatusChangedMessage>(this, async _ => await RefreshNearbySyncshellsAsync().ConfigureAwait(false)); Mediator.Subscribe<BroadcastStatusChangedMessage>(this, async _ => await RefreshNearbySyncshellsAsync().ConfigureAwait(false));
Mediator.Subscribe<UserLeftSyncshell>(this, async _ => await RefreshNearbySyncshellsAsync(_.gid).ConfigureAwait(false)); Mediator.Subscribe<UserLeftSyncshell>(this, async _ => await RefreshNearbySyncshellsAsync(_.gid).ConfigureAwait(false));
Mediator.Subscribe<UserJoinedSyncshell>(this, async _ => await RefreshNearbySyncshellsAsync(_.gid).ConfigureAwait(false)); Mediator.Subscribe<UserJoinedSyncshell>(this, async _ => await RefreshNearbySyncshellsAsync(_.gid).ConfigureAwait(false));
_lightFinderPlateHandler = lightFinderPlateHandler;
} }
#endregion #endregion
@@ -1379,17 +1382,53 @@ public class LightFinderUI : WindowMediatorSubscriberBase
#endregion #endregion
#if DEBUG
#region Debug Tab
private void DrawDebugTab() private void DrawDebugTab()
{ {
#if 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, ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY, new Vector2(-1, 200f))) if (ImGui.BeginTable("##BroadcastCacheTable", 4,
ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY,
new Vector2(-1, 225f)))
{ {
ImGui.TableSetupColumn("CID", ImGuiTableColumnFlags.WidthStretch); ImGui.TableSetupColumn("CID", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Broadcasting", ImGuiTableColumnFlags.WidthStretch); ImGui.TableSetupColumn("IsBroadcasting", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Expires In", ImGuiTableColumnFlags.WidthStretch); ImGui.TableSetupColumn("Expires In", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Syncshell GID", ImGuiTableColumnFlags.WidthStretch); ImGui.TableSetupColumn("Syncshell GID", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableHeadersRow(); ImGui.TableHeadersRow();
@@ -1427,10 +1466,8 @@ public class LightFinderUI : WindowMediatorSubscriberBase
ImGui.EndTable(); ImGui.EndTable();
} }
}
#endregion
#endif #endif
}
#region Data Refresh #region Data Refresh

View File

@@ -86,6 +86,8 @@ 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();
@@ -2387,7 +2389,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
var labelRenderer = _configService.Current.LightfinderLabelRenderer; var labelRenderer = _configService.Current.LightfinderLabelRenderer;
var labelRendererLabel = labelRenderer switch var labelRendererLabel = labelRenderer switch
{ {
LightfinderLabelRenderer.SignatureHook => "Native nameplate (sig hook)", LightfinderLabelRenderer.SignatureHook => "Native Nameplate Rendering",
_ => "ImGui Overlay", _ => "ImGui Overlay",
}; };
@@ -2397,18 +2399,25 @@ public class SettingsUi : WindowMediatorSubscriberBase
{ {
var optionLabel = option switch var optionLabel = option switch
{ {
LightfinderLabelRenderer.SignatureHook => "Native Nameplate (sig hook)", LightfinderLabelRenderer.SignatureHook => "Native Nameplate Rendering",
_ => "ImGui Overlay", _ => "ImGui Overlay",
}; };
var selected = option == labelRenderer; var selected = option == labelRenderer;
if (ImGui.Selectable(optionLabel, selected)) if (ImGui.Selectable(optionLabel, selected))
{
if (option == LightfinderLabelRenderer.SignatureHook)
{
_pendingLightfinderRenderer = option;
_showLightfinderRendererWarning = true;
}
else
{ {
_configService.Current.LightfinderLabelRenderer = option; _configService.Current.LightfinderLabelRenderer = option;
_configService.Save(); _configService.Save();
_nameplateService.RequestRedraw(); _nameplateService.RequestRedraw();
} }
}
if (selected) if (selected)
ImGui.SetItemDefaultFocus(); ImGui.SetItemDefaultFocus();
} }
@@ -2416,6 +2425,34 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.EndCombo(); 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."); _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); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
@@ -2602,7 +2639,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.ToIconEditorString(optionGlyph); _lightfinderIconInput = LightFinderPlateHandler.NormalizeIconGlyph(optionGlyph);
_lightfinderIconPresetIndex = i; _lightfinderIconPresetIndex = i;
} }
} }
@@ -4083,7 +4120,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.ToIconEditorString(normalized); _lightfinderIconInput = LightFinderPlateHandler.NormalizeIconGlyph(normalized);
_lightfinderIconInputInitialized = true; _lightfinderIconInputInitialized = true;
_lightfinderIconPresetIndex = -1; _lightfinderIconPresetIndex = -1;
@@ -4101,7 +4138,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
{ {
_configService.Current.LightfinderLabelIconGlyph = normalizedGlyph; _configService.Current.LightfinderLabelIconGlyph = normalizedGlyph;
_configService.Save(); _configService.Save();
_lightfinderIconInput = LightFinderPlateHandler.ToIconEditorString(normalizedGlyph); _lightfinderIconInput = LightFinderPlateHandler.NormalizeIconGlyph(normalizedGlyph);
_lightfinderIconPresetIndex = presetIndex; _lightfinderIconPresetIndex = presetIndex;
_lightfinderIconInputInitialized = true; _lightfinderIconInputInitialized = true;
} }

View File

@@ -11,6 +11,7 @@ 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;
@@ -22,6 +23,7 @@ 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;
@@ -60,7 +62,8 @@ 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,
@@ -94,6 +97,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
.Apply(); .Apply();
IsOpen = true; IsOpen = true;
_apiController = apiController;
} }
public Pair? Pair { get; } public Pair? Pair { get; }
@@ -248,19 +252,33 @@ 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;
@@ -282,11 +300,15 @@ 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>

View File

@@ -993,19 +993,34 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
{ {
_refocusChatInput = true; _refocusChatInput = true;
_refocusChatInputKey = channel.Key; _refocusChatInputKey = channel.Key;
var sanitized = SanitizeOutgoingDraft(draft);
var draftAtSend = draft;
var sanitized = SanitizeOutgoingDraft(draftAtSend);
if (sanitized is not null) if (sanitized is not null)
{ {
TrackPendingDraftClear(channel.Key, sanitized); TrackPendingDraftClear(channel.Key, sanitized);
if (TrySendDraft(channel, sanitized)) draft = string.Empty;
{ _draftMessages[channel.Key] = draft;
_scrollToBottom = true; _scrollToBottom = true;
}
else _ = Task.Run(async () =>
{
try
{
var succeeded = await _zoneChatService.SendMessageAsync(channel.Descriptor, sanitized).ConfigureAwait(false);
if (!succeeded)
{ {
RemovePendingDraftClear(channel.Key, sanitized); RemovePendingDraftClear(channel.Key, sanitized);
} }
} }
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to send chat message");
RemovePendingDraftClear(channel.Key, sanitized);
}
});
}
} }
} }