various 'improvements'
This commit is contained in:
@@ -1,249 +1,692 @@
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.Addon.Lifecycle;
|
||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
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.Services.Rendering;
|
||||
using LightlessSync.UI;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using LightlessSync.UI.Services;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.UtilsEnum.Enum;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Pictomancy;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace LightlessSync.Services.LightFinder
|
||||
namespace LightlessSync.Services.LightFinder;
|
||||
|
||||
public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscriber
|
||||
{
|
||||
public class LightFinderPlateHandler : IHostedService, IMediatorSubscriber
|
||||
private readonly ILogger<LightFinderPlateHandler> _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 readonly IUiBuilder _uiBuilder;
|
||||
private bool _mEnabled;
|
||||
private bool _needsLabelRefresh;
|
||||
private bool _drawSubscribed;
|
||||
private AddonNamePlate* _mpNameplateAddon;
|
||||
private readonly object _labelLock = new();
|
||||
private readonly NameplateBuffers _buffers = new();
|
||||
private int _labelRenderCount;
|
||||
|
||||
private const string DefaultLabelText = "LightFinder";
|
||||
private const SeIconChar DefaultIcon = SeIconChar.Hyadelyn;
|
||||
private static readonly string DefaultIconGlyph = SeIconCharExtensions.ToIconString(DefaultIcon);
|
||||
private static readonly Vector2 DefaultPivot = new(0.5f, 1f);
|
||||
|
||||
private ImmutableHashSet<string> _activeBroadcastingCids = [];
|
||||
|
||||
public LightFinderPlateHandler(
|
||||
ILogger<LightFinderPlateHandler> logger,
|
||||
IAddonLifecycle addonLifecycle,
|
||||
IGameGui gameGui,
|
||||
LightlessConfigService configService,
|
||||
LightlessMediator mediator,
|
||||
IObjectTable objectTable,
|
||||
PairUiService pairUiService,
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
PictomancyService pictomancyService)
|
||||
{
|
||||
private readonly ILogger<LightFinderPlateHandler> _logger;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly IDalamudPluginInterface _pluginInterface;
|
||||
private readonly IObjectTable _gameObjects;
|
||||
private readonly IGameGui _gameGui;
|
||||
_logger = logger;
|
||||
_addonLifecycle = addonLifecycle;
|
||||
_gameGui = gameGui;
|
||||
_configService = configService;
|
||||
_mediator = mediator;
|
||||
_objectTable = objectTable;
|
||||
_pairUiService = pairUiService;
|
||||
_uiBuilder = pluginInterface.UiBuilder ?? throw new ArgumentNullException(nameof(pluginInterface));
|
||||
_ = pictomancyService ?? throw new ArgumentNullException(nameof(pictomancyService));
|
||||
|
||||
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)
|
||||
internal void Init()
|
||||
{
|
||||
if (!_drawSubscribed)
|
||||
{
|
||||
_logger = logger;
|
||||
Mediator = mediator;
|
||||
_pluginInterface = dalamudPluginInterface;
|
||||
_configService = configService;
|
||||
_gameObjects = gameObjects;
|
||||
_gameGui = gameGui;
|
||||
_uiBuilder.Draw += OnUiBuilderDraw;
|
||||
_drawSubscribed = true;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
EnableNameplate();
|
||||
_mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick);
|
||||
}
|
||||
|
||||
internal void Uninit()
|
||||
{
|
||||
DisableNameplate();
|
||||
if (_drawSubscribed)
|
||||
{
|
||||
_logger.LogInformation("Starting LightFinderPlateHandler...");
|
||||
|
||||
_pluginInterface.UiBuilder.Draw += OnDraw;
|
||||
|
||||
_logger.LogInformation("LightFinderPlateHandler started.");
|
||||
return Task.CompletedTask;
|
||||
_uiBuilder.Draw -= OnUiBuilderDraw;
|
||||
_drawSubscribed = false;
|
||||
}
|
||||
ClearLabelBuffer();
|
||||
_mediator.Unsubscribe<PriorityFrameworkUpdateMessage>(this);
|
||||
_mpNameplateAddon = null;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
internal void EnableNameplate()
|
||||
{
|
||||
if (!_mEnabled)
|
||||
{
|
||||
_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>())
|
||||
try
|
||||
{
|
||||
//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); */
|
||||
_addonLifecycle.RegisterListener(AddonEvent.PostDraw, "NamePlate", NameplateDrawDetour);
|
||||
_mEnabled = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Unknown error while trying to enable nameplate.");
|
||||
DisableNameplate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
internal void DisableNameplate()
|
||||
{
|
||||
if (_mEnabled)
|
||||
{
|
||||
var local = _gameObjects.LocalPlayer;
|
||||
if (local == null)
|
||||
return 32.1f;
|
||||
try
|
||||
{
|
||||
_addonLifecycle.UnregisterListener(NameplateDrawDetour);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Unknown error while unregistering nameplate listener.");
|
||||
}
|
||||
|
||||
var delta = player.Position - local.Position;
|
||||
var dist = MathF.Sqrt(delta.X * delta.X + delta.Z * delta.Z);
|
||||
_mEnabled = false;
|
||||
ClearNameplateCaches();
|
||||
}
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
private bool TryGetApproxNameplateScreenPos(IPlayerCharacter player, out Vector2 screenPos)
|
||||
var pNameplateAddon = (AddonNamePlate*)args.Addon.Address;
|
||||
|
||||
if (_mpNameplateAddon != pNameplateAddon)
|
||||
{
|
||||
screenPos = default;
|
||||
ClearNameplateCaches();
|
||||
_mpNameplateAddon = pNameplateAddon;
|
||||
}
|
||||
|
||||
var worldPos = player.Position;
|
||||
UpdateNameplateNodes();
|
||||
}
|
||||
|
||||
var visualHeight = GetVisualHeight(player);
|
||||
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.");
|
||||
ClearLabelBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
worldPos.Y += (visualHeight + 1.2f) + _defaultHeightOffset;
|
||||
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;
|
||||
}
|
||||
|
||||
if (!_gameGui.WorldToScreen(worldPos, out var raw))
|
||||
return false;
|
||||
var framework = Framework.Instance();
|
||||
if (framework == null)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("Framework instance unavailable during nameplate update, skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
screenPos = raw;
|
||||
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)
|
||||
{
|
||||
ClearLabelBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
var visibleUserIdsSnapshot = VisibleUserIds;
|
||||
var safeCount = System.Math.Min(ui3DModule->NamePlateObjectInfoCount, vec.Length);
|
||||
var currentConfig = _configService.Current;
|
||||
var labelColor = UIColors.Get("Lightfinder");
|
||||
var edgeColor = UIColors.Get("LightfinderEdge");
|
||||
var scratchCount = 0;
|
||||
|
||||
for (int i = 0; i < safeCount; ++i)
|
||||
{
|
||||
var objectInfoPtr = vec[i];
|
||||
if (objectInfoPtr == null)
|
||||
continue;
|
||||
|
||||
var objectInfo = objectInfoPtr.Value;
|
||||
if (objectInfo == null || objectInfo->GameObject == null)
|
||||
continue;
|
||||
|
||||
var nameplateIndex = objectInfo->NamePlateIndex;
|
||||
if (nameplateIndex < 0 || nameplateIndex >= AddonNamePlate.NumNamePlateObjects)
|
||||
continue;
|
||||
|
||||
var gameObject = objectInfo->GameObject;
|
||||
if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player)
|
||||
continue;
|
||||
|
||||
// CID gating
|
||||
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject);
|
||||
if (cid == null || !_activeBroadcastingCids.Contains(cid))
|
||||
continue;
|
||||
|
||||
var local = _objectTable.LocalPlayer;
|
||||
if (!currentConfig.LightfinderLabelShowOwn && local != null &&
|
||||
objectInfo->GameObject->GetGameObjectId() == local.GameObjectId)
|
||||
continue;
|
||||
|
||||
var hidePaired = !currentConfig.LightfinderLabelShowPaired;
|
||||
var goId = gameObject->GetGameObjectId();
|
||||
if (hidePaired && visibleUserIdsSnapshot.Contains(goId))
|
||||
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)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("Nameplate {Index} missing required nodes during update, skipping.", nameplateIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
root->Component->UldManager.UpdateDrawNodeList();
|
||||
|
||||
bool isVisible =
|
||||
(marker != null && marker->AtkResNode.IsVisible()) ||
|
||||
(nameContainer->IsVisible() && nameText->AtkResNode.IsVisible()) ||
|
||||
currentConfig.LightfinderLabelShowHidden;
|
||||
|
||||
if (!isVisible)
|
||||
continue;
|
||||
|
||||
var scaleMultiplier = System.Math.Clamp(currentConfig.LightfinderLabelScale, 0.5f, 2.0f);
|
||||
var baseScale = currentConfig.LightfinderLabelUseIcon ? 1.0f : 0.5f;
|
||||
var effectiveScale = baseScale * scaleMultiplier;
|
||||
var baseFontSize = currentConfig.LightfinderLabelUseIcon ? 36f : 24f;
|
||||
var targetFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier);
|
||||
var labelContent = currentConfig.LightfinderLabelUseIcon
|
||||
? NormalizeIconGlyph(currentConfig.LightfinderLabelIconGlyph)
|
||||
: DefaultLabelText;
|
||||
|
||||
if (!currentConfig.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal)))
|
||||
labelContent = DefaultLabelText;
|
||||
|
||||
var nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
||||
var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
|
||||
AlignmentType alignment;
|
||||
|
||||
var textScaleY = nameText->AtkResNode.ScaleY;
|
||||
if (textScaleY <= 0f)
|
||||
textScaleY = 1f;
|
||||
|
||||
var blockHeight = ResolveCache(
|
||||
_buffers.TextHeights,
|
||||
nameplateIndex,
|
||||
System.Math.Abs((int)nameplateObject.TextH),
|
||||
() => GetScaledTextHeight(nameText),
|
||||
nodeHeight);
|
||||
|
||||
var containerHeight = ResolveCache(
|
||||
_buffers.ContainerHeights,
|
||||
nameplateIndex,
|
||||
(int)nameContainer->Height,
|
||||
() =>
|
||||
{
|
||||
var computed = blockHeight + (int)System.Math.Round(8 * textScaleY);
|
||||
return computed <= blockHeight ? blockHeight + 1 : computed;
|
||||
},
|
||||
blockHeight + 1);
|
||||
|
||||
var blockTop = containerHeight - blockHeight;
|
||||
if (blockTop < 0)
|
||||
blockTop = 0;
|
||||
var verticalPadding = (int)System.Math.Round(4 * effectiveScale);
|
||||
|
||||
var positionY = blockTop - verticalPadding;
|
||||
|
||||
var rawTextWidth = (int)nameplateObject.TextW;
|
||||
var textWidth = ResolveCache(
|
||||
_buffers.TextWidths,
|
||||
nameplateIndex,
|
||||
System.Math.Abs(rawTextWidth),
|
||||
() => GetScaledTextWidth(nameText),
|
||||
nodeWidth);
|
||||
|
||||
var textOffset = (int)System.Math.Round(nameText->AtkResNode.X);
|
||||
var hasValidOffset = TryCacheTextOffset(nameplateIndex, rawTextWidth, textOffset);
|
||||
|
||||
if (nameContainer == null)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("Nameplate {Index} container became unavailable during update, skipping.", nameplateIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
float finalX;
|
||||
if (currentConfig.LightfinderAutoAlign)
|
||||
{
|
||||
var measuredWidth = System.Math.Max(1, textWidth > 0 ? textWidth : nodeWidth);
|
||||
var measuredWidthF = (float)measuredWidth;
|
||||
var alignmentType = currentConfig.LabelAlignment;
|
||||
|
||||
var containerScale = nameContainer->ScaleX;
|
||||
if (containerScale <= 0f)
|
||||
containerScale = 1f;
|
||||
var containerWidthRaw = (float)nameContainer->Width;
|
||||
if (containerWidthRaw <= 0f)
|
||||
containerWidthRaw = measuredWidthF;
|
||||
var containerWidth = containerWidthRaw * containerScale;
|
||||
if (containerWidth <= 0f)
|
||||
containerWidth = measuredWidthF;
|
||||
|
||||
var containerLeft = nameContainer->ScreenX;
|
||||
var containerRight = containerLeft + containerWidth;
|
||||
var containerCenter = containerLeft + (containerWidth * 0.5f);
|
||||
|
||||
var iconMargin = currentConfig.LightfinderLabelUseIcon
|
||||
? System.Math.Min(containerWidth * 0.1f, 14f * containerScale)
|
||||
: 0f;
|
||||
|
||||
switch (alignmentType)
|
||||
{
|
||||
case LabelAlignment.Left:
|
||||
finalX = containerLeft + iconMargin;
|
||||
alignment = AlignmentType.BottomLeft;
|
||||
break;
|
||||
case LabelAlignment.Right:
|
||||
finalX = containerRight - iconMargin;
|
||||
alignment = AlignmentType.BottomRight;
|
||||
break;
|
||||
default:
|
||||
finalX = containerCenter;
|
||||
alignment = AlignmentType.Bottom;
|
||||
break;
|
||||
}
|
||||
|
||||
finalX += currentConfig.LightfinderLabelOffsetX;
|
||||
}
|
||||
else
|
||||
{
|
||||
var cachedTextOffset = _buffers.TextOffsets[nameplateIndex];
|
||||
var hasCachedOffset = cachedTextOffset != int.MinValue;
|
||||
var baseOffsetX = (!currentConfig.LightfinderLabelUseIcon && hasValidOffset && hasCachedOffset) ? cachedTextOffset : 0;
|
||||
finalX = nameContainer->ScreenX + baseOffsetX + 58 + currentConfig.LightfinderLabelOffsetX;
|
||||
alignment = AlignmentType.Bottom;
|
||||
}
|
||||
|
||||
positionY += currentConfig.LightfinderLabelOffsetY;
|
||||
alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8);
|
||||
|
||||
var finalPosition = new Vector2(finalX, nameContainer->ScreenY + positionY);
|
||||
var pivot = (currentConfig.LightfinderAutoAlign || currentConfig.LightfinderLabelUseIcon)
|
||||
? AlignmentToPivot(alignment)
|
||||
: DefaultPivot;
|
||||
var textColorPacked = PackColor(labelColor);
|
||||
var edgeColorPacked = PackColor(edgeColor);
|
||||
|
||||
_buffers.LabelScratch[scratchCount++] = new NameplateLabelInfo(
|
||||
finalPosition,
|
||||
labelContent,
|
||||
textColorPacked,
|
||||
edgeColorPacked,
|
||||
targetFontSize,
|
||||
pivot,
|
||||
currentConfig.LightfinderLabelUseIcon);
|
||||
}
|
||||
|
||||
lock (_labelLock)
|
||||
{
|
||||
if (scratchCount == 0)
|
||||
{
|
||||
_labelRenderCount = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Array.Copy(_buffers.LabelScratch, _buffers.LabelRender, scratchCount);
|
||||
_labelRenderCount = scratchCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUiBuilderDraw()
|
||||
{
|
||||
if (!_mEnabled)
|
||||
return;
|
||||
|
||||
int copyCount;
|
||||
lock (_labelLock)
|
||||
{
|
||||
copyCount = _labelRenderCount;
|
||||
if (copyCount == 0)
|
||||
return;
|
||||
|
||||
Array.Copy(_buffers.LabelRender, _buffers.LabelCopy, copyCount);
|
||||
}
|
||||
|
||||
using var drawList = PictoService.Draw();
|
||||
if (drawList == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < copyCount; ++i)
|
||||
{
|
||||
ref var info = ref _buffers.LabelCopy[i];
|
||||
var font = default(ImFontPtr);
|
||||
if (info.UseIcon)
|
||||
{
|
||||
var ioFonts = ImGui.GetIO().Fonts;
|
||||
font = ioFonts.Fonts.Size > 1 ? new ImFontPtr(ioFonts.Fonts[1]) : ImGui.GetFont();
|
||||
}
|
||||
|
||||
drawList.AddScreenText(info.ScreenPosition, info.Text, info.TextColor, info.FontSize, info.Pivot, info.EdgeColor, font);
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector2 AlignmentToPivot(AlignmentType alignment) => alignment switch
|
||||
{
|
||||
AlignmentType.BottomLeft => new Vector2(0f, 1f),
|
||||
AlignmentType.BottomRight => new Vector2(1f, 1f),
|
||||
AlignmentType.TopLeft => new Vector2(0f, 0f),
|
||||
AlignmentType.TopRight => new Vector2(1f, 0f),
|
||||
AlignmentType.Top => new Vector2(0.5f, 0f),
|
||||
AlignmentType.Left => new Vector2(0f, 0.5f),
|
||||
AlignmentType.Right => new Vector2(1f, 0.5f),
|
||||
_ => DefaultPivot
|
||||
};
|
||||
|
||||
private static uint PackColor(Vector4 color)
|
||||
{
|
||||
var r = (byte)System.Math.Clamp(color.X * 255f, 0f, 255f);
|
||||
var g = (byte)System.Math.Clamp(color.Y * 255f, 0f, 255f);
|
||||
var b = (byte)System.Math.Clamp(color.Z * 255f, 0f, 255f);
|
||||
var a = (byte)System.Math.Clamp(color.W * 255f, 0f, 255f);
|
||||
return (uint)((a << 24) | (b << 16) | (g << 8) | r);
|
||||
}
|
||||
|
||||
private void ClearLabelBuffer()
|
||||
{
|
||||
lock (_labelLock)
|
||||
{
|
||||
_labelRenderCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private static int ResolveCache(
|
||||
int[] cache,
|
||||
int index,
|
||||
int rawValue,
|
||||
Func<int> fallback,
|
||||
int fallbackWhenZero)
|
||||
{
|
||||
if (rawValue > 0)
|
||||
{
|
||||
cache[index] = rawValue;
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
var cachedValue = cache[index];
|
||||
if (cachedValue > 0)
|
||||
return cachedValue;
|
||||
|
||||
var computed = fallback();
|
||||
if (computed <= 0)
|
||||
computed = fallbackWhenZero;
|
||||
|
||||
cache[index] = computed;
|
||||
return computed;
|
||||
}
|
||||
|
||||
private bool TryCacheTextOffset(int nameplateIndex, int measuredTextWidth, int textOffset)
|
||||
{
|
||||
if (System.Math.Abs(measuredTextWidth) > 0 || textOffset != 0)
|
||||
{
|
||||
_buffers.TextOffsets[nameplateIndex] = textOffset;
|
||||
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)
|
||||
return false;
|
||||
}
|
||||
|
||||
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 readonly struct NameplateLabelInfo
|
||||
{
|
||||
public NameplateLabelInfo(
|
||||
Vector2 screenPosition,
|
||||
string text,
|
||||
uint textColor,
|
||||
uint edgeColor,
|
||||
float fontSize,
|
||||
Vector2 pivot,
|
||||
bool useIcon)
|
||||
{
|
||||
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 _);
|
||||
ScreenPosition = screenPosition;
|
||||
Text = text;
|
||||
TextColor = textColor;
|
||||
EdgeColor = edgeColor;
|
||||
FontSize = fontSize;
|
||||
Pivot = pivot;
|
||||
UseIcon = useIcon;
|
||||
}
|
||||
|
||||
private static unsafe float GetVisualHeight(IPlayerCharacter player)
|
||||
public Vector2 ScreenPosition { get; }
|
||||
public string Text { get; }
|
||||
public uint TextColor { get; }
|
||||
public uint EdgeColor { get; }
|
||||
public float FontSize { get; }
|
||||
public Vector2 Pivot { get; }
|
||||
public bool UseIcon { get; }
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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));
|
||||
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()
|
||||
{
|
||||
_buffers.Clear();
|
||||
ClearLabelBuffer();
|
||||
}
|
||||
|
||||
private sealed class NameplateBuffers
|
||||
{
|
||||
public NameplateBuffers()
|
||||
{
|
||||
TextOffsets = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
System.Array.Fill(TextOffsets, int.MinValue);
|
||||
}
|
||||
|
||||
public int[] TextWidths { get; } = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
public int[] TextHeights { get; } = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
public int[] ContainerHeights { get; } = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
public int[] TextOffsets { get; }
|
||||
public NameplateLabelInfo[] LabelScratch { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects];
|
||||
public NameplateLabelInfo[] LabelRender { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects];
|
||||
public NameplateLabelInfo[] LabelCopy { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects];
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
System.Array.Clear(TextWidths, 0, TextWidths.Length);
|
||||
System.Array.Clear(TextHeights, 0, TextHeights.Length);
|
||||
System.Array.Clear(ContainerHeights, 0, ContainerHeights.Length);
|
||||
System.Array.Fill(TextOffsets, int.MinValue);
|
||||
}
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Init();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Uninit();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user