Added documentation/comments, Added gpose detection.
This commit is contained in:
@@ -267,6 +267,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
sp.GetRequiredService<ILogger<LightFinderPlateHandler>>(),
|
sp.GetRequiredService<ILogger<LightFinderPlateHandler>>(),
|
||||||
addonLifecycle,
|
addonLifecycle,
|
||||||
gameGui,
|
gameGui,
|
||||||
|
clientState,
|
||||||
sp.GetRequiredService<LightlessConfigService>(),
|
sp.GetRequiredService<LightlessConfigService>(),
|
||||||
sp.GetRequiredService<LightlessMediator>(),
|
sp.GetRequiredService<LightlessMediator>(),
|
||||||
objectTable,
|
objectTable,
|
||||||
|
|||||||
@@ -28,12 +28,16 @@ using Task = System.Threading.Tasks.Task;
|
|||||||
|
|
||||||
namespace LightlessSync.Services.LightFinder;
|
namespace LightlessSync.Services.LightFinder;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The new lightfinder nameplate handler using ImGUI (pictomancy) for rendering the icon/labels.
|
||||||
|
/// </summary>
|
||||||
public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscriber
|
public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscriber
|
||||||
{
|
{
|
||||||
private readonly ILogger<LightFinderPlateHandler> _logger;
|
private readonly ILogger<LightFinderPlateHandler> _logger;
|
||||||
private readonly IAddonLifecycle _addonLifecycle;
|
private readonly IAddonLifecycle _addonLifecycle;
|
||||||
private readonly IGameGui _gameGui;
|
private readonly IGameGui _gameGui;
|
||||||
private readonly IObjectTable _objectTable;
|
private readonly IObjectTable _objectTable;
|
||||||
|
private readonly IClientState _clientState;
|
||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly PairUiService _pairUiService;
|
private readonly PairUiService _pairUiService;
|
||||||
private readonly LightlessMediator _mediator;
|
private readonly LightlessMediator _mediator;
|
||||||
@@ -44,16 +48,17 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
private bool _needsLabelRefresh;
|
private bool _needsLabelRefresh;
|
||||||
private bool _drawSubscribed;
|
private bool _drawSubscribed;
|
||||||
private AddonNamePlate* _mpNameplateAddon;
|
private AddonNamePlate* _mpNameplateAddon;
|
||||||
private readonly object _labelLock = new();
|
private readonly Lock _labelLock = new();
|
||||||
private readonly NameplateBuffers _buffers = new();
|
private readonly NameplateBuffers _buffers = new();
|
||||||
private int _labelRenderCount;
|
private int _labelRenderCount;
|
||||||
|
|
||||||
private const string DefaultLabelText = "LightFinder";
|
private const string _defaultLabelText = "LightFinder";
|
||||||
private const SeIconChar DefaultIcon = SeIconChar.Hyadelyn;
|
private const SeIconChar _defaultIcon = SeIconChar.Hyadelyn;
|
||||||
private static readonly string _defaultIconGlyph = SeIconCharExtensions.ToIconString(DefaultIcon);
|
private static readonly string _defaultIconGlyph = SeIconCharExtensions.ToIconString(_defaultIcon);
|
||||||
private static readonly Vector2 _defaultPivot = new(0.5f, 1f);
|
private static readonly Vector2 _defaultPivot = new(0.5f, 1f);
|
||||||
private uint _lastNamePlateDrawFrame;
|
private uint _lastNamePlateDrawFrame;
|
||||||
|
|
||||||
|
// / Overlay window flags
|
||||||
private const ImGuiWindowFlags _overlayFlags =
|
private const ImGuiWindowFlags _overlayFlags =
|
||||||
ImGuiWindowFlags.NoDecoration |
|
ImGuiWindowFlags.NoDecoration |
|
||||||
ImGuiWindowFlags.NoBackground |
|
ImGuiWindowFlags.NoBackground |
|
||||||
@@ -69,6 +74,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
ILogger<LightFinderPlateHandler> logger,
|
ILogger<LightFinderPlateHandler> logger,
|
||||||
IAddonLifecycle addonLifecycle,
|
IAddonLifecycle addonLifecycle,
|
||||||
IGameGui gameGui,
|
IGameGui gameGui,
|
||||||
|
IClientState clientState,
|
||||||
LightlessConfigService configService,
|
LightlessConfigService configService,
|
||||||
LightlessMediator mediator,
|
LightlessMediator mediator,
|
||||||
IObjectTable objectTable,
|
IObjectTable objectTable,
|
||||||
@@ -79,6 +85,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
_addonLifecycle = addonLifecycle;
|
_addonLifecycle = addonLifecycle;
|
||||||
_gameGui = gameGui;
|
_gameGui = gameGui;
|
||||||
|
_clientState = clientState;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
_objectTable = objectTable;
|
_objectTable = objectTable;
|
||||||
@@ -113,6 +120,9 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
_mpNameplateAddon = null;
|
_mpNameplateAddon = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enable nameplate handling.
|
||||||
|
/// </summary>
|
||||||
internal void EnableNameplate()
|
internal void EnableNameplate()
|
||||||
{
|
{
|
||||||
if (!_mEnabled)
|
if (!_mEnabled)
|
||||||
@@ -130,6 +140,9 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disable nameplate handling.
|
||||||
|
/// </summary>
|
||||||
internal void DisableNameplate()
|
internal void DisableNameplate()
|
||||||
{
|
{
|
||||||
if (_mEnabled)
|
if (_mEnabled)
|
||||||
@@ -148,8 +161,21 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draw detour for nameplate addon.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type"></param>
|
||||||
|
/// <param name="args"></param>
|
||||||
private void NameplateDrawDetour(AddonEvent type, AddonArgs args)
|
private void NameplateDrawDetour(AddonEvent type, AddonArgs args)
|
||||||
{
|
{
|
||||||
|
if (_clientState.IsGPosing)
|
||||||
|
{
|
||||||
|
ClearLabelBuffer();
|
||||||
|
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
|
||||||
|
_lastNamePlateDrawFrame = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (args.Addon.Address == nint.Zero)
|
if (args.Addon.Address == nint.Zero)
|
||||||
{
|
{
|
||||||
if (_logger.IsEnabled(LogLevel.Warning))
|
if (_logger.IsEnabled(LogLevel.Warning))
|
||||||
@@ -172,6 +198,9 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
UpdateNameplateNodes();
|
UpdateNameplateNodes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the nameplate nodes with LightFinder objects.
|
||||||
|
/// </summary>
|
||||||
private void UpdateNameplateNodes()
|
private void UpdateNameplateNodes()
|
||||||
{
|
{
|
||||||
var currentHandle = _gameGui.GetAddonByName("NamePlate");
|
var currentHandle = _gameGui.GetAddonByName("NamePlate");
|
||||||
@@ -229,7 +258,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
}
|
}
|
||||||
|
|
||||||
var visibleUserIdsSnapshot = VisibleUserIds;
|
var visibleUserIdsSnapshot = VisibleUserIds;
|
||||||
var safeCount = System.Math.Min(ui3DModule->NamePlateObjectInfoCount, vec.Length);
|
var safeCount = Math.Min(ui3DModule->NamePlateObjectInfoCount, vec.Length);
|
||||||
var currentConfig = _configService.Current;
|
var currentConfig = _configService.Current;
|
||||||
var labelColor = UIColors.Get("Lightfinder");
|
var labelColor = UIColors.Get("Lightfinder");
|
||||||
var edgeColor = UIColors.Get("LightfinderEdge");
|
var edgeColor = UIColors.Get("LightfinderEdge");
|
||||||
@@ -273,7 +302,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
var root = nameplateObject.RootComponentNode;
|
var root = nameplateObject.RootComponentNode;
|
||||||
var nameContainer = nameplateObject.NameContainer;
|
var nameContainer = nameplateObject.NameContainer;
|
||||||
var nameText = nameplateObject.NameText;
|
var nameText = nameplateObject.NameText;
|
||||||
var marker = nameplateObject.MarkerIcon;
|
|
||||||
|
|
||||||
if (root == null || root->Component == null || nameContainer == null || nameText == null)
|
if (root == null || root->Component == null || nameContainer == null || nameText == null)
|
||||||
{
|
{
|
||||||
@@ -291,6 +319,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
if (!currentConfig.LightfinderLabelShowHidden && !isNameplateVisible)
|
if (!currentConfig.LightfinderLabelShowHidden && !isNameplateVisible)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// Prepare label content and scaling
|
||||||
var scaleMultiplier = System.Math.Clamp(currentConfig.LightfinderLabelScale, 0.5f, 2.0f);
|
var scaleMultiplier = System.Math.Clamp(currentConfig.LightfinderLabelScale, 0.5f, 2.0f);
|
||||||
var baseScale = currentConfig.LightfinderLabelUseIcon ? 1.0f : 0.5f;
|
var baseScale = currentConfig.LightfinderLabelUseIcon ? 1.0f : 0.5f;
|
||||||
var effectiveScale = baseScale * scaleMultiplier;
|
var effectiveScale = baseScale * scaleMultiplier;
|
||||||
@@ -298,10 +327,10 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
var targetFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier);
|
var targetFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier);
|
||||||
var labelContent = currentConfig.LightfinderLabelUseIcon
|
var labelContent = currentConfig.LightfinderLabelUseIcon
|
||||||
? NormalizeIconGlyph(currentConfig.LightfinderLabelIconGlyph)
|
? NormalizeIconGlyph(currentConfig.LightfinderLabelIconGlyph)
|
||||||
: DefaultLabelText;
|
: _defaultLabelText;
|
||||||
|
|
||||||
if (!currentConfig.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal)))
|
if (!currentConfig.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal)))
|
||||||
labelContent = DefaultLabelText;
|
labelContent = _defaultLabelText;
|
||||||
|
|
||||||
var nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
var nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
||||||
var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
|
var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
|
||||||
@@ -344,6 +373,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
() => GetScaledTextWidth(nameText),
|
() => GetScaledTextWidth(nameText),
|
||||||
nodeWidth);
|
nodeWidth);
|
||||||
|
|
||||||
|
// Text offset caching
|
||||||
var textOffset = (int)System.Math.Round(nameText->AtkResNode.X);
|
var textOffset = (int)System.Math.Round(nameText->AtkResNode.X);
|
||||||
var hasValidOffset = TryCacheTextOffset(nameplateIndex, rawTextWidth, textOffset);
|
var hasValidOffset = TryCacheTextOffset(nameplateIndex, rawTextWidth, textOffset);
|
||||||
|
|
||||||
@@ -354,11 +384,13 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var res = nameContainer;
|
var res = nameContainer;
|
||||||
|
|
||||||
|
// X scale
|
||||||
var worldScaleX = GetWorldScaleX(res);
|
var worldScaleX = GetWorldScaleX(res);
|
||||||
if (worldScaleX <= 0f) worldScaleX = 1f;
|
if (worldScaleX <= 0f) worldScaleX = 1f;
|
||||||
|
|
||||||
|
// Y scale
|
||||||
var worldScaleY = GetWorldScaleY(res);
|
var worldScaleY = GetWorldScaleY(res);
|
||||||
if (worldScaleY <= 0f) worldScaleY = 1f;
|
if (worldScaleY <= 0f) worldScaleY = 1f;
|
||||||
|
|
||||||
@@ -368,12 +400,15 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
float finalX;
|
float finalX;
|
||||||
if (currentConfig.LightfinderAutoAlign)
|
if (currentConfig.LightfinderAutoAlign)
|
||||||
{
|
{
|
||||||
|
// auto X positioning
|
||||||
var measuredWidth = Math.Max(1, textWidth > 0 ? textWidth : nodeWidth);
|
var measuredWidth = Math.Max(1, textWidth > 0 ? textWidth : nodeWidth);
|
||||||
var measuredWidthF = (float)measuredWidth;
|
var measuredWidthF = (float)measuredWidth;
|
||||||
|
|
||||||
|
// consider icon width
|
||||||
var containerWidthLocal = res->Width > 0 ? res->Width : measuredWidthF;
|
var containerWidthLocal = res->Width > 0 ? res->Width : measuredWidthF;
|
||||||
var containerWidthScreen = containerWidthLocal * worldScaleX;
|
var containerWidthScreen = containerWidthLocal * worldScaleX;
|
||||||
|
|
||||||
|
// container bounds for positions
|
||||||
var containerLeft = res->ScreenX;
|
var containerLeft = res->ScreenX;
|
||||||
var containerRight = containerLeft + containerWidthScreen;
|
var containerRight = containerLeft + containerWidthScreen;
|
||||||
var containerCenter = containerLeft + (containerWidthScreen * 0.5f);
|
var containerCenter = containerLeft + (containerWidthScreen * 0.5f);
|
||||||
@@ -384,6 +419,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
|
|
||||||
var offsetXScreen = currentConfig.LightfinderLabelOffsetX * worldScaleX;
|
var offsetXScreen = currentConfig.LightfinderLabelOffsetX * worldScaleX;
|
||||||
|
|
||||||
|
// alignment based on config
|
||||||
switch (currentConfig.LabelAlignment)
|
switch (currentConfig.LabelAlignment)
|
||||||
{
|
{
|
||||||
case LabelAlignment.Left:
|
case LabelAlignment.Left:
|
||||||
@@ -402,6 +438,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// manual X positioning
|
||||||
var cachedTextOffset = _buffers.TextOffsets[nameplateIndex];
|
var cachedTextOffset = _buffers.TextOffsets[nameplateIndex];
|
||||||
var hasCachedOffset = cachedTextOffset != int.MinValue;
|
var hasCachedOffset = cachedTextOffset != int.MinValue;
|
||||||
var baseOffsetXLocal = (!currentConfig.LightfinderLabelUseIcon && hasValidOffset && hasCachedOffset)
|
var baseOffsetXLocal = (!currentConfig.LightfinderLabelUseIcon && hasValidOffset && hasCachedOffset)
|
||||||
@@ -419,6 +456,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
|
|
||||||
alignment = (AlignmentType)Math.Clamp((int)alignment, 0, 8);
|
alignment = (AlignmentType)Math.Clamp((int)alignment, 0, 8);
|
||||||
|
|
||||||
|
// final position before smoothing
|
||||||
var finalPosition = new Vector2(finalX, res->ScreenY + positionYScreen);
|
var finalPosition = new Vector2(finalX, res->ScreenY + positionYScreen);
|
||||||
var dpiScale = ImGui.GetIO().DisplayFramebufferScale.X; // often same for Y
|
var dpiScale = ImGui.GetIO().DisplayFramebufferScale.X; // often same for Y
|
||||||
var fw = Framework.Instance();
|
var fw = Framework.Instance();
|
||||||
@@ -429,6 +467,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
finalPosition = SmoothPosition(nameplateIndex, finalPosition, dt);
|
finalPosition = SmoothPosition(nameplateIndex, finalPosition, dt);
|
||||||
finalPosition = SnapToPixels(finalPosition, dpiScale);
|
finalPosition = SnapToPixels(finalPosition, dpiScale);
|
||||||
|
|
||||||
|
// prepare label info
|
||||||
var pivot = (currentConfig.LightfinderAutoAlign || currentConfig.LightfinderLabelUseIcon)
|
var pivot = (currentConfig.LightfinderAutoAlign || currentConfig.LightfinderLabelUseIcon)
|
||||||
? AlignmentToPivot(alignment)
|
? AlignmentToPivot(alignment)
|
||||||
: _defaultPivot;
|
: _defaultPivot;
|
||||||
@@ -459,6 +498,9 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// On each tick, process any needed updates for the UI Builder.
|
||||||
|
/// </summary>
|
||||||
private void OnUiBuilderDraw()
|
private void OnUiBuilderDraw()
|
||||||
{
|
{
|
||||||
if (!_mEnabled)
|
if (!_mEnabled)
|
||||||
@@ -468,6 +510,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
if (fw == null)
|
if (fw == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Frame skip check
|
||||||
var frame = fw->FrameCounter;
|
var frame = fw->FrameCounter;
|
||||||
|
|
||||||
if (_lastNamePlateDrawFrame == 0 || (frame - _lastNamePlateDrawFrame) > 1)
|
if (_lastNamePlateDrawFrame == 0 || (frame - _lastNamePlateDrawFrame) > 1)
|
||||||
@@ -478,6 +521,16 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Gpose Check
|
||||||
|
if (_clientState.IsGPosing)
|
||||||
|
{
|
||||||
|
ClearLabelBuffer();
|
||||||
|
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
|
||||||
|
_lastNamePlateDrawFrame = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If nameplate addon is not visible, skip rendering
|
||||||
if (!IsNamePlateAddonVisible())
|
if (!IsNamePlateAddonVisible())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -506,6 +559,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
_uiRects.Clear();
|
_uiRects.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Needed for imgui overlay viewport for the multi window view.
|
||||||
var vp = ImGui.GetMainViewport();
|
var vp = ImGui.GetMainViewport();
|
||||||
var vpPos = vp.Pos;
|
var vpPos = vp.Pos;
|
||||||
|
|
||||||
@@ -532,6 +586,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
{
|
{
|
||||||
ref var info = ref _buffers.LabelCopy[i];
|
ref var info = ref _buffers.LabelCopy[i];
|
||||||
|
|
||||||
|
// final draw position with viewport offset
|
||||||
var drawPos = info.ScreenPosition + vpPos;
|
var drawPos = info.ScreenPosition + vpPos;
|
||||||
var font = default(ImFontPtr);
|
var font = default(ImFontPtr);
|
||||||
if (info.UseIcon)
|
if (info.UseIcon)
|
||||||
@@ -547,21 +602,25 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
if (!font.IsNull)
|
if (!font.IsNull)
|
||||||
ImGui.PushFont(font);
|
ImGui.PushFont(font);
|
||||||
|
|
||||||
|
// calculate size for occlusion checking
|
||||||
var baseSize = ImGui.CalcTextSize(info.Text);
|
var baseSize = ImGui.CalcTextSize(info.Text);
|
||||||
var baseFontSize = ImGui.GetFontSize();
|
var baseFontSize = ImGui.GetFontSize();
|
||||||
|
|
||||||
if (!font.IsNull)
|
if (!font.IsNull)
|
||||||
ImGui.PopFont();
|
ImGui.PopFont();
|
||||||
|
|
||||||
|
// scale size based on font size
|
||||||
var scale = baseFontSize > 0 ? (info.FontSize / baseFontSize) : 1f;
|
var scale = baseFontSize > 0 ? (info.FontSize / baseFontSize) : 1f;
|
||||||
var size = baseSize * scale;
|
var size = baseSize * scale;
|
||||||
|
|
||||||
|
// label rect for occlusion checking
|
||||||
var topLeft = info.ScreenPosition - new Vector2(size.X * info.Pivot.X, size.Y * info.Pivot.Y);
|
var topLeft = info.ScreenPosition - new Vector2(size.X * info.Pivot.X, size.Y * info.Pivot.Y);
|
||||||
var labelRect = new RectF(topLeft.X, topLeft.Y, topLeft.X + size.X, topLeft.Y + size.Y);
|
var labelRect = new RectF(topLeft.X, topLeft.Y, topLeft.X + size.X, topLeft.Y + size.Y);
|
||||||
|
|
||||||
|
// occlusion check
|
||||||
if (IsOccludedByAnyUi(labelRect))
|
if (IsOccludedByAnyUi(labelRect))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
drawList.AddScreenText(drawPos, info.Text, info.TextColor, info.FontSize, info.Pivot, info.EdgeColor, font);
|
drawList.AddScreenText(drawPos, info.Text, info.TextColor, info.FontSize, info.Pivot, info.EdgeColor, font);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -580,10 +639,10 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
|
|
||||||
private static uint PackColor(Vector4 color)
|
private static uint PackColor(Vector4 color)
|
||||||
{
|
{
|
||||||
var r = (byte)System.Math.Clamp(color.X * 255f, 0f, 255f);
|
var r = (byte)Math.Clamp(color.X * 255f, 0f, 255f);
|
||||||
var g = (byte)System.Math.Clamp(color.Y * 255f, 0f, 255f);
|
var g = (byte)Math.Clamp(color.Y * 255f, 0f, 255f);
|
||||||
var b = (byte)System.Math.Clamp(color.Z * 255f, 0f, 255f);
|
var b = (byte)Math.Clamp(color.Z * 255f, 0f, 255f);
|
||||||
var a = (byte)System.Math.Clamp(color.W * 255f, 0f, 255f);
|
var a = (byte)Math.Clamp(color.W * 255f, 0f, 255f);
|
||||||
return (uint)((a << 24) | (b << 16) | (g << 8) | r);
|
return (uint)((a << 24) | (b << 16) | (g << 8) | r);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -629,10 +688,19 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
if (scale <= 0f)
|
if (scale <= 0f)
|
||||||
scale = 1f;
|
scale = 1f;
|
||||||
|
|
||||||
var computed = (int)System.Math.Round(rawWidth * scale);
|
var computed = (int)Math.Round(rawWidth * scale);
|
||||||
return System.Math.Max(1, computed);
|
return Math.Max(1, computed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves a cached value for the given index.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cache"></param>
|
||||||
|
/// <param name="index"></param>
|
||||||
|
/// <param name="rawValue"></param>
|
||||||
|
/// <param name="fallback"></param>
|
||||||
|
/// <param name="fallbackWhenZero"></param>
|
||||||
|
/// <returns></returns>
|
||||||
private static int ResolveCache(
|
private static int ResolveCache(
|
||||||
int[] cache,
|
int[] cache,
|
||||||
int index,
|
int index,
|
||||||
@@ -660,7 +728,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
|
|
||||||
private bool TryCacheTextOffset(int nameplateIndex, int measuredTextWidth, int textOffset)
|
private bool TryCacheTextOffset(int nameplateIndex, int measuredTextWidth, int textOffset)
|
||||||
{
|
{
|
||||||
if (System.Math.Abs(measuredTextWidth) > 0 || textOffset != 0)
|
if (Math.Abs(measuredTextWidth) > 0 || textOffset != 0)
|
||||||
{
|
{
|
||||||
_buffers.TextOffsets[nameplateIndex] = textOffset;
|
_buffers.TextOffsets[nameplateIndex] = textOffset;
|
||||||
return true;
|
return true;
|
||||||
@@ -669,6 +737,12 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Snapping a position to pixel grid based on DPI scale.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="p">Position</param>
|
||||||
|
/// <param name="dpiScale">DPI Scale</param>
|
||||||
|
/// <returns></returns>
|
||||||
private static Vector2 SnapToPixels(Vector2 p, float dpiScale)
|
private static Vector2 SnapToPixels(Vector2 p, float dpiScale)
|
||||||
{
|
{
|
||||||
// snap to pixel grid
|
// snap to pixel grid
|
||||||
@@ -677,6 +751,15 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
return new Vector2(x, y);
|
return new Vector2(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Smooths the position using exponential smoothing.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idx">Nameplate Index</param>
|
||||||
|
/// <param name="target">Final position</param>
|
||||||
|
/// <param name="dt">Delta Time</param>
|
||||||
|
/// <param name="responsiveness">How responssive the smooting should be</param>
|
||||||
|
/// <returns></returns>
|
||||||
private Vector2 SmoothPosition(int idx, Vector2 target, float dt, float responsiveness = 24f)
|
private Vector2 SmoothPosition(int idx, Vector2 target, float dt, float responsiveness = 24f)
|
||||||
{
|
{
|
||||||
// exponential smoothing
|
// exponential smoothing
|
||||||
@@ -687,56 +770,78 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get current smoothed position
|
||||||
var cur = _buffers.SmoothedPos[idx];
|
var cur = _buffers.SmoothedPos[idx];
|
||||||
|
|
||||||
// compute smoothing factor
|
// compute smoothing factor
|
||||||
var a = 1f - MathF.Exp(-responsiveness * dt);
|
var a = 1f - MathF.Exp(-responsiveness * dt);
|
||||||
|
|
||||||
|
// snap if close enough
|
||||||
if (Vector2.DistanceSquared(cur, target) < 0.25f)
|
if (Vector2.DistanceSquared(cur, target) < 0.25f)
|
||||||
return cur;
|
return cur;
|
||||||
|
|
||||||
|
// lerp towards target
|
||||||
cur = Vector2.Lerp(cur, target, a);
|
cur = Vector2.Lerp(cur, target, a);
|
||||||
_buffers.SmoothedPos[idx] = cur;
|
_buffers.SmoothedPos[idx] = cur;
|
||||||
return cur;
|
return cur;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to get a valid screen rect for the given addon.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="addon">Addon UI</param>
|
||||||
|
/// <param name="screen">Screen positioning/param>
|
||||||
|
/// <param name="rect">RectF of Addon</param>
|
||||||
|
/// <returns></returns>
|
||||||
private static bool TryGetAddonRect(AtkUnitBase* addon, Vector2 screen, out RectF rect)
|
private static bool TryGetAddonRect(AtkUnitBase* addon, Vector2 screen, out RectF rect)
|
||||||
{
|
{
|
||||||
// validate addon visibility and size
|
// Addon existence
|
||||||
rect = default;
|
rect = default;
|
||||||
if (addon == null)
|
if (addon == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// Visibility check
|
||||||
var root = addon->RootNode;
|
var root = addon->RootNode;
|
||||||
if (root == null || !root->IsVisible())
|
if (root == null || !root->IsVisible())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// Size check
|
||||||
float w = root->Width;
|
float w = root->Width;
|
||||||
float h = root->Height;
|
float h = root->Height;
|
||||||
|
|
||||||
if (w <= 0 || h <= 0)
|
if (w <= 0 || h <= 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// apply scale
|
// Local scale
|
||||||
var sx = root->ScaleX; if (sx <= 0f) sx = 1f;
|
float sx = root->ScaleX; if (sx <= 0f) sx = 1f;
|
||||||
var sy = root->ScaleY; if (sy <= 0f) sy = 1f;
|
float sy = root->ScaleY; if (sy <= 0f) sy = 1f;
|
||||||
|
|
||||||
w *= sx;
|
// World/composed scale from Transform
|
||||||
h *= sy;
|
float wsx = GetWorldScaleX(root);
|
||||||
|
float wsy = GetWorldScaleY(root);
|
||||||
|
if (wsx <= 0f) wsx = 1f;
|
||||||
|
if (wsy <= 0f) wsy = 1f;
|
||||||
|
|
||||||
|
// World scale may include parent scaling; use it if meaningfully different.
|
||||||
|
float useX = MathF.Abs(wsx - sx) > 0.01f ? wsx : sx;
|
||||||
|
float useY = MathF.Abs(wsy - sy) > 0.01f ? wsy : sy;
|
||||||
|
|
||||||
|
w *= useX;
|
||||||
|
h *= useY;
|
||||||
|
|
||||||
if (w < 4f || h < 4f)
|
if (w < 4f || h < 4f)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// compute screen rect
|
// Screen coords
|
||||||
float l = root->ScreenX;
|
float l = root->ScreenX;
|
||||||
float t = root->ScreenY;
|
float t = root->ScreenY;
|
||||||
float r = l + w;
|
float r = l + w;
|
||||||
float b = t + h;
|
float b = t + h;
|
||||||
|
|
||||||
// cull too large or out-of-bounds rects
|
// Drop fullscreen-ish / insane rects
|
||||||
if (w >= screen.X * 0.98f && h >= screen.Y * 0.98f)
|
if (w >= screen.X * 0.98f && h >= screen.Y * 0.98f)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// Drop offscreen rects
|
||||||
if (l < -screen.X || t < -screen.Y || r > screen.X * 2f || b > screen.Y * 2f)
|
if (l < -screen.X || t < -screen.Y || r > screen.X * 2f || b > screen.Y * 2f)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -744,6 +849,10 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Refreshes the cached UI rects for occlusion checking.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="unitMgr">Unit Manager</param>
|
||||||
private void RefreshUiRects(RaptureAtkUnitManager* unitMgr)
|
private void RefreshUiRects(RaptureAtkUnitManager* unitMgr)
|
||||||
{
|
{
|
||||||
_uiRects.Clear();
|
_uiRects.Clear();
|
||||||
@@ -769,6 +878,11 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is the given label rect occluded by any UI rects?
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="labelRect">UI/Label Rect</param>
|
||||||
|
/// <returns>Is occluded or not</returns>
|
||||||
private bool IsOccludedByAnyUi(RectF labelRect)
|
private bool IsOccludedByAnyUi(RectF labelRect)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < _uiRects.Count; i++)
|
for (int i = 0; i < _uiRects.Count; i++)
|
||||||
@@ -779,18 +893,33 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the world scale X of the given node.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="n">Node</param>
|
||||||
|
/// <returns>World Scale of node</returns>
|
||||||
private static float GetWorldScaleX(AtkResNode* n)
|
private static float GetWorldScaleX(AtkResNode* n)
|
||||||
{
|
{
|
||||||
var t = n->Transform;
|
var t = n->Transform;
|
||||||
return MathF.Sqrt(t.M11 * t.M11 + t.M12 * t.M12);
|
return MathF.Sqrt(t.M11 * t.M11 + t.M12 * t.M12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the world scale Y of the given node.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="n">Node</param>
|
||||||
|
/// <returns>World Scale of node</returns>
|
||||||
private static float GetWorldScaleY(AtkResNode* n)
|
private static float GetWorldScaleY(AtkResNode* n)
|
||||||
{
|
{
|
||||||
var t = n->Transform;
|
var t = n->Transform;
|
||||||
return MathF.Sqrt(t.M21 * t.M21 + t.M22 * t.M22);
|
return MathF.Sqrt(t.M21 * t.M21 + t.M22 * t.M22);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Normalize an icon glyph input into a valid string.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="rawInput">Raw glyph input</param>
|
||||||
|
/// <returns>Normalized glyph input</returns>
|
||||||
internal static string NormalizeIconGlyph(string? rawInput)
|
internal static string NormalizeIconGlyph(string? rawInput)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(rawInput))
|
if (string.IsNullOrWhiteSpace(rawInput))
|
||||||
@@ -814,6 +943,11 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
|
|
||||||
return _defaultIconGlyph;
|
return _defaultIconGlyph;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is the nameplate addon visible?
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Is it visible?</returns>
|
||||||
private bool IsNamePlateAddonVisible()
|
private bool IsNamePlateAddonVisible()
|
||||||
{
|
{
|
||||||
if (_mpNameplateAddon == null)
|
if (_mpNameplateAddon == null)
|
||||||
@@ -823,6 +957,11 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
return root != null && root->IsVisible();
|
return root != null && root->IsVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts raw icon glyph input into an icon editor string.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="rawInput">Raw icon glyph input</param>
|
||||||
|
/// <returns>Icon editor string</returns>
|
||||||
internal static string ToIconEditorString(string? rawInput)
|
internal static string ToIconEditorString(string? rawInput)
|
||||||
{
|
{
|
||||||
var normalized = NormalizeIconGlyph(rawInput);
|
var normalized = NormalizeIconGlyph(rawInput);
|
||||||
@@ -831,6 +970,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
? runeEnumerator.Current.Value.ToString("X4", CultureInfo.InvariantCulture)
|
? runeEnumerator.Current.Value.ToString("X4", CultureInfo.InvariantCulture)
|
||||||
: _defaultIconGlyph;
|
: _defaultIconGlyph;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly struct NameplateLabelInfo
|
private readonly struct NameplateLabelInfo
|
||||||
{
|
{
|
||||||
public NameplateLabelInfo(
|
public NameplateLabelInfo(
|
||||||
@@ -860,6 +1000,9 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
public bool UseIcon { get; }
|
public bool UseIcon { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Visible paired user IDs snapshot.
|
||||||
|
/// </summary>
|
||||||
private HashSet<ulong> VisibleUserIds
|
private HashSet<ulong> VisibleUserIds
|
||||||
=> [.. _pairUiService.GetSnapshot().PairsByUid.Values
|
=> [.. _pairUiService.GetSnapshot().PairsByUid.Values
|
||||||
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
||||||
@@ -879,6 +1022,10 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update the active broadcasting CIDs.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cids">Inbound new CIDs</param>
|
||||||
public void UpdateBroadcastingCids(IEnumerable<string> cids)
|
public void UpdateBroadcastingCids(IEnumerable<string> cids)
|
||||||
{
|
{
|
||||||
var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal);
|
var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal);
|
||||||
@@ -891,6 +1038,9 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
FlagRefresh();
|
FlagRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears all nameplate related caches.
|
||||||
|
/// </summary>
|
||||||
public void ClearNameplateCaches()
|
public void ClearNameplateCaches()
|
||||||
{
|
{
|
||||||
_buffers.Clear();
|
_buffers.Clear();
|
||||||
@@ -929,18 +1079,31 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the LightFinder Plate Handler.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation Token</param>
|
||||||
|
/// <returns>Task Completed</returns>
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Init();
|
Init();
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops the LightFinder Plate Handler.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation Token</param>
|
||||||
|
/// <returns>Task Completed</returns>
|
||||||
public Task StopAsync(CancellationToken cancellationToken)
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Uninit();
|
Uninit();
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rectangle with float coordinates for intersection testing.
|
||||||
|
/// </summary>
|
||||||
[StructLayout(LayoutKind.Auto)]
|
[StructLayout(LayoutKind.Auto)]
|
||||||
private readonly struct RectF
|
private readonly struct RectF
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user