Merge branch '1.12.4' into notification-reworks
This commit is contained in:
@@ -28,7 +28,7 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos
|
||||
private readonly CancellationTokenSource _cleanupCts = new();
|
||||
private Task? _cleanupTask;
|
||||
|
||||
private int _checkEveryFrames = 20;
|
||||
private readonly int _checkEveryFrames = 20;
|
||||
private int _frameCounter = 0;
|
||||
private int _lookupsThisFrame = 0;
|
||||
private const int MaxLookupsPerFrame = 30;
|
||||
@@ -221,6 +221,16 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos
|
||||
(excludeHashedCid is null || !comparer.Equals(entry.Key, excludeHashedCid)));
|
||||
}
|
||||
|
||||
public List<KeyValuePair<string, BroadcastEntry>> GetActiveBroadcasts(string? excludeHashedCid = null)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var comparer = StringComparer.Ordinal;
|
||||
return [.. _broadcastCache.Where(entry =>
|
||||
entry.Value.IsBroadcasting &&
|
||||
entry.Value.ExpiryTime > now &&
|
||||
(excludeHashedCid is null || !comparer.Equals(entry.Key, excludeHashedCid)))];
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
@@ -144,11 +144,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
||||
IsLightFinderAvailable = false;
|
||||
ApplyBroadcastDisabled(forcePublish: true);
|
||||
_logger.LogDebug("Cleared Lightfinder state due to disconnect.");
|
||||
|
||||
_mediator.Publish(new NotificationMessage(
|
||||
"Disconnected from Server",
|
||||
"Your Lightfinder broadcast has been disabled due to disconnection.",
|
||||
NotificationType.Warning));
|
||||
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
|
||||
@@ -13,7 +13,8 @@ namespace LightlessSync.Services;
|
||||
|
||||
public sealed class CommandManagerService : IDisposable
|
||||
{
|
||||
private const string _commandName = "/light";
|
||||
private const string _longName = "/lightless";
|
||||
private const string _shortName = "/light";
|
||||
|
||||
private readonly ApiController _apiController;
|
||||
private readonly ICommandManager _commandManager;
|
||||
@@ -34,7 +35,11 @@ public sealed class CommandManagerService : IDisposable
|
||||
_apiController = apiController;
|
||||
_mediator = mediator;
|
||||
_lightlessConfigService = lightlessConfigService;
|
||||
_commandManager.AddHandler(_commandName, new CommandInfo(OnCommand)
|
||||
_commandManager.AddHandler(_longName, new CommandInfo(OnCommand)
|
||||
{
|
||||
HelpMessage = $"\u2191;"
|
||||
});
|
||||
_commandManager.AddHandler(_shortName, new CommandInfo(OnCommand)
|
||||
{
|
||||
HelpMessage = "Opens the Lightless Sync UI" + Environment.NewLine + Environment.NewLine +
|
||||
"Additionally possible commands:" + Environment.NewLine +
|
||||
@@ -49,7 +54,8 @@ public sealed class CommandManagerService : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_commandManager.RemoveHandler(_commandName);
|
||||
_commandManager.RemoveHandler(_longName);
|
||||
_commandManager.RemoveHandler(_shortName);
|
||||
}
|
||||
|
||||
private void OnCommand(string command, string args)
|
||||
|
||||
245
LightlessSync/Services/Compression/BatchFileFragService.cs
Normal file
245
LightlessSync/Services/Compression/BatchFileFragService.cs
Normal file
@@ -0,0 +1,245 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Diagnostics;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace LightlessSync.Services.Compression
|
||||
{
|
||||
/// <summary>
|
||||
/// This batch service is made for the File Frag command, because of each file needing to use this command.
|
||||
/// It's better to combine into one big command in batches then doing each command on each compressed call.
|
||||
/// </summary>
|
||||
public sealed partial class BatchFilefragService : IDisposable
|
||||
{
|
||||
private readonly Channel<(string path, TaskCompletionSource<bool> tcs)> _ch;
|
||||
private readonly Task _worker;
|
||||
private readonly bool _useShell;
|
||||
private readonly ILogger _log;
|
||||
private readonly int _batchSize;
|
||||
private readonly TimeSpan _flushDelay;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
|
||||
public BatchFilefragService(bool useShell, ILogger log, int batchSize = 128, int flushMs = 25)
|
||||
{
|
||||
_useShell = useShell;
|
||||
_log = log;
|
||||
_batchSize = Math.Max(8, batchSize);
|
||||
_flushDelay = TimeSpan.FromMilliseconds(Math.Max(5, flushMs));
|
||||
_ch = Channel.CreateUnbounded<(string, TaskCompletionSource<bool>)>(new UnboundedChannelOptions { SingleReader = true, SingleWriter = false });
|
||||
_worker = Task.Run(ProcessAsync, _cts.Token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the file is compressed using Btrfs using tasks
|
||||
/// </summary>
|
||||
/// <param name="linuxPath">Linux/Wine path given for the file.</param>
|
||||
/// <param name="ct">Cancellation Token</param>
|
||||
/// <returns>If it was compressed or not</returns>
|
||||
public Task<bool> IsCompressedAsync(string linuxPath, CancellationToken ct = default)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
if (!_ch.Writer.TryWrite((linuxPath, tcs)))
|
||||
{
|
||||
tcs.TrySetResult(false);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
if (ct.CanBeCanceled)
|
||||
{
|
||||
var reg = ct.Register(() => tcs.TrySetCanceled(ct));
|
||||
_ = tcs.Task.ContinueWith(_ => reg.Dispose(), CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
|
||||
}
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the pending compression tasks asynchronously
|
||||
/// </summary>
|
||||
/// <returns>Task</returns>
|
||||
private async Task ProcessAsync()
|
||||
{
|
||||
var reader = _ch.Reader;
|
||||
var pending = new List<(string path, TaskCompletionSource<bool> tcs)>(_batchSize);
|
||||
|
||||
try
|
||||
{
|
||||
while (await reader.WaitToReadAsync(_cts.Token).ConfigureAwait(false))
|
||||
{
|
||||
if (!reader.TryRead(out var first)) continue;
|
||||
pending.Add(first);
|
||||
|
||||
var flushAt = DateTime.UtcNow + _flushDelay;
|
||||
while (pending.Count < _batchSize && DateTime.UtcNow < flushAt)
|
||||
{
|
||||
if (reader.TryRead(out var item))
|
||||
{
|
||||
pending.Add(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((flushAt - DateTime.UtcNow) <= TimeSpan.Zero) break;
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(5), _cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var map = await RunBatchAsync(pending.Select(p => p.path)).ConfigureAwait(false);
|
||||
foreach (var (path, tcs) in pending)
|
||||
{
|
||||
tcs.TrySetResult(map.TryGetValue(path, out var c) && c);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogDebug(ex, "filefrag batch failed. falling back to false");
|
||||
foreach (var (_, tcs) in pending)
|
||||
{
|
||||
tcs.TrySetResult(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
pending.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
//Shutting down worker, exception called
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Running the batch of each file in the queue in one file frag command.
|
||||
/// </summary>
|
||||
/// <param name="paths">Paths that are needed for the command building for the batch return</param>
|
||||
/// <returns>Path of the file and if it went correctly</returns>
|
||||
/// <exception cref="InvalidOperationException">Failing to start filefrag on the system if this exception is found</exception>
|
||||
private async Task<Dictionary<string, bool>> RunBatchAsync(IEnumerable<string> paths)
|
||||
{
|
||||
var list = paths.Distinct(StringComparer.Ordinal).ToList();
|
||||
var result = list.ToDictionary(p => p, _ => false, StringComparer.Ordinal);
|
||||
|
||||
ProcessStartInfo psi;
|
||||
if (_useShell)
|
||||
{
|
||||
var inner = "filefrag -v -- " + string.Join(' ', list.Select(QuoteSingle));
|
||||
psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "/bin/bash",
|
||||
Arguments = "-c " + QuoteDouble(inner),
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WorkingDirectory = "/"
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "filefrag",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
psi.ArgumentList.Add("-v");
|
||||
psi.ArgumentList.Add("--");
|
||||
foreach (var p in list) psi.ArgumentList.Add(p);
|
||||
}
|
||||
|
||||
using var proc = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start filefrag");
|
||||
var stdoutTask = proc.StandardOutput.ReadToEndAsync(_cts.Token);
|
||||
var stderrTask = proc.StandardError.ReadToEndAsync(_cts.Token);
|
||||
await Task.WhenAll(stdoutTask, stderrTask).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await proc.WaitForExitAsync(_cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogWarning(ex, "Error in the batch frag service. proc = {proc}", proc);
|
||||
}
|
||||
|
||||
var stdout = await stdoutTask.ConfigureAwait(false);
|
||||
var stderr = await stderrTask.ConfigureAwait(false);
|
||||
|
||||
if (proc.ExitCode != 0 && !string.IsNullOrWhiteSpace(stderr))
|
||||
_log.LogTrace("filefrag exited {code}: {err}", proc.ExitCode, stderr.Trim());
|
||||
|
||||
ParseFilefrag(stdout, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parsing the string given from the File Frag command into mapping
|
||||
/// </summary>
|
||||
/// <param name="output">Output of the process from the File Frag</param>
|
||||
/// <param name="map">Mapping of the processed files</param>
|
||||
private static void ParseFilefrag(string output, Dictionary<string, bool> map)
|
||||
{
|
||||
var reHeaderColon = ColonRegex();
|
||||
var reHeaderSize = SizeRegex();
|
||||
|
||||
string? current = null;
|
||||
using var sr = new StringReader(output);
|
||||
for (string? line = sr.ReadLine(); line != null; line = sr.ReadLine())
|
||||
{
|
||||
var m1 = reHeaderColon.Match(line);
|
||||
if (m1.Success) { current = m1.Groups[1].Value; continue; }
|
||||
|
||||
var m2 = reHeaderSize.Match(line);
|
||||
if (m2.Success) { current = m2.Groups[1].Value; continue; }
|
||||
|
||||
if (current is not null && line.Contains("flags:", StringComparison.OrdinalIgnoreCase) &&
|
||||
line.Contains("compressed", StringComparison.OrdinalIgnoreCase) && map.ContainsKey(current))
|
||||
{
|
||||
map[current] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string QuoteSingle(string s) => "'" + s.Replace("'", "'\\''", StringComparison.Ordinal) + "'";
|
||||
private static string QuoteDouble(string s) => "\"" + s.Replace("\\", "\\\\", StringComparison.Ordinal).Replace("\"", "\\\"", StringComparison.Ordinal).Replace("$", "\\$", StringComparison.Ordinal).Replace("`", "\\`", StringComparison.Ordinal) + "\"";
|
||||
|
||||
/// <summary>
|
||||
/// Regex of the File Size return on the Linux/Wine systems, giving back the amount
|
||||
/// </summary>
|
||||
/// <returns>Regex of the File Size</returns>
|
||||
[GeneratedRegex(@"^File size of (/.+?) is ", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant,matchTimeoutMilliseconds: 500)]
|
||||
private static partial Regex SizeRegex();
|
||||
|
||||
/// <summary>
|
||||
/// Regex on colons return on the Linux/Wine systems
|
||||
/// </summary>
|
||||
/// <returns>Regex of the colons in the given path</returns>
|
||||
[GeneratedRegex(@"^(/.+?):\s", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 500)]
|
||||
private static partial Regex ColonRegex();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_ch.Writer.TryComplete();
|
||||
_cts.Cancel();
|
||||
try
|
||||
{
|
||||
_worker.Wait(TimeSpan.FromSeconds(2), _cts.Token);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore the catch in dispose
|
||||
}
|
||||
_cts.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ internal class ContextMenuService : IHostedService
|
||||
if (targetData == null || targetData.Address == nint.Zero)
|
||||
return;
|
||||
|
||||
//Check if user is paired or is own.
|
||||
//Check if user is directly paired or is own.
|
||||
if (VisibleUserIds.Any(u => u == target.TargetObjectId) || _clientState.LocalPlayer.GameObjectId == target.TargetObjectId)
|
||||
return;
|
||||
|
||||
@@ -116,7 +116,7 @@ internal class ContextMenuService : IHostedService
|
||||
|
||||
args.AddMenuItem(new MenuItem
|
||||
{
|
||||
Name = "Send Pair Request",
|
||||
Name = "Send Direct Pair Request",
|
||||
PrefixChar = 'L',
|
||||
UseDefaultPrefix = false,
|
||||
PrefixColor = 708,
|
||||
@@ -159,7 +159,7 @@ internal class ContextMenuService : IHostedService
|
||||
}
|
||||
}
|
||||
|
||||
private HashSet<ulong> VisibleUserIds => [.. _pairManager.GetOnlineUserPairs()
|
||||
private HashSet<ulong> VisibleUserIds => [.. _pairManager.DirectPairs
|
||||
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
||||
.Select(u => (ulong)u.PlayerCharacterId)];
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Dalamud.Game.Addon.Lifecycle;
|
||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||
@@ -15,8 +16,9 @@ using LightlessSync.UtilsEnum.Enum;
|
||||
// Created using https://github.com/PunishedPineapple/Distance as a reference, thank you!
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
@@ -32,10 +34,10 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
private readonly LightlessMediator _mediator;
|
||||
public LightlessMediator Mediator => _mediator;
|
||||
|
||||
private bool mEnabled = false;
|
||||
private bool _mEnabled = false;
|
||||
private bool _needsLabelRefresh = false;
|
||||
private AddonNamePlate* mpNameplateAddon = null;
|
||||
private readonly AtkTextNode*[] mTextNodes = new AtkTextNode*[AddonNamePlate.NumNamePlateObjects];
|
||||
private AddonNamePlate* _mpNameplateAddon = null;
|
||||
private readonly AtkTextNode*[] _mTextNodes = new AtkTextNode*[AddonNamePlate.NumNamePlateObjects];
|
||||
private readonly int[] _cachedNameplateTextWidths = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
private readonly int[] _cachedNameplateTextHeights = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
private readonly int[] _cachedNameplateContainerHeights = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
@@ -44,10 +46,10 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
internal const uint mNameplateNodeIDBase = 0x7D99D500;
|
||||
private const string DefaultLabelText = "LightFinder";
|
||||
private const SeIconChar DefaultIcon = SeIconChar.Hyadelyn;
|
||||
private const int ContainerOffsetX = 50;
|
||||
private const int _containerOffsetX = 50;
|
||||
private static readonly string DefaultIconGlyph = SeIconCharExtensions.ToIconString(DefaultIcon);
|
||||
|
||||
private volatile HashSet<string> _activeBroadcastingCids = [];
|
||||
private ImmutableHashSet<string> _activeBroadcastingCids = [];
|
||||
|
||||
public NameplateHandler(ILogger<NameplateHandler> logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, DalamudUtilService dalamudUtil, LightlessConfigService configService, LightlessMediator mediator, IClientState clientState, PairManager pairManager)
|
||||
{
|
||||
@@ -74,17 +76,17 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
DisableNameplate();
|
||||
DestroyNameplateNodes();
|
||||
_mediator.Unsubscribe<PriorityFrameworkUpdateMessage>(this);
|
||||
mpNameplateAddon = null;
|
||||
_mpNameplateAddon = null;
|
||||
}
|
||||
|
||||
internal void EnableNameplate()
|
||||
{
|
||||
if (!mEnabled)
|
||||
if (!_mEnabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
_addonLifecycle.RegisterListener(AddonEvent.PostDraw, "NamePlate", NameplateDrawDetour);
|
||||
mEnabled = true;
|
||||
_mEnabled = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -96,7 +98,7 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
|
||||
internal void DisableNameplate()
|
||||
{
|
||||
if (mEnabled)
|
||||
if (_mEnabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -107,24 +109,30 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
_logger.LogError($"Unknown error while unregistering nameplate listener:\n{e}");
|
||||
}
|
||||
|
||||
mEnabled = false;
|
||||
_mEnabled = false;
|
||||
HideAllNameplateNodes();
|
||||
}
|
||||
}
|
||||
|
||||
private void NameplateDrawDetour(AddonEvent type, AddonArgs args)
|
||||
{
|
||||
if (args.Addon.Address == nint.Zero)
|
||||
{
|
||||
_logger.LogWarning("Nameplate draw detour received a null addon address, skipping update.");
|
||||
return;
|
||||
}
|
||||
|
||||
var pNameplateAddon = (AddonNamePlate*)args.Addon.Address;
|
||||
|
||||
if (mpNameplateAddon != pNameplateAddon)
|
||||
if (_mpNameplateAddon != pNameplateAddon)
|
||||
{
|
||||
for (int i = 0; i < mTextNodes.Length; ++i) mTextNodes[i] = null;
|
||||
for (int i = 0; i < _mTextNodes.Length; ++i) _mTextNodes[i] = null;
|
||||
System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length);
|
||||
System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length);
|
||||
System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length);
|
||||
System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
||||
mpNameplateAddon = pNameplateAddon;
|
||||
if (mpNameplateAddon != null) CreateNameplateNodes();
|
||||
_mpNameplateAddon = pNameplateAddon;
|
||||
if (_mpNameplateAddon != null) CreateNameplateNodes();
|
||||
}
|
||||
|
||||
UpdateNameplateNodes();
|
||||
@@ -138,7 +146,16 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
if (nameplateObject == null)
|
||||
continue;
|
||||
|
||||
var rootNode = nameplateObject.Value.RootComponentNode;
|
||||
if (rootNode == null || rootNode->Component == null)
|
||||
continue;
|
||||
|
||||
var pNameplateResNode = nameplateObject.Value.NameContainer;
|
||||
if (pNameplateResNode == null)
|
||||
continue;
|
||||
if (pNameplateResNode->ChildNode == null)
|
||||
continue;
|
||||
|
||||
var pNewNode = AtkNodeHelpers.CreateOrphanTextNode(mNameplateNodeIDBase + (uint)i, TextFlags.Edge | TextFlags.Glare);
|
||||
|
||||
if (pNewNode != null)
|
||||
@@ -148,24 +165,43 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
pNewNode->AtkResNode.NextSiblingNode = pLastChild;
|
||||
pNewNode->AtkResNode.ParentNode = pNameplateResNode;
|
||||
pLastChild->PrevSiblingNode = (AtkResNode*)pNewNode;
|
||||
nameplateObject.Value.RootComponentNode->Component->UldManager.UpdateDrawNodeList();
|
||||
rootNode->Component->UldManager.UpdateDrawNodeList();
|
||||
pNewNode->AtkResNode.SetUseDepthBasedPriority(true);
|
||||
mTextNodes[i] = pNewNode;
|
||||
_mTextNodes[i] = pNewNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DestroyNameplateNodes()
|
||||
{
|
||||
var pCurrentNameplateAddon = (AddonNamePlate*)_gameGui.GetAddonByName("NamePlate", 1).Address;
|
||||
if (mpNameplateAddon == null || mpNameplateAddon != pCurrentNameplateAddon)
|
||||
var currentHandle = _gameGui.GetAddonByName("NamePlate", 1);
|
||||
if (currentHandle.Address == nint.Zero)
|
||||
{
|
||||
_logger.LogWarning("Unable to destroy nameplate nodes because the NamePlate addon is not available.");
|
||||
return;
|
||||
}
|
||||
|
||||
var pCurrentNameplateAddon = (AddonNamePlate*)currentHandle.Address;
|
||||
if (_mpNameplateAddon == null)
|
||||
return;
|
||||
|
||||
if (_mpNameplateAddon != pCurrentNameplateAddon)
|
||||
{
|
||||
_logger.LogWarning("Skipping nameplate node destroy due to addon address mismatch (cached {Cached:X}, current {Current:X}).", (IntPtr)_mpNameplateAddon, (IntPtr)pCurrentNameplateAddon);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i)
|
||||
{
|
||||
var pTextNode = mTextNodes[i];
|
||||
var pTextNode = _mTextNodes[i];
|
||||
var pNameplateNode = GetNameplateComponentNode(i);
|
||||
if (pTextNode != null && pNameplateNode != null)
|
||||
if (pTextNode != null && (pNameplateNode == null || pNameplateNode->Component == null))
|
||||
{
|
||||
_logger.LogDebug("Skipping destroy for nameplate {Index} because its component node is unavailable.", i);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pTextNode != null && pNameplateNode != null && pNameplateNode->Component != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -175,7 +211,7 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
pTextNode->AtkResNode.NextSiblingNode->PrevSiblingNode = pTextNode->AtkResNode.PrevSiblingNode;
|
||||
pNameplateNode->Component->UldManager.UpdateDrawNodeList();
|
||||
pTextNode->AtkResNode.Destroy(true);
|
||||
mTextNodes[i] = null;
|
||||
_mTextNodes[i] = null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -192,7 +228,7 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
|
||||
private void HideAllNameplateNodes()
|
||||
{
|
||||
for (int i = 0; i < mTextNodes.Length; ++i)
|
||||
for (int i = 0; i < _mTextNodes.Length; ++i)
|
||||
{
|
||||
HideNameplateTextNode(i);
|
||||
}
|
||||
@@ -200,22 +236,62 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
|
||||
private void UpdateNameplateNodes()
|
||||
{
|
||||
var framework = Framework.Instance();
|
||||
var ui3DModule = framework->GetUIModule()->GetUI3DModule();
|
||||
var currentHandle = _gameGui.GetAddonByName("NamePlate");
|
||||
if (currentHandle.Address == nint.Zero)
|
||||
{
|
||||
_logger.LogDebug("NamePlate addon unavailable during update, skipping label refresh.");
|
||||
return;
|
||||
}
|
||||
|
||||
var currentAddon = (AddonNamePlate*)currentHandle.Address;
|
||||
if (_mpNameplateAddon == null || currentAddon == null || currentAddon != _mpNameplateAddon)
|
||||
{
|
||||
if (_mpNameplateAddon != null)
|
||||
_logger.LogDebug("Cached NamePlate addon pointer differs from current: waiting for new hook (cached {Cached:X}, current {Current:X}).", (IntPtr)_mpNameplateAddon, (IntPtr)currentAddon);
|
||||
return;
|
||||
}
|
||||
|
||||
var framework = Framework.Instance();
|
||||
if (framework == null)
|
||||
{
|
||||
_logger.LogDebug("Framework instance unavailable during nameplate update, skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var uiModule = framework->GetUIModule();
|
||||
if (uiModule == null)
|
||||
{
|
||||
_logger.LogDebug("UI module unavailable during nameplate update, skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var ui3DModule = uiModule->GetUI3DModule();
|
||||
if (ui3DModule == null)
|
||||
{
|
||||
_logger.LogDebug("UI3D module unavailable during nameplate update, skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var vec = ui3DModule->NamePlateObjectInfoPointers;
|
||||
if (vec.IsEmpty)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < ui3DModule->NamePlateObjectInfoCount; ++i)
|
||||
var visibleUserIdsSnapshot = VisibleUserIds;
|
||||
|
||||
var safeCount = System.Math.Min(
|
||||
ui3DModule->NamePlateObjectInfoCount,
|
||||
vec.Length
|
||||
);
|
||||
|
||||
for (int i = 0; i < safeCount; ++i)
|
||||
{
|
||||
if (ui3DModule->NamePlateObjectInfoPointers.IsEmpty) continue;
|
||||
var config = _configService.Current;
|
||||
|
||||
var objectInfoPtr = ui3DModule->NamePlateObjectInfoPointers[i];
|
||||
|
||||
if (objectInfoPtr == null) continue;
|
||||
var objectInfoPtr = vec[i];
|
||||
if (objectInfoPtr == null)
|
||||
continue;
|
||||
|
||||
var objectInfo = objectInfoPtr.Value;
|
||||
|
||||
if (objectInfo == null || objectInfo->GameObject == null)
|
||||
continue;
|
||||
|
||||
@@ -223,62 +299,68 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
if (nameplateIndex < 0 || nameplateIndex >= AddonNamePlate.NumNamePlateObjects)
|
||||
continue;
|
||||
|
||||
var pNode = mTextNodes[nameplateIndex];
|
||||
var pNode = _mTextNodes[nameplateIndex];
|
||||
if (pNode == null)
|
||||
continue;
|
||||
|
||||
if (mpNameplateAddon == null)
|
||||
var gameObject = objectInfo->GameObject;
|
||||
if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player)
|
||||
{
|
||||
pNode->AtkResNode.ToggleVisibility(enable: false);
|
||||
continue;
|
||||
|
||||
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)objectInfo->GameObject);
|
||||
}
|
||||
|
||||
// CID gating
|
||||
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject);
|
||||
if (cid == null || !_activeBroadcastingCids.Contains(cid))
|
||||
{
|
||||
pNode->AtkResNode.ToggleVisibility(false);
|
||||
pNode->AtkResNode.ToggleVisibility(enable: false);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_configService.Current.LightfinderLabelShowOwn && (objectInfo->GameObject->GetGameObjectId() == _clientState.LocalPlayer.GameObjectId))
|
||||
var local = _clientState.LocalPlayer;
|
||||
if (!config.LightfinderLabelShowOwn && local != null &&
|
||||
objectInfo->GameObject->GetGameObjectId() == local.GameObjectId)
|
||||
{
|
||||
pNode->AtkResNode.ToggleVisibility(false);
|
||||
pNode->AtkResNode.ToggleVisibility(enable: false);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_configService.Current.LightfinderLabelShowPaired && VisibleUserIds.Any(u => u == objectInfo->GameObject->GetGameObjectId()))
|
||||
var hidePaired = !config.LightfinderLabelShowPaired;
|
||||
|
||||
var goId = (ulong)gameObject->GetGameObjectId();
|
||||
if (hidePaired && visibleUserIdsSnapshot.Contains(goId))
|
||||
{
|
||||
pNode->AtkResNode.ToggleVisibility(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
var nameplateObject = mpNameplateAddon->NamePlateObjectArray[nameplateIndex];
|
||||
nameplateObject.RootComponentNode->Component->UldManager.UpdateDrawNodeList();
|
||||
|
||||
var pNameplateIconNode = nameplateObject.MarkerIcon;
|
||||
var pNameplateResNode = nameplateObject.NameContainer;
|
||||
var pNameplateTextNode = nameplateObject.NameText;
|
||||
bool IsVisible = pNameplateIconNode->AtkResNode.IsVisible() || (pNameplateResNode->IsVisible() && pNameplateTextNode->AtkResNode.IsVisible()) || _configService.Current.LightfinderLabelShowHidden;
|
||||
pNode->AtkResNode.ToggleVisibility(IsVisible);
|
||||
|
||||
if (nameplateObject.RootComponentNode == null ||
|
||||
nameplateObject.NameContainer == null ||
|
||||
nameplateObject.NameText == null)
|
||||
{
|
||||
pNode->AtkResNode.ToggleVisibility(false);
|
||||
pNode->AtkResNode.ToggleVisibility(enable: false);
|
||||
continue;
|
||||
}
|
||||
|
||||
var nameplateObject = _mpNameplateAddon->NamePlateObjectArray[nameplateIndex];
|
||||
var root = nameplateObject.RootComponentNode;
|
||||
var nameContainer = nameplateObject.NameContainer;
|
||||
var nameText = nameplateObject.NameText;
|
||||
var marker = nameplateObject.MarkerIcon;
|
||||
|
||||
if (nameContainer == null || nameText == null)
|
||||
if (root == null || root->Component == null || nameContainer == null || nameText == null)
|
||||
{
|
||||
pNode->AtkResNode.ToggleVisibility(false);
|
||||
_logger.LogDebug("Nameplate {Index} missing required nodes during update, skipping.", nameplateIndex);
|
||||
pNode->AtkResNode.ToggleVisibility(enable: false);
|
||||
continue;
|
||||
}
|
||||
|
||||
root->Component->UldManager.UpdateDrawNodeList();
|
||||
|
||||
bool isVisible =
|
||||
((marker != null) && marker->AtkResNode.IsVisible()) ||
|
||||
(nameContainer->IsVisible() && nameText->AtkResNode.IsVisible()) ||
|
||||
config.LightfinderLabelShowHidden;
|
||||
|
||||
pNode->AtkResNode.ToggleVisibility(isVisible);
|
||||
if (!isVisible)
|
||||
continue;
|
||||
|
||||
var labelColor = UIColors.Get("Lightfinder");
|
||||
var edgeColor = UIColors.Get("LightfinderEdge");
|
||||
var config = _configService.Current;
|
||||
|
||||
var scaleMultiplier = System.Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f);
|
||||
var baseScale = config.LightfinderLabelUseIcon ? 1.0f : 0.5f;
|
||||
@@ -437,7 +519,7 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
positionY += config.LightfinderLabelOffsetY;
|
||||
|
||||
alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8);
|
||||
pNode->AtkResNode.SetUseDepthBasedPriority(true);
|
||||
pNode->AtkResNode.SetUseDepthBasedPriority(enable: true);
|
||||
|
||||
pNode->AtkResNode.Color.A = 255;
|
||||
|
||||
@@ -545,7 +627,7 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
}
|
||||
private void HideNameplateTextNode(int i)
|
||||
{
|
||||
var pNode = mTextNodes[i];
|
||||
var pNode = _mTextNodes[i];
|
||||
if (pNode != null)
|
||||
{
|
||||
pNode->AtkResNode.ToggleVisibility(false);
|
||||
@@ -555,10 +637,10 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
private AddonNamePlate.NamePlateObject? GetNameplateObject(int i)
|
||||
{
|
||||
if (i < AddonNamePlate.NumNamePlateObjects &&
|
||||
mpNameplateAddon != null &&
|
||||
mpNameplateAddon->NamePlateObjectArray[i].RootComponentNode != null)
|
||||
_mpNameplateAddon != null &&
|
||||
_mpNameplateAddon->NamePlateObjectArray[i].RootComponentNode != null)
|
||||
{
|
||||
return mpNameplateAddon->NamePlateObjectArray[i];
|
||||
return _mpNameplateAddon->NamePlateObjectArray[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -571,10 +653,12 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
var nameplateObject = GetNameplateObject(i);
|
||||
return nameplateObject != null ? nameplateObject.Value.RootComponentNode : null;
|
||||
}
|
||||
|
||||
private HashSet<ulong> VisibleUserIds => [.. _pairManager.GetOnlineUserPairs()
|
||||
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
||||
.Select(u => (ulong)u.PlayerCharacterId)];
|
||||
|
||||
|
||||
public void FlagRefresh()
|
||||
{
|
||||
_needsLabelRefresh = true;
|
||||
@@ -591,18 +675,12 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
||||
|
||||
public void UpdateBroadcastingCids(IEnumerable<string> cids)
|
||||
{
|
||||
var newSet = cids.ToHashSet();
|
||||
|
||||
var changed = !_activeBroadcastingCids.SetEquals(newSet);
|
||||
if (!changed)
|
||||
var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal);
|
||||
if (ReferenceEquals(_activeBroadcastingCids, newSet) || _activeBroadcastingCids.SetEquals(newSet))
|
||||
return;
|
||||
|
||||
_activeBroadcastingCids.Clear();
|
||||
foreach (var cid in newSet)
|
||||
_activeBroadcastingCids.Add(cid);
|
||||
|
||||
_logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(",", _activeBroadcastingCids));
|
||||
|
||||
_activeBroadcastingCids = newSet;
|
||||
_logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(',', _activeBroadcastingCids));
|
||||
FlagRefresh();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services.Mediator;
|
||||
@@ -14,10 +10,10 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly Lazy<WebAPI.ApiController> _apiController;
|
||||
private readonly object _syncRoot = new();
|
||||
private readonly Lock _syncRoot = new();
|
||||
private readonly List<PairRequestEntry> _requests = [];
|
||||
|
||||
private static readonly TimeSpan Expiration = TimeSpan.FromMinutes(5);
|
||||
private static readonly TimeSpan _expiration = TimeSpan.FromMinutes(5);
|
||||
|
||||
public PairRequestService(
|
||||
ILogger<PairRequestService> logger,
|
||||
@@ -189,7 +185,7 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
return _requests.RemoveAll(r => now - r.ReceivedAt > Expiration) > 0;
|
||||
return _requests.RemoveAll(r => now - r.ReceivedAt > _expiration) > 0;
|
||||
}
|
||||
|
||||
public void AcceptPairRequest(string hashedCid, string displayName)
|
||||
|
||||
Reference in New Issue
Block a user