2.0.0 #92
@@ -294,7 +294,10 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
sp.GetRequiredService<LightlessMediator>()));
|
sp.GetRequiredService<LightlessMediator>()));
|
||||||
collection.AddSingleton<HubFactory>();
|
collection.AddSingleton<HubFactory>();
|
||||||
collection.AddSingleton(s => new LightFinderScannerService(s.GetRequiredService<ILogger<LightFinderScannerService>>(), framework, s.GetRequiredService<LightFinderService>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<NameplateHandler>(), s.GetRequiredService<ActorObjectService>()));
|
collection.AddSingleton(s => new LightFinderScannerService(s.GetRequiredService<ILogger<LightFinderScannerService>>(), framework, s.GetRequiredService<LightFinderService>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<NameplateHandler>(), s.GetRequiredService<ActorObjectService>()));
|
||||||
|
collection.AddSingleton((s) => new LightFinderPlateHandler(s.GetRequiredService<ILogger<LightFinderPlateHandler>>(),
|
||||||
|
s.GetRequiredService<LightlessMediator>(), pluginInterface,
|
||||||
|
s.GetRequiredService<LightlessConfigService>(),
|
||||||
|
objectTable, gameGui));
|
||||||
|
|
||||||
// add scoped services
|
// add scoped services
|
||||||
collection.AddScoped<DrawEntityFactory>();
|
collection.AddScoped<DrawEntityFactory>();
|
||||||
@@ -347,8 +350,6 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
s.GetRequiredService<LightlessMediator>()));
|
s.GetRequiredService<LightlessMediator>()));
|
||||||
collection.AddScoped((s) => new NameplateService(s.GetRequiredService<ILogger<NameplateService>>(), s.GetRequiredService<LightlessConfigService>(), clientState, gameGui, objectTable, gameInteropProvider,
|
collection.AddScoped((s) => new NameplateService(s.GetRequiredService<ILogger<NameplateService>>(), s.GetRequiredService<LightlessConfigService>(), clientState, gameGui, objectTable, gameInteropProvider,
|
||||||
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PairUiService>()));
|
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PairUiService>()));
|
||||||
collection.AddScoped((s) => new NameplateHandler(s.GetRequiredService<ILogger<NameplateHandler>>(), addonLifecycle, gameGui,
|
|
||||||
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), objectTable, s.GetRequiredService<PairUiService>()));
|
|
||||||
|
|
||||||
collection.AddHostedService(p => p.GetRequiredService<ConfigurationSaveService>());
|
collection.AddHostedService(p => p.GetRequiredService<ConfigurationSaveService>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<ActorObjectService>());
|
collection.AddHostedService(p => p.GetRequiredService<ActorObjectService>());
|
||||||
@@ -365,6 +366,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddHostedService(p => p.GetRequiredService<LightlessPlugin>());
|
collection.AddHostedService(p => p.GetRequiredService<LightlessPlugin>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<ContextMenuService>());
|
collection.AddHostedService(p => p.GetRequiredService<ContextMenuService>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<LightFinderService>());
|
collection.AddHostedService(p => p.GetRequiredService<LightFinderService>());
|
||||||
|
collection.AddHostedService(p => p.GetRequiredService<LightFinderPlateHandler>());
|
||||||
})
|
})
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
|
|||||||
249
LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs
Normal file
249
LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
|
using LightlessSync.LightlessConfiguration;
|
||||||
|
using LightlessSync.Services.Mediator;
|
||||||
|
using LightlessSync.UI;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace LightlessSync.Services.LightFinder
|
||||||
|
{
|
||||||
|
public class LightFinderPlateHandler : IHostedService, IMediatorSubscriber
|
||||||
|
{
|
||||||
|
private readonly ILogger<LightFinderPlateHandler> _logger;
|
||||||
|
private readonly LightlessConfigService _configService;
|
||||||
|
private readonly IDalamudPluginInterface _pluginInterface;
|
||||||
|
private readonly IObjectTable _gameObjects;
|
||||||
|
private readonly IGameGui _gameGui;
|
||||||
|
|
||||||
|
private const float _defaultNameplateDistance = 15.0f;
|
||||||
|
private ImmutableHashSet<string> _activeBroadcastingCids = [];
|
||||||
|
private readonly Dictionary<IGameObject, Vector3> _smoothed = [];
|
||||||
|
private readonly float _defaultHeightOffset = 0f;
|
||||||
|
|
||||||
|
public LightlessMediator Mediator { get; }
|
||||||
|
|
||||||
|
public LightFinderPlateHandler(
|
||||||
|
ILogger<LightFinderPlateHandler> logger,
|
||||||
|
LightlessMediator mediator,
|
||||||
|
IDalamudPluginInterface dalamudPluginInterface,
|
||||||
|
LightlessConfigService configService,
|
||||||
|
IObjectTable gameObjects,
|
||||||
|
IGameGui gameGui)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
Mediator = mediator;
|
||||||
|
_pluginInterface = dalamudPluginInterface;
|
||||||
|
_configService = configService;
|
||||||
|
_gameObjects = gameObjects;
|
||||||
|
_gameGui = gameGui;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Starting LightFinderPlateHandler...");
|
||||||
|
|
||||||
|
_pluginInterface.UiBuilder.Draw += OnDraw;
|
||||||
|
|
||||||
|
_logger.LogInformation("LightFinderPlateHandler started.");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Stopping LightFinderPlateHandler...");
|
||||||
|
|
||||||
|
_pluginInterface.UiBuilder.Draw -= OnDraw;
|
||||||
|
|
||||||
|
_logger.LogInformation("LightFinderPlateHandler stopped.");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void OnDraw()
|
||||||
|
{
|
||||||
|
if (!_configService.Current.BroadcastEnabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_activeBroadcastingCids.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var drawList = ImGui.GetForegroundDrawList();
|
||||||
|
|
||||||
|
foreach (var obj in _gameObjects.PlayerObjects.OfType<IPlayerCharacter>())
|
||||||
|
{
|
||||||
|
//Double check to be sure, should always be true due to OfType filter above
|
||||||
|
if (obj is not IPlayerCharacter player)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (player.Address == IntPtr.Zero)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var hashedCID = DalamudUtilService.GetHashedCIDFromPlayerPointer(player.Address);
|
||||||
|
if (!_activeBroadcastingCids.Contains(hashedCID))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
//Approximate check if nameplate should be visible (at short distances)
|
||||||
|
if (!ShouldApproximateNameplateVisible(player))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!TryGetApproxNameplateScreenPos(player, out var rawScreenPos))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var rawVector3 = new Vector3(rawScreenPos.X, rawScreenPos.Y, 0f);
|
||||||
|
|
||||||
|
if (rawVector3 == Vector3.Zero)
|
||||||
|
{
|
||||||
|
_smoothed.Remove(obj);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Possible have to rework this. Currently just a simple distance check to avoid jitter.
|
||||||
|
Vector3 smoothedVector3;
|
||||||
|
|
||||||
|
if (_smoothed.TryGetValue(obj, out var lastVector3))
|
||||||
|
{
|
||||||
|
var deltaVector2 = new Vector2(rawVector3.X - lastVector3.X, rawVector3.Y - lastVector3.Y);
|
||||||
|
if (deltaVector2.Length() < 1f)
|
||||||
|
smoothedVector3 = lastVector3;
|
||||||
|
else
|
||||||
|
smoothedVector3 = rawVector3;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
smoothedVector3 = rawVector3;
|
||||||
|
}
|
||||||
|
|
||||||
|
_smoothed[obj] = smoothedVector3;
|
||||||
|
|
||||||
|
var screenPos = new Vector2(smoothedVector3.X, smoothedVector3.Y);
|
||||||
|
|
||||||
|
var radiusWorld = Math.Max(player.HitboxRadius, 0.5f);
|
||||||
|
var radiusPx = radiusWorld * 8.0f;
|
||||||
|
var offsetPx = GetScreenOffset(player);
|
||||||
|
var drawPos = new Vector2(screenPos.X, screenPos.Y - offsetPx);
|
||||||
|
|
||||||
|
var fillColor = ImGui.GetColorU32(UiSharedService.Color(UIColors.Get("Lightfinder")));
|
||||||
|
var outlineColor = ImGui.GetColorU32(UiSharedService.Color(UIColors.Get("LightfinderEdge")));
|
||||||
|
|
||||||
|
drawList.AddCircleFilled(drawPos, radiusPx, fillColor);
|
||||||
|
drawList.AddCircle(drawPos, radiusPx, outlineColor, 0, 2.0f);
|
||||||
|
|
||||||
|
var label = "LightFinder";
|
||||||
|
var icon = FontAwesomeIcon.Bullseye.ToIconString();
|
||||||
|
|
||||||
|
ImGui.PushFont(UiBuilder.IconFont);
|
||||||
|
var iconSize = ImGui.CalcTextSize(icon);
|
||||||
|
var iconPos = new Vector2(drawPos.X - iconSize.X / 2f, drawPos.Y - radiusPx - iconSize.Y - 2f);
|
||||||
|
drawList.AddText(iconPos, fillColor, icon);
|
||||||
|
ImGui.PopFont();
|
||||||
|
|
||||||
|
/* var scale = 1.4f;
|
||||||
|
var font = ImGui.GetFont();
|
||||||
|
var baseFontSize = ImGui.GetFontSize();
|
||||||
|
var fontSize = baseFontSize * scale;
|
||||||
|
|
||||||
|
var baseTextSize = ImGui.CalcTextSize(label);
|
||||||
|
var textSize = baseTextSize * scale;
|
||||||
|
|
||||||
|
var textPos = new Vector2(
|
||||||
|
drawPos.X - textSize.X / 2f,
|
||||||
|
drawPos.Y - radiusPx - textSize.Y - 2f
|
||||||
|
);
|
||||||
|
|
||||||
|
drawList.AddText(font, fontSize, textPos, fillColor, label); */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get screen offset based on distance to local player (to scale size appropriately)
|
||||||
|
// I need to fine tune these values still
|
||||||
|
private float GetScreenOffset(IPlayerCharacter player)
|
||||||
|
{
|
||||||
|
var local = _gameObjects.LocalPlayer;
|
||||||
|
if (local == null)
|
||||||
|
return 32.1f;
|
||||||
|
|
||||||
|
var delta = player.Position - local.Position;
|
||||||
|
var dist = MathF.Sqrt(delta.X * delta.X + delta.Z * delta.Z);
|
||||||
|
|
||||||
|
const float minDist = 2.1f;
|
||||||
|
const float maxDist = 30.4f;
|
||||||
|
dist = Math.Clamp(dist, minDist, maxDist);
|
||||||
|
|
||||||
|
var t = 1f - (dist - minDist) / (maxDist - minDist);
|
||||||
|
|
||||||
|
const float minOffset = 24.4f;
|
||||||
|
const float maxOffset = 56.4f;
|
||||||
|
return minOffset + (maxOffset - minOffset) * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetApproxNameplateScreenPos(IPlayerCharacter player, out Vector2 screenPos)
|
||||||
|
{
|
||||||
|
screenPos = default;
|
||||||
|
|
||||||
|
var worldPos = player.Position;
|
||||||
|
|
||||||
|
var visualHeight = GetVisualHeight(player);
|
||||||
|
|
||||||
|
worldPos.Y += (visualHeight + 1.2f) + _defaultHeightOffset;
|
||||||
|
|
||||||
|
if (!_gameGui.WorldToScreen(worldPos, out var raw))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
screenPos = raw;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Approximate check to see if nameplate would be visible based on distance and screen position
|
||||||
|
// Also has to be fine tuned still
|
||||||
|
private bool ShouldApproximateNameplateVisible(IPlayerCharacter player)
|
||||||
|
{
|
||||||
|
var local = _gameObjects.LocalPlayer;
|
||||||
|
if (local == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var delta = player.Position - local.Position;
|
||||||
|
var distance2D = MathF.Sqrt(delta.X * delta.X + delta.Z * delta.Z);
|
||||||
|
if (distance2D > _defaultNameplateDistance)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var verticalDelta = MathF.Abs(delta.Y);
|
||||||
|
if (verticalDelta > 3.4f)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return TryGetApproxNameplateScreenPos(player, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static unsafe float GetVisualHeight(IPlayerCharacter player)
|
||||||
|
{
|
||||||
|
var gameObject = (GameObject*)player.Address;
|
||||||
|
if (gameObject == null)
|
||||||
|
return Math.Max(player.HitboxRadius * 2.0f, 1.7f); // fallback
|
||||||
|
|
||||||
|
// This should account for transformations (sitting, crouching, etc.)
|
||||||
|
var radius = gameObject->GetRadius(adjustByTransformation: true);
|
||||||
|
if (radius <= 0)
|
||||||
|
radius = Math.Max(player.HitboxRadius * 2.0f, 1.7f);
|
||||||
|
|
||||||
|
return radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the set of active broadcasting CIDs (Same uses as in NameplateHnadler before)
|
||||||
|
public void UpdateBroadcastingCids(IEnumerable<string> cids)
|
||||||
|
{
|
||||||
|
var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal);
|
||||||
|
if (ReferenceEquals(_activeBroadcastingCids, newSet) || _activeBroadcastingCids.SetEquals(newSet))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_activeBroadcastingCids = newSet;
|
||||||
|
if (_logger.IsEnabled(LogLevel.Information))
|
||||||
|
_logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(',', _activeBroadcastingCids));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
private readonly IFramework _framework;
|
private readonly IFramework _framework;
|
||||||
|
|
||||||
private readonly LightFinderService _broadcastService;
|
private readonly LightFinderService _broadcastService;
|
||||||
private readonly NameplateHandler _nameplateHandler;
|
private readonly LightFinderPlateHandler _lightFinderPlateHandler;
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, BroadcastEntry> _broadcastCache = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, BroadcastEntry> _broadcastCache = new(StringComparer.Ordinal);
|
||||||
private readonly Queue<string> _lookupQueue = new();
|
private readonly Queue<string> _lookupQueue = new();
|
||||||
@@ -41,22 +41,21 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
IFramework framework,
|
IFramework framework,
|
||||||
LightFinderService broadcastService,
|
LightFinderService broadcastService,
|
||||||
LightlessMediator mediator,
|
LightlessMediator mediator,
|
||||||
NameplateHandler nameplateHandler,
|
LightFinderPlateHandler lightFinderPlateHandler,
|
||||||
ActorObjectService actorTracker) : base(logger, mediator)
|
ActorObjectService actorTracker) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_actorTracker = actorTracker;
|
_actorTracker = actorTracker;
|
||||||
_broadcastService = broadcastService;
|
_broadcastService = broadcastService;
|
||||||
_nameplateHandler = nameplateHandler;
|
_lightFinderPlateHandler = lightFinderPlateHandler;
|
||||||
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_framework = framework;
|
_framework = framework;
|
||||||
_framework.Update += OnFrameworkUpdate;
|
_framework.Update += OnFrameworkUpdate;
|
||||||
|
|
||||||
Mediator.Subscribe<BroadcastStatusChangedMessage>(this, OnBroadcastStatusChanged);
|
Mediator.Subscribe<BroadcastStatusChangedMessage>(this, OnBroadcastStatusChanged);
|
||||||
_cleanupTask = Task.Run(ExpiredBroadcastCleanupLoop);
|
_cleanupTask = Task.Run(ExpiredBroadcastCleanupLoop, _cleanupCts.Token);
|
||||||
|
|
||||||
_nameplateHandler.Init();
|
|
||||||
_actorTracker = actorTracker;
|
_actorTracker = actorTracker;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +128,7 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
.Select(e => e.Key)
|
.Select(e => e.Key)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
_nameplateHandler.UpdateBroadcastingCids(activeCids);
|
_lightFinderPlateHandler.UpdateBroadcastingCids(activeCids);
|
||||||
UpdateSyncshellBroadcasts();
|
UpdateSyncshellBroadcasts();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +141,7 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
_lookupQueuedCids.Clear();
|
_lookupQueuedCids.Clear();
|
||||||
_syncshellCids.Clear();
|
_syncshellCids.Clear();
|
||||||
|
|
||||||
_nameplateHandler.UpdateBroadcastingCids(Enumerable.Empty<string>());
|
_lightFinderPlateHandler.UpdateBroadcastingCids([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,6 +242,5 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
_cleanupTask?.Wait(100);
|
_cleanupTask?.Wait(100);
|
||||||
_cleanupCts.Dispose();
|
_cleanupCts.Dispose();
|
||||||
_nameplateHandler.Uninit();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,693 +0,0 @@
|
|||||||
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;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
|
||||||
using LightlessSync.LightlessConfiguration;
|
|
||||||
using LightlessSync.Services.Mediator;
|
|
||||||
using LightlessSync.UI;
|
|
||||||
using LightlessSync.UI.Services;
|
|
||||||
using LightlessSync.Utils;
|
|
||||||
using LightlessSync.UtilsEnum.Enum;
|
|
||||||
|
|
||||||
// Created using https://github.com/PunishedPineapple/Distance as a reference, thank you!
|
|
||||||
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Collections.Immutable;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace LightlessSync.Services;
|
|
||||||
|
|
||||||
public unsafe class NameplateHandler : IMediatorSubscriber
|
|
||||||
{
|
|
||||||
private readonly ILogger<NameplateHandler> _logger;
|
|
||||||
private readonly IAddonLifecycle _addonLifecycle;
|
|
||||||
private readonly IGameGui _gameGui;
|
|
||||||
private readonly IObjectTable _objectTable;
|
|
||||||
private readonly LightlessConfigService _configService;
|
|
||||||
private readonly PairUiService _pairUiService;
|
|
||||||
private readonly LightlessMediator _mediator;
|
|
||||||
public LightlessMediator Mediator => _mediator;
|
|
||||||
|
|
||||||
private bool _mEnabled = false;
|
|
||||||
private bool _needsLabelRefresh = false;
|
|
||||||
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];
|
|
||||||
private readonly int[] _cachedNameplateTextOffsets = new int[AddonNamePlate.NumNamePlateObjects];
|
|
||||||
|
|
||||||
internal const uint mNameplateNodeIDBase = 0x7D99D500;
|
|
||||||
private const string DefaultLabelText = "LightFinder";
|
|
||||||
private const SeIconChar DefaultIcon = SeIconChar.Hyadelyn;
|
|
||||||
private static readonly string DefaultIconGlyph = SeIconCharExtensions.ToIconString(DefaultIcon);
|
|
||||||
|
|
||||||
private ImmutableHashSet<string> _activeBroadcastingCids = [];
|
|
||||||
|
|
||||||
public NameplateHandler(ILogger<NameplateHandler> logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, LightlessConfigService configService, LightlessMediator mediator, IObjectTable objectTable, PairUiService pairUiService)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_addonLifecycle = addonLifecycle;
|
|
||||||
_gameGui = gameGui;
|
|
||||||
_configService = configService;
|
|
||||||
_mediator = mediator;
|
|
||||||
_objectTable = objectTable;
|
|
||||||
_pairUiService = pairUiService;
|
|
||||||
|
|
||||||
System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void Init()
|
|
||||||
{
|
|
||||||
EnableNameplate();
|
|
||||||
_mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void Uninit()
|
|
||||||
{
|
|
||||||
DisableNameplate();
|
|
||||||
DestroyNameplateNodes();
|
|
||||||
_mediator.Unsubscribe<PriorityFrameworkUpdateMessage>(this);
|
|
||||||
_mpNameplateAddon = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void EnableNameplate()
|
|
||||||
{
|
|
||||||
if (!_mEnabled)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_addonLifecycle.RegisterListener(AddonEvent.PostDraw, "NamePlate", NameplateDrawDetour);
|
|
||||||
_mEnabled = true;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_logger.LogError($"Unknown error while trying to enable nameplate distances:\n{e}");
|
|
||||||
DisableNameplate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void DisableNameplate()
|
|
||||||
{
|
|
||||||
if (_mEnabled)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_addonLifecycle.UnregisterListener(NameplateDrawDetour);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_logger.LogError($"Unknown error while unregistering nameplate listener:\n{e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
_mEnabled = false;
|
|
||||||
HideAllNameplateNodes();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void NameplateDrawDetour(AddonEvent type, AddonArgs args)
|
|
||||||
{
|
|
||||||
if (args.Addon.Address == nint.Zero)
|
|
||||||
{
|
|
||||||
if (_logger.IsEnabled(LogLevel.Warning))
|
|
||||||
_logger.LogWarning("Nameplate draw detour received a null addon address, skipping update.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var pNameplateAddon = (AddonNamePlate*)args.Addon.Address;
|
|
||||||
|
|
||||||
if (_mpNameplateAddon != pNameplateAddon)
|
|
||||||
{
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateNameplateNodes();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CreateNameplateNodes()
|
|
||||||
{
|
|
||||||
for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i)
|
|
||||||
{
|
|
||||||
var nameplateObject = GetNameplateObject(i);
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
var pLastChild = pNameplateResNode->ChildNode;
|
|
||||||
while (pLastChild->PrevSiblingNode != null) pLastChild = pLastChild->PrevSiblingNode;
|
|
||||||
pNewNode->AtkResNode.NextSiblingNode = pLastChild;
|
|
||||||
pNewNode->AtkResNode.ParentNode = pNameplateResNode;
|
|
||||||
pLastChild->PrevSiblingNode = (AtkResNode*)pNewNode;
|
|
||||||
rootNode->Component->UldManager.UpdateDrawNodeList();
|
|
||||||
pNewNode->AtkResNode.SetUseDepthBasedPriority(true);
|
|
||||||
_mTextNodes[i] = pNewNode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DestroyNameplateNodes()
|
|
||||||
{
|
|
||||||
var currentHandle = _gameGui.GetAddonByName("NamePlate", 1);
|
|
||||||
if (currentHandle.Address == nint.Zero)
|
|
||||||
{
|
|
||||||
if (_logger.IsEnabled(LogLevel.Warning))
|
|
||||||
_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)
|
|
||||||
{
|
|
||||||
if (_logger.IsEnabled(LogLevel.Warning))
|
|
||||||
_logger.LogWarning("Skipping nameplate node destroy due to addon address mismatch (cached {Cached}, current {Current}).", (IntPtr)_mpNameplateAddon, (IntPtr)pCurrentNameplateAddon);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i)
|
|
||||||
{
|
|
||||||
var pTextNode = _mTextNodes[i];
|
|
||||||
var pNameplateNode = GetNameplateComponentNode(i);
|
|
||||||
if (pTextNode != null && (pNameplateNode == null || pNameplateNode->Component == null))
|
|
||||||
{
|
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
|
||||||
_logger.LogDebug("Skipping destroy for nameplate {Index} because its component node is unavailable.", i);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pTextNode != null && pNameplateNode != null && pNameplateNode->Component != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (pTextNode->AtkResNode.PrevSiblingNode != null)
|
|
||||||
pTextNode->AtkResNode.PrevSiblingNode->NextSiblingNode = pTextNode->AtkResNode.NextSiblingNode;
|
|
||||||
if (pTextNode->AtkResNode.NextSiblingNode != null)
|
|
||||||
pTextNode->AtkResNode.NextSiblingNode->PrevSiblingNode = pTextNode->AtkResNode.PrevSiblingNode;
|
|
||||||
pNameplateNode->Component->UldManager.UpdateDrawNodeList();
|
|
||||||
pTextNode->AtkResNode.Destroy(free: true);
|
|
||||||
_mTextNodes[i] = null;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
if (_logger.IsEnabled(LogLevel.Error))
|
|
||||||
_logger.LogError("Unknown error while removing text node 0x{textNode} for nameplate {i} on component node 0x{nameplateNode}:\n{e}", (IntPtr)pTextNode, i, (IntPtr)pNameplateNode, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HideAllNameplateNodes()
|
|
||||||
{
|
|
||||||
for (int i = 0; i < _mTextNodes.Length; ++i)
|
|
||||||
{
|
|
||||||
HideNameplateTextNode(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateNameplateNodes()
|
|
||||||
{
|
|
||||||
var currentHandle = _gameGui.GetAddonByName("NamePlate");
|
|
||||||
if (currentHandle.Address == nint.Zero)
|
|
||||||
{
|
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
|
||||||
_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.IsEnabled(LogLevel.Debug))
|
|
||||||
_logger.LogDebug("Cached NamePlate addon pointer differs from current: waiting for new hook (cached {Cached}, current {Current}).", (IntPtr)_mpNameplateAddon, (IntPtr)currentAddon);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var framework = Framework.Instance();
|
|
||||||
if (framework == null)
|
|
||||||
{
|
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
|
||||||
_logger.LogDebug("Framework instance unavailable during nameplate update, skipping.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var uiModule = framework->GetUIModule();
|
|
||||||
if (uiModule == null)
|
|
||||||
{
|
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
|
||||||
_logger.LogDebug("UI module unavailable during nameplate update, skipping.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ui3DModule = uiModule->GetUI3DModule();
|
|
||||||
if (ui3DModule == null)
|
|
||||||
{
|
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
|
||||||
_logger.LogDebug("UI3D module unavailable during nameplate update, skipping.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var vec = ui3DModule->NamePlateObjectInfoPointers;
|
|
||||||
if (vec.IsEmpty)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var visibleUserIdsSnapshot = VisibleUserIds;
|
|
||||||
|
|
||||||
var safeCount = System.Math.Min(
|
|
||||||
ui3DModule->NamePlateObjectInfoCount,
|
|
||||||
vec.Length
|
|
||||||
);
|
|
||||||
|
|
||||||
for (int i = 0; i < safeCount; ++i)
|
|
||||||
{
|
|
||||||
var config = _configService.Current;
|
|
||||||
|
|
||||||
var objectInfoPtr = vec[i];
|
|
||||||
if (objectInfoPtr == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var objectInfo = objectInfoPtr.Value;
|
|
||||||
if (objectInfo == null || objectInfo->GameObject == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var nameplateIndex = objectInfo->NamePlateIndex;
|
|
||||||
if (nameplateIndex < 0 || nameplateIndex >= AddonNamePlate.NumNamePlateObjects)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var pNode = _mTextNodes[nameplateIndex];
|
|
||||||
if (pNode == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var gameObject = objectInfo->GameObject;
|
|
||||||
if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player)
|
|
||||||
{
|
|
||||||
pNode->AtkResNode.ToggleVisibility(enable: false);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// CID gating
|
|
||||||
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject);
|
|
||||||
if (cid == null || !_activeBroadcastingCids.Contains(cid))
|
|
||||||
{
|
|
||||||
pNode->AtkResNode.ToggleVisibility(enable: false);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var local = _objectTable.LocalPlayer;
|
|
||||||
if (!config.LightfinderLabelShowOwn && local != null &&
|
|
||||||
objectInfo->GameObject->GetGameObjectId() == local.GameObjectId)
|
|
||||||
{
|
|
||||||
pNode->AtkResNode.ToggleVisibility(enable: false);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var hidePaired = !config.LightfinderLabelShowPaired;
|
|
||||||
|
|
||||||
var goId = (ulong)gameObject->GetGameObjectId();
|
|
||||||
if (hidePaired && visibleUserIdsSnapshot.Contains(goId))
|
|
||||||
{
|
|
||||||
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 (root == null || root->Component == null || nameContainer == null || nameText == null)
|
|
||||||
{
|
|
||||||
_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 scaleMultiplier = System.Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f);
|
|
||||||
var baseScale = config.LightfinderLabelUseIcon ? 1.0f : 0.5f;
|
|
||||||
var effectiveScale = baseScale * scaleMultiplier;
|
|
||||||
var labelContent = config.LightfinderLabelUseIcon
|
|
||||||
? NormalizeIconGlyph(config.LightfinderLabelIconGlyph)
|
|
||||||
: DefaultLabelText;
|
|
||||||
|
|
||||||
pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed;
|
|
||||||
pNode->AtkResNode.SetScale(effectiveScale, effectiveScale);
|
|
||||||
var nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
|
||||||
if (nodeWidth <= 0)
|
|
||||||
nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
|
||||||
var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
|
|
||||||
var baseFontSize = config.LightfinderLabelUseIcon ? 36f : 24f;
|
|
||||||
var computedFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier);
|
|
||||||
pNode->FontSize = (byte)System.Math.Clamp(computedFontSize, 1, 255);
|
|
||||||
AlignmentType alignment;
|
|
||||||
|
|
||||||
var textScaleY = nameText->AtkResNode.ScaleY;
|
|
||||||
if (textScaleY <= 0f)
|
|
||||||
textScaleY = 1f;
|
|
||||||
|
|
||||||
var blockHeight = System.Math.Abs((int)nameplateObject.TextH);
|
|
||||||
if (blockHeight > 0)
|
|
||||||
{
|
|
||||||
_cachedNameplateTextHeights[nameplateIndex] = blockHeight;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
blockHeight = _cachedNameplateTextHeights[nameplateIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (blockHeight <= 0)
|
|
||||||
{
|
|
||||||
blockHeight = GetScaledTextHeight(nameText);
|
|
||||||
if (blockHeight <= 0)
|
|
||||||
blockHeight = nodeHeight;
|
|
||||||
|
|
||||||
_cachedNameplateTextHeights[nameplateIndex] = blockHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
var containerHeight = (int)nameContainer->Height;
|
|
||||||
if (containerHeight > 0)
|
|
||||||
{
|
|
||||||
_cachedNameplateContainerHeights[nameplateIndex] = containerHeight;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
containerHeight = _cachedNameplateContainerHeights[nameplateIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (containerHeight <= 0)
|
|
||||||
{
|
|
||||||
containerHeight = blockHeight + (int)System.Math.Round(8 * textScaleY);
|
|
||||||
if (containerHeight <= blockHeight)
|
|
||||||
containerHeight = blockHeight + 1;
|
|
||||||
|
|
||||||
_cachedNameplateContainerHeights[nameplateIndex] = containerHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
var blockTop = containerHeight - blockHeight;
|
|
||||||
if (blockTop < 0)
|
|
||||||
blockTop = 0;
|
|
||||||
var verticalPadding = (int)System.Math.Round(4 * effectiveScale);
|
|
||||||
|
|
||||||
var positionY = blockTop - verticalPadding - nodeHeight;
|
|
||||||
|
|
||||||
var textWidth = System.Math.Abs((int)nameplateObject.TextW);
|
|
||||||
if (textWidth <= 0)
|
|
||||||
{
|
|
||||||
textWidth = GetScaledTextWidth(nameText);
|
|
||||||
if (textWidth <= 0)
|
|
||||||
textWidth = nodeWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textWidth > 0)
|
|
||||||
{
|
|
||||||
_cachedNameplateTextWidths[nameplateIndex] = textWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
var textOffset = (int)System.Math.Round(nameText->AtkResNode.X);
|
|
||||||
var hasValidOffset = true;
|
|
||||||
|
|
||||||
if (System.Math.Abs((int)nameplateObject.TextW) > 0 || textOffset != 0)
|
|
||||||
{
|
|
||||||
_cachedNameplateTextOffsets[nameplateIndex] = textOffset;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
hasValidOffset = false;
|
|
||||||
}
|
|
||||||
int positionX;
|
|
||||||
|
|
||||||
|
|
||||||
if (!config.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal)))
|
|
||||||
labelContent = DefaultLabelText;
|
|
||||||
|
|
||||||
pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed;
|
|
||||||
|
|
||||||
pNode->SetText(labelContent);
|
|
||||||
|
|
||||||
if (!config.LightfinderLabelUseIcon)
|
|
||||||
{
|
|
||||||
pNode->TextFlags &= ~TextFlags.AutoAdjustNodeSize;
|
|
||||||
pNode->AtkResNode.Width = 0;
|
|
||||||
nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
|
||||||
if (nodeWidth <= 0)
|
|
||||||
nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
|
||||||
pNode->AtkResNode.Width = (ushort)nodeWidth;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
pNode->TextFlags |= TextFlags.AutoAdjustNodeSize;
|
|
||||||
pNode->AtkResNode.Width = 0;
|
|
||||||
nodeWidth = pNode->AtkResNode.GetWidth();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (config.LightfinderAutoAlign && nameContainer != null && hasValidOffset)
|
|
||||||
{
|
|
||||||
var nameplateWidth = (int)nameContainer->Width;
|
|
||||||
|
|
||||||
int leftPos = nameplateWidth / 8;
|
|
||||||
int rightPos = nameplateWidth - nodeWidth - (nameplateWidth / 8);
|
|
||||||
int centrePos = (nameplateWidth - nodeWidth) / 2;
|
|
||||||
int staticMargin = 24;
|
|
||||||
int calcMargin = (int)(nameplateWidth * 0.08f);
|
|
||||||
|
|
||||||
switch (config.LabelAlignment)
|
|
||||||
{
|
|
||||||
case LabelAlignment.Left:
|
|
||||||
positionX = config.LightfinderLabelUseIcon ? leftPos + staticMargin : leftPos;
|
|
||||||
alignment = AlignmentType.BottomLeft;
|
|
||||||
break;
|
|
||||||
case LabelAlignment.Right:
|
|
||||||
positionX = config.LightfinderLabelUseIcon ? rightPos - staticMargin : nameplateWidth - nodeWidth + calcMargin;
|
|
||||||
alignment = AlignmentType.BottomRight;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
positionX = config.LightfinderLabelUseIcon ? centrePos : centrePos + calcMargin;
|
|
||||||
alignment = AlignmentType.Bottom;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
positionX = 58 + config.LightfinderLabelOffsetX;
|
|
||||||
alignment = AlignmentType.Bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
positionY += config.LightfinderLabelOffsetY;
|
|
||||||
|
|
||||||
alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8);
|
|
||||||
pNode->AtkResNode.SetUseDepthBasedPriority(enable: true);
|
|
||||||
|
|
||||||
pNode->AtkResNode.Color.A = 255;
|
|
||||||
|
|
||||||
pNode->TextColor.R = (byte)(labelColor.X * 255);
|
|
||||||
pNode->TextColor.G = (byte)(labelColor.Y * 255);
|
|
||||||
pNode->TextColor.B = (byte)(labelColor.Z * 255);
|
|
||||||
pNode->TextColor.A = (byte)(labelColor.W * 255);
|
|
||||||
|
|
||||||
pNode->EdgeColor.R = (byte)(edgeColor.X * 255);
|
|
||||||
pNode->EdgeColor.G = (byte)(edgeColor.Y * 255);
|
|
||||||
pNode->EdgeColor.B = (byte)(edgeColor.Z * 255);
|
|
||||||
pNode->EdgeColor.A = (byte)(edgeColor.W * 255);
|
|
||||||
|
|
||||||
|
|
||||||
if (!config.LightfinderLabelUseIcon)
|
|
||||||
{
|
|
||||||
pNode->AlignmentType = AlignmentType.Bottom;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
pNode->AlignmentType = alignment;
|
|
||||||
}
|
|
||||||
pNode->AtkResNode.SetPositionShort(
|
|
||||||
(short)System.Math.Clamp(positionX, short.MinValue, short.MaxValue),
|
|
||||||
(short)System.Math.Clamp(positionY, short.MinValue, short.MaxValue)
|
|
||||||
);
|
|
||||||
var computedLineSpacing = (int)System.Math.Round(24 * scaleMultiplier);
|
|
||||||
pNode->LineSpacing = (byte)System.Math.Clamp(computedLineSpacing, 0, byte.MaxValue);
|
|
||||||
pNode->CharSpacing = 1;
|
|
||||||
pNode->TextFlags = config.LightfinderLabelUseIcon
|
|
||||||
? TextFlags.Edge | TextFlags.Glare | TextFlags.AutoAdjustNodeSize
|
|
||||||
: TextFlags.Edge | TextFlags.Glare;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static unsafe int GetScaledTextHeight(AtkTextNode* node)
|
|
||||||
{
|
|
||||||
if (node == null)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
var resNode = &node->AtkResNode;
|
|
||||||
var rawHeight = (int)resNode->GetHeight();
|
|
||||||
if (rawHeight <= 0 && node->LineSpacing > 0)
|
|
||||||
rawHeight = node->LineSpacing;
|
|
||||||
if (rawHeight <= 0)
|
|
||||||
rawHeight = AtkNodeHelpers.DefaultTextNodeHeight;
|
|
||||||
|
|
||||||
var scale = resNode->ScaleY;
|
|
||||||
if (scale <= 0f)
|
|
||||||
scale = 1f;
|
|
||||||
|
|
||||||
var computed = (int)System.Math.Round(rawHeight * scale);
|
|
||||||
return System.Math.Max(1, computed);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static unsafe int GetScaledTextWidth(AtkTextNode* node)
|
|
||||||
{
|
|
||||||
if (node == null)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
var resNode = &node->AtkResNode;
|
|
||||||
var rawWidth = (int)resNode->GetWidth();
|
|
||||||
if (rawWidth <= 0)
|
|
||||||
rawWidth = AtkNodeHelpers.DefaultTextNodeWidth;
|
|
||||||
|
|
||||||
var scale = resNode->ScaleX;
|
|
||||||
if (scale <= 0f)
|
|
||||||
scale = 1f;
|
|
||||||
|
|
||||||
var computed = (int)System.Math.Round(rawWidth * scale);
|
|
||||||
return System.Math.Max(1, computed);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static string NormalizeIconGlyph(string? rawInput)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(rawInput))
|
|
||||||
return DefaultIconGlyph;
|
|
||||||
|
|
||||||
var trimmed = rawInput.Trim();
|
|
||||||
|
|
||||||
if (Enum.TryParse<SeIconChar>(trimmed, true, out var iconEnum))
|
|
||||||
return SeIconCharExtensions.ToIconString(iconEnum);
|
|
||||||
|
|
||||||
var hexCandidate = trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
|
||||||
? trimmed[2..]
|
|
||||||
: trimmed;
|
|
||||||
|
|
||||||
if (ushort.TryParse(hexCandidate, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hexValue))
|
|
||||||
return char.ConvertFromUtf32(hexValue);
|
|
||||||
|
|
||||||
var enumerator = trimmed.EnumerateRunes();
|
|
||||||
if (enumerator.MoveNext())
|
|
||||||
return enumerator.Current.ToString();
|
|
||||||
|
|
||||||
return DefaultIconGlyph;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 void HideNameplateTextNode(int i)
|
|
||||||
{
|
|
||||||
var pNode = _mTextNodes[i];
|
|
||||||
if (pNode != null)
|
|
||||||
{
|
|
||||||
pNode->AtkResNode.ToggleVisibility(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private AddonNamePlate.NamePlateObject? GetNameplateObject(int i)
|
|
||||||
{
|
|
||||||
if (i < AddonNamePlate.NumNamePlateObjects &&
|
|
||||||
_mpNameplateAddon != null &&
|
|
||||||
_mpNameplateAddon->NamePlateObjectArray[i].RootComponentNode != null)
|
|
||||||
{
|
|
||||||
return _mpNameplateAddon->NamePlateObjectArray[i];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private AtkComponentNode* GetNameplateComponentNode(int i)
|
|
||||||
{
|
|
||||||
var nameplateObject = GetNameplateObject(i);
|
|
||||||
return nameplateObject != null ? nameplateObject.Value.RootComponentNode : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private HashSet<ulong> VisibleUserIds
|
|
||||||
=> [.. _pairUiService.GetSnapshot().PairsByUid.Values
|
|
||||||
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
|
||||||
.Select(u => (ulong)u.PlayerCharacterId)];
|
|
||||||
|
|
||||||
public void FlagRefresh()
|
|
||||||
{
|
|
||||||
_needsLabelRefresh = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnTick(PriorityFrameworkUpdateMessage _)
|
|
||||||
{
|
|
||||||
if (_needsLabelRefresh)
|
|
||||||
{
|
|
||||||
UpdateNameplateNodes();
|
|
||||||
_needsLabelRefresh = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateBroadcastingCids(IEnumerable<string> cids)
|
|
||||||
{
|
|
||||||
var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal);
|
|
||||||
if (ReferenceEquals(_activeBroadcastingCids, newSet) || _activeBroadcastingCids.SetEquals(newSet))
|
|
||||||
return;
|
|
||||||
|
|
||||||
_activeBroadcastingCids = newSet;
|
|
||||||
if (_logger.IsEnabled(LogLevel.Information))
|
|
||||||
_logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(',', _activeBroadcastingCids));
|
|
||||||
FlagRefresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ClearNameplateCaches()
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -277,7 +277,7 @@ public partial class EditProfileUi
|
|||||||
|
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture"))
|
||||||
{
|
{
|
||||||
_fileDialogManager.OpenFileDialog("Select syncshell profile picture", ImageFileDialogFilter, (success, file) =>
|
_fileDialogManager.OpenFileDialog("Select syncshell profile picture", _imageFileDialogFilter, (success, file) =>
|
||||||
{
|
{
|
||||||
if (!success || string.IsNullOrEmpty(file))
|
if (!success || string.IsNullOrEmpty(file))
|
||||||
return;
|
return;
|
||||||
@@ -305,7 +305,7 @@ public partial class EditProfileUi
|
|||||||
|
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile banner"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile banner"))
|
||||||
{
|
{
|
||||||
_fileDialogManager.OpenFileDialog("Select syncshell profile banner", ImageFileDialogFilter, (success, file) =>
|
_fileDialogManager.OpenFileDialog("Select syncshell profile banner", _imageFileDialogFilter, (success, file) =>
|
||||||
{
|
{
|
||||||
if (!success || string.IsNullOrEmpty(file))
|
if (!success || string.IsNullOrEmpty(file))
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -19,12 +19,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
using SixLabors.ImageSharp.Formats;
|
using SixLabors.ImageSharp.Formats;
|
||||||
using SixLabors.ImageSharp.PixelFormats;
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Linq;
|
|
||||||
using LightlessSync.Services.Profiles;
|
using LightlessSync.Services.Profiles;
|
||||||
|
|
||||||
namespace LightlessSync.UI;
|
namespace LightlessSync.UI;
|
||||||
@@ -56,9 +51,9 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
"webp",
|
"webp",
|
||||||
"bmp"
|
"bmp"
|
||||||
};
|
};
|
||||||
private const string ImageFileDialogFilter = "Images{.png,.jpg,.jpeg,.webp,.bmp}";
|
private const string _imageFileDialogFilter = "Images{.png,.jpg,.jpeg,.webp,.bmp}";
|
||||||
private readonly List<int> _tagEditorSelection = new();
|
private readonly List<int> _tagEditorSelection = new();
|
||||||
private int[] _profileTagIds = Array.Empty<int>();
|
private int[] _profileTagIds = [];
|
||||||
private readonly List<SeStringUtils.SeStringSegment> _tagPreviewSegments = new();
|
private readonly List<SeStringUtils.SeStringSegment> _tagPreviewSegments = new();
|
||||||
private enum ProfileEditorMode
|
private enum ProfileEditorMode
|
||||||
{
|
{
|
||||||
@@ -77,8 +72,8 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
private byte[]? _queuedBannerImage;
|
private byte[]? _queuedBannerImage;
|
||||||
private readonly Vector4 _tagBackgroundColor = new(0.18f, 0.18f, 0.18f, 0.95f);
|
private readonly Vector4 _tagBackgroundColor = new(0.18f, 0.18f, 0.18f, 0.95f);
|
||||||
private readonly Vector4 _tagBorderColor = new(0.35f, 0.35f, 0.35f, 0.4f);
|
private readonly Vector4 _tagBorderColor = new(0.35f, 0.35f, 0.35f, 0.4f);
|
||||||
private const int MaxProfileTags = 12;
|
private const int _maxProfileTags = 12;
|
||||||
private const int AvailableTagsPerPage = 6;
|
private const int _availableTagsPerPage = 6;
|
||||||
private int _availableTagPage;
|
private int _availableTagPage;
|
||||||
private UserData? _selfProfileUserData;
|
private UserData? _selfProfileUserData;
|
||||||
private string _descriptionText = string.Empty;
|
private string _descriptionText = string.Empty;
|
||||||
@@ -92,10 +87,10 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
private bool _wasOpen;
|
private bool _wasOpen;
|
||||||
|
|
||||||
private Vector4 _currentBg = new(0.15f, 0.15f, 0.15f, 1f);
|
private Vector4 _currentBg = new(0.15f, 0.15f, 0.15f, 1f);
|
||||||
private bool textEnabled;
|
private bool _textEnabled;
|
||||||
private bool glowEnabled;
|
private bool _glowEnabled;
|
||||||
private Vector4 textColor;
|
private Vector4 _textColor;
|
||||||
private Vector4 glowColor;
|
private Vector4 _glowColor;
|
||||||
|
|
||||||
private sealed record VanityState(bool TextEnabled, bool GlowEnabled, Vector4 TextColor, Vector4 GlowColor);
|
private sealed record VanityState(bool TextEnabled, bool GlowEnabled, Vector4 TextColor, Vector4 GlowColor);
|
||||||
private VanityState? _savedVanity;
|
private VanityState? _savedVanity;
|
||||||
@@ -154,13 +149,13 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
private void LoadVanity()
|
private void LoadVanity()
|
||||||
{
|
{
|
||||||
textEnabled = !string.IsNullOrEmpty(_apiController.TextColorHex);
|
_textEnabled = !string.IsNullOrEmpty(_apiController.TextColorHex);
|
||||||
glowEnabled = !string.IsNullOrEmpty(_apiController.TextGlowColorHex);
|
_glowEnabled = !string.IsNullOrEmpty(_apiController.TextGlowColorHex);
|
||||||
|
|
||||||
textColor = textEnabled ? UIColors.HexToRgba(_apiController.TextColorHex!) : Vector4.One;
|
_textColor = _textEnabled ? UIColors.HexToRgba(_apiController.TextColorHex!) : Vector4.One;
|
||||||
glowColor = glowEnabled ? UIColors.HexToRgba(_apiController.TextGlowColorHex!) : Vector4.Zero;
|
_glowColor = _glowEnabled ? UIColors.HexToRgba(_apiController.TextGlowColorHex!) : Vector4.Zero;
|
||||||
|
|
||||||
_savedVanity = new VanityState(textEnabled, glowEnabled, textColor, glowColor);
|
_savedVanity = new VanityState(_textEnabled, _glowEnabled, _textColor, _glowColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async void OnOpen()
|
public override async void OnOpen()
|
||||||
@@ -465,7 +460,7 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture"))
|
||||||
{
|
{
|
||||||
var existingBanner = GetCurrentProfileBannerBase64(profile);
|
var existingBanner = GetCurrentProfileBannerBase64(profile);
|
||||||
_fileDialogManager.OpenFileDialog("Select new Profile picture", ImageFileDialogFilter, (success, file) =>
|
_fileDialogManager.OpenFileDialog("Select new Profile picture", _imageFileDialogFilter, (success, file) =>
|
||||||
{
|
{
|
||||||
if (!success) return;
|
if (!success) return;
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
@@ -529,7 +524,7 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile banner"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile banner"))
|
||||||
{
|
{
|
||||||
var existingProfile = GetCurrentProfilePictureBase64(profile);
|
var existingProfile = GetCurrentProfilePictureBase64(profile);
|
||||||
_fileDialogManager.OpenFileDialog("Select new Profile banner", ImageFileDialogFilter, (success, file) =>
|
_fileDialogManager.OpenFileDialog("Select new Profile banner", _imageFileDialogFilter, (success, file) =>
|
||||||
{
|
{
|
||||||
if (!success) return;
|
if (!success) return;
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
@@ -686,7 +681,7 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
var defaultTextColorU32 = ImGui.GetColorU32(ImGuiCol.Text);
|
var defaultTextColorU32 = ImGui.GetColorU32(ImGuiCol.Text);
|
||||||
|
|
||||||
var selectedCount = _tagEditorSelection.Count;
|
var selectedCount = _tagEditorSelection.Count;
|
||||||
ImGui.TextColored(UIColors.Get("LightlessBlue"), $"Selected Tags ({selectedCount}/{MaxProfileTags})");
|
ImGui.TextColored(UIColors.Get("LightlessBlue"), $"Selected Tags ({selectedCount}/{_maxProfileTags})");
|
||||||
|
|
||||||
int? tagToRemove = null;
|
int? tagToRemove = null;
|
||||||
int? moveUpRequest = null;
|
int? moveUpRequest = null;
|
||||||
@@ -766,9 +761,9 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
if (tagToRemove.HasValue)
|
if (tagToRemove.HasValue)
|
||||||
_tagEditorSelection.Remove(tagToRemove.Value);
|
_tagEditorSelection.Remove(tagToRemove.Value);
|
||||||
|
|
||||||
bool limitReached = _tagEditorSelection.Count >= MaxProfileTags;
|
bool limitReached = _tagEditorSelection.Count >= _maxProfileTags;
|
||||||
if (limitReached)
|
if (limitReached)
|
||||||
UiSharedService.ColorTextWrapped($"You have reached the maximum of {MaxProfileTags} tags. Remove one before adding more.", UIColors.Get("DimRed"));
|
UiSharedService.ColorTextWrapped($"You have reached the maximum of {_maxProfileTags} tags. Remove one before adding more.", UIColors.Get("DimRed"));
|
||||||
|
|
||||||
ImGui.Dummy(new Vector2(0f, 6f * scale));
|
ImGui.Dummy(new Vector2(0f, 6f * scale));
|
||||||
ImGui.TextColored(UIColors.Get("LightlessPurple"), "Available Tags");
|
ImGui.TextColored(UIColors.Get("LightlessPurple"), "Available Tags");
|
||||||
@@ -798,10 +793,10 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
int pageCount = Math.Max(1, (totalAvailable + AvailableTagsPerPage - 1) / AvailableTagsPerPage);
|
int pageCount = Math.Max(1, (totalAvailable + _availableTagsPerPage - 1) / _availableTagsPerPage);
|
||||||
_availableTagPage = Math.Clamp(_availableTagPage, 0, pageCount - 1);
|
_availableTagPage = Math.Clamp(_availableTagPage, 0, pageCount - 1);
|
||||||
int start = _availableTagPage * AvailableTagsPerPage;
|
int start = _availableTagPage * _availableTagsPerPage;
|
||||||
int end = Math.Min(totalAvailable, start + AvailableTagsPerPage);
|
int end = Math.Min(totalAvailable, start + _availableTagsPerPage);
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.TextDisabled($"Page {_availableTagPage + 1}/{pageCount}");
|
ImGui.TextDisabled($"Page {_availableTagPage + 1}/{pageCount}");
|
||||||
@@ -1118,8 +1113,8 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
var monoFont = UiBuilder.MonoFont;
|
var monoFont = UiBuilder.MonoFont;
|
||||||
using (ImRaii.PushFont(monoFont))
|
using (ImRaii.PushFont(monoFont))
|
||||||
{
|
{
|
||||||
var previewTextColor = textEnabled ? textColor : Vector4.One;
|
var previewTextColor = _textEnabled ? _textColor : Vector4.One;
|
||||||
var previewGlowColor = glowEnabled ? glowColor : Vector4.Zero;
|
var previewGlowColor = _glowEnabled ? _glowColor : Vector4.Zero;
|
||||||
var seString = SeStringUtils.BuildFormattedPlayerName(_apiController.DisplayName, previewTextColor, previewGlowColor);
|
var seString = SeStringUtils.BuildFormattedPlayerName(_apiController.DisplayName, previewTextColor, previewGlowColor);
|
||||||
|
|
||||||
var drawList = ImGui.GetWindowDrawList();
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
@@ -1151,33 +1146,33 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase
|
|||||||
if (!hasVanity)
|
if (!hasVanity)
|
||||||
ImGui.BeginDisabled();
|
ImGui.BeginDisabled();
|
||||||
|
|
||||||
if (DrawCheckboxRow("Enable custom text color", textEnabled, out var newTextEnabled))
|
if (DrawCheckboxRow("Enable custom text color", _textEnabled, out var newTextEnabled))
|
||||||
textEnabled = newTextEnabled;
|
_textEnabled = newTextEnabled;
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.BeginDisabled(!textEnabled);
|
ImGui.BeginDisabled(!_textEnabled);
|
||||||
ImGui.ColorEdit4("Text Color##vanityTextColor", ref textColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf);
|
ImGui.ColorEdit4("Text Color##vanityTextColor", ref _textColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf);
|
||||||
ImGui.EndDisabled();
|
ImGui.EndDisabled();
|
||||||
|
|
||||||
if (DrawCheckboxRow("Enable glow color", glowEnabled, out var newGlowEnabled))
|
if (DrawCheckboxRow("Enable glow color", _glowEnabled, out var newGlowEnabled))
|
||||||
glowEnabled = newGlowEnabled;
|
_glowEnabled = newGlowEnabled;
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.BeginDisabled(!glowEnabled);
|
ImGui.BeginDisabled(!_glowEnabled);
|
||||||
ImGui.ColorEdit4("Glow Color##vanityGlowColor", ref glowColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf);
|
ImGui.ColorEdit4("Glow Color##vanityGlowColor", ref _glowColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf);
|
||||||
ImGui.EndDisabled();
|
ImGui.EndDisabled();
|
||||||
|
|
||||||
bool changed = !Equals(_savedVanity, new VanityState(textEnabled, glowEnabled, textColor, glowColor));
|
bool changed = !Equals(_savedVanity, new VanityState(_textEnabled, _glowEnabled, _textColor, _glowColor));
|
||||||
if (!changed)
|
if (!changed)
|
||||||
ImGui.BeginDisabled();
|
ImGui.BeginDisabled();
|
||||||
|
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Vanity Changes"))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Vanity Changes"))
|
||||||
{
|
{
|
||||||
string? newText = textEnabled ? UIColors.RgbaToHex(textColor) : string.Empty;
|
string? newText = _textEnabled ? UIColors.RgbaToHex(_textColor) : string.Empty;
|
||||||
string? newGlow = glowEnabled ? UIColors.RgbaToHex(glowColor) : string.Empty;
|
string? newGlow = _glowEnabled ? UIColors.RgbaToHex(_glowColor) : string.Empty;
|
||||||
|
|
||||||
_ = _apiController.UserUpdateVanityColors(new UserVanityColorsDto(newText, newGlow));
|
_ = _apiController.UserUpdateVanityColors(new UserVanityColorsDto(newText, newGlow));
|
||||||
_savedVanity = new VanityState(textEnabled, glowEnabled, textColor, glowColor);
|
_savedVanity = new VanityState(_textEnabled, _glowEnabled, _textColor, _glowColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!changed)
|
if (!changed)
|
||||||
|
|||||||
Reference in New Issue
Block a user