Merge remote-tracking branch 'origin/2.0.0-crashing-bugfixes' into 2.0.0-crashing-bugfixes
# Conflicts: # LightlessSync/Services/DalamudUtilService.cs # LightlessSync/UI/DtrEntry.cs
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.FileCache;
|
||||
@@ -98,11 +99,13 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
_analysisCts = null;
|
||||
if (print) PrintAnalysis();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_analysisCts.CancelDispose();
|
||||
_baseAnalysisCts.Dispose();
|
||||
}
|
||||
|
||||
public async Task UpdateFileEntriesAsync(IEnumerable<string> filePaths, CancellationToken token)
|
||||
{
|
||||
var normalized = new HashSet<string>(
|
||||
@@ -125,6 +128,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BaseAnalysis(CharacterData charaData, CancellationToken token)
|
||||
{
|
||||
if (string.Equals(charaData.DataHash.Value, _lastDataHash, StringComparison.Ordinal)) return;
|
||||
@@ -136,29 +140,47 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var fileCacheEntries = (await _fileCacheManager.GetAllFileCachesByHashAsync(fileEntry.Hash, ignoreCacheEntries: true, validate: false, token).ConfigureAwait(false)).ToList();
|
||||
if (fileCacheEntries.Count == 0) continue;
|
||||
var filePath = fileCacheEntries[0].ResolvedFilepath;
|
||||
FileInfo fi = new(filePath);
|
||||
string ext = "unk?";
|
||||
try
|
||||
{
|
||||
ext = fi.Extension[1..];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Could not identify extension for {path}", filePath);
|
||||
}
|
||||
var fileCacheEntries = (await _fileCacheManager
|
||||
.GetAllFileCachesByHashAsync(fileEntry.Hash, ignoreCacheEntries: true, validate: false, token)
|
||||
.ConfigureAwait(false))
|
||||
.ToList();
|
||||
|
||||
if (fileCacheEntries.Count == 0)
|
||||
continue;
|
||||
|
||||
var resolved = fileCacheEntries[0].ResolvedFilepath;
|
||||
|
||||
var extWithDot = Path.GetExtension(resolved);
|
||||
var ext = string.IsNullOrEmpty(extWithDot) ? "unk?" : extWithDot.TrimStart('.');
|
||||
|
||||
var tris = await _xivDataAnalyzer.GetTrianglesByHash(fileEntry.Hash).ConfigureAwait(false);
|
||||
foreach (var entry in fileCacheEntries)
|
||||
|
||||
var distinctFilePaths = fileCacheEntries
|
||||
.Select(c => c.ResolvedFilepath)
|
||||
.Where(p => !string.IsNullOrWhiteSpace(p))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
long orig = 0, comp = 0;
|
||||
var first = fileCacheEntries[0];
|
||||
if (first.Size > 0) orig = first.Size.Value;
|
||||
if (first.CompressedSize > 0) comp = first.CompressedSize.Value;
|
||||
|
||||
if (_fileCacheManager.TryGetSizeInfo(fileEntry.Hash, out var cached))
|
||||
{
|
||||
data[fileEntry.Hash] = new FileDataEntry(fileEntry.Hash, ext,
|
||||
[.. fileEntry.GamePaths],
|
||||
[.. fileCacheEntries.Select(c => c.ResolvedFilepath).Distinct(StringComparer.Ordinal)],
|
||||
entry.Size > 0 ? entry.Size.Value : 0,
|
||||
entry.CompressedSize > 0 ? entry.CompressedSize.Value : 0,
|
||||
tris);
|
||||
if (orig <= 0 && cached.Original > 0) orig = cached.Original;
|
||||
if (comp <= 0 && cached.Compressed > 0) comp = cached.Compressed;
|
||||
}
|
||||
|
||||
data[fileEntry.Hash] = new FileDataEntry(
|
||||
fileEntry.Hash,
|
||||
ext,
|
||||
[.. fileEntry.GamePaths],
|
||||
distinctFilePaths,
|
||||
orig,
|
||||
comp,
|
||||
tris,
|
||||
fileCacheEntries);
|
||||
}
|
||||
LastAnalysis[obj.Key] = data;
|
||||
}
|
||||
@@ -167,6 +189,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
Mediator.Publish(new CharacterDataAnalyzedMessage());
|
||||
_lastDataHash = charaData.DataHash.Value;
|
||||
}
|
||||
|
||||
private void RecalculateSummary()
|
||||
{
|
||||
var builder = ImmutableDictionary.CreateBuilder<ObjectKind, CharacterAnalysisObjectSummary>();
|
||||
@@ -192,6 +215,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
|
||||
_latestSummary = new CharacterAnalysisSummary(builder.ToImmutable());
|
||||
}
|
||||
|
||||
private void PrintAnalysis()
|
||||
{
|
||||
if (LastAnalysis.Count == 0) return;
|
||||
@@ -235,42 +259,79 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
UiSharedService.ByteToString(LastAnalysis.Values.Sum(c => c.Values.Sum(v => v.CompressedSize))));
|
||||
Logger.LogInformation("IMPORTANT NOTES:\n\r- For Lightless up- and downloads only the compressed size is relevant.\n\r- An unusually high total files count beyond 200 and up will also increase your download time to others significantly.");
|
||||
}
|
||||
internal sealed record FileDataEntry(string Hash, string FileType, List<string> GamePaths, List<string> FilePaths, long OriginalSize, long CompressedSize, long Triangles)
|
||||
{
|
||||
public bool IsComputed => OriginalSize > 0 && CompressedSize > 0;
|
||||
public async Task ComputeSizes(FileCacheManager fileCacheManager, CancellationToken token)
|
||||
{
|
||||
var compressedsize = await fileCacheManager.GetCompressedFileData(Hash, token).ConfigureAwait(false);
|
||||
var normalSize = new FileInfo(FilePaths[0]).Length;
|
||||
var entries = await fileCacheManager.GetAllFileCachesByHashAsync(Hash, ignoreCacheEntries: true, validate: false, token).ConfigureAwait(false);
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
entry.Size = normalSize;
|
||||
entry.CompressedSize = compressedsize.Item2.LongLength;
|
||||
}
|
||||
OriginalSize = normalSize;
|
||||
CompressedSize = compressedsize.Item2.LongLength;
|
||||
RefreshFormat();
|
||||
}
|
||||
public long OriginalSize { get; private set; } = OriginalSize;
|
||||
public long CompressedSize { get; private set; } = CompressedSize;
|
||||
public long Triangles { get; private set; } = Triangles;
|
||||
public Lazy<string> Format => _format ??= CreateFormatValue();
|
||||
|
||||
internal sealed class FileDataEntry
|
||||
{
|
||||
public string Hash { get; }
|
||||
public string FileType { get; }
|
||||
public List<string> GamePaths { get; }
|
||||
public List<string> FilePaths { get; }
|
||||
|
||||
public long OriginalSize { get; private set; }
|
||||
public long CompressedSize { get; private set; }
|
||||
public long Triangles { get; private set; }
|
||||
|
||||
public IReadOnlyList<FileCacheEntity> CacheEntries { get; }
|
||||
|
||||
public bool IsComputed => OriginalSize > 0 && CompressedSize > 0;
|
||||
|
||||
public FileDataEntry(
|
||||
string hash,
|
||||
string fileType,
|
||||
List<string> gamePaths,
|
||||
List<string> filePaths,
|
||||
long originalSize,
|
||||
long compressedSize,
|
||||
long triangles,
|
||||
IReadOnlyList<FileCacheEntity> cacheEntries)
|
||||
{
|
||||
Hash = hash;
|
||||
FileType = fileType;
|
||||
GamePaths = gamePaths;
|
||||
FilePaths = filePaths;
|
||||
OriginalSize = originalSize;
|
||||
CompressedSize = compressedSize;
|
||||
Triangles = triangles;
|
||||
CacheEntries = cacheEntries;
|
||||
}
|
||||
|
||||
public async Task ComputeSizes(FileCacheManager fileCacheManager, CancellationToken token, bool force = false)
|
||||
{
|
||||
if (!force && IsComputed)
|
||||
return;
|
||||
|
||||
if (FilePaths.Count == 0 || string.IsNullOrWhiteSpace(FilePaths[0]))
|
||||
return;
|
||||
|
||||
var path = FilePaths[0];
|
||||
|
||||
if (!File.Exists(path))
|
||||
return;
|
||||
|
||||
var original = new FileInfo(path).Length;
|
||||
|
||||
var compressedLen = await fileCacheManager.GetCompressedSizeAsync(Hash, token).ConfigureAwait(false);
|
||||
|
||||
fileCacheManager.SetSizeInfo(Hash, original, compressedLen);
|
||||
FileCacheManager.ApplySizesToEntries(CacheEntries, original, compressedLen);
|
||||
|
||||
OriginalSize = original;
|
||||
CompressedSize = compressedLen;
|
||||
|
||||
if (string.Equals(FileType, "tex", StringComparison.OrdinalIgnoreCase))
|
||||
RefreshFormat();
|
||||
}
|
||||
|
||||
public Lazy<string> Format => _format ??= CreateFormatValue();
|
||||
private Lazy<string>? _format;
|
||||
|
||||
public void RefreshFormat()
|
||||
{
|
||||
_format = CreateFormatValue();
|
||||
}
|
||||
public void RefreshFormat() => _format = CreateFormatValue();
|
||||
|
||||
private Lazy<string> CreateFormatValue()
|
||||
=> new(() =>
|
||||
{
|
||||
if (!string.Equals(FileType, "tex", StringComparison.Ordinal))
|
||||
{
|
||||
if (!string.Equals(FileType, "tex", StringComparison.OrdinalIgnoreCase))
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Control;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using LightlessSync.API.Dto.CharaData;
|
||||
@@ -28,7 +26,6 @@ using System.Text;
|
||||
using BattleNpcSubKind = FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSubKind;
|
||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
||||
using Map = Lumina.Excel.Sheets.Map;
|
||||
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
@@ -85,18 +82,18 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
_configService = configService;
|
||||
_playerPerformanceConfigService = playerPerformanceConfigService;
|
||||
_pairFactory = pairFactory;
|
||||
var clientLanguage = _clientState.ClientLanguage;
|
||||
WorldData = new(() =>
|
||||
{
|
||||
return gameData.GetExcelSheet<Lumina.Excel.Sheets.World>(Dalamud.Game.ClientLanguage.English)!
|
||||
return gameData.GetExcelSheet<Lumina.Excel.Sheets.World>(clientLanguage)!
|
||||
.Where(w => !w.Name.IsEmpty && w.DataCenter.RowId != 0 && (w.IsPublic || char.IsUpper(w.Name.ToString()[0])))
|
||||
.ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString());
|
||||
});
|
||||
JobData = new(() =>
|
||||
{
|
||||
return gameData.GetExcelSheet<ClassJob>(Dalamud.Game.ClientLanguage.English)!
|
||||
.ToDictionary(k => k.RowId, k => k.NameEnglish.ToString());
|
||||
return gameData.GetExcelSheet<ClassJob>(clientLanguage)!
|
||||
.ToDictionary(k => k.RowId, k => k.Name.ToString());
|
||||
});
|
||||
var clientLanguage = _clientState.ClientLanguage;
|
||||
TerritoryData = new(() => BuildTerritoryData(clientLanguage));
|
||||
TerritoryDataEnglish = new(() => BuildTerritoryData(Dalamud.Game.ClientLanguage.English));
|
||||
MapData = new(() => BuildMapData(clientLanguage));
|
||||
@@ -662,7 +659,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
|
||||
var location = new LocationInfo();
|
||||
location.ServerId = _playerState.CurrentWorld.RowId;
|
||||
location.InstanceId = UIState.Instance()->PublicInstance.InstanceId;
|
||||
//location.InstanceId = UIState.Instance()->PublicInstance.InstanceId; //TODO:Need API update first
|
||||
location.TerritoryId = _clientState.TerritoryType;
|
||||
location.MapId = _clientState.MapId;
|
||||
if (houseMan != null)
|
||||
@@ -716,10 +713,10 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
str += $" - {MapData.Value[(ushort)location.MapId].MapName}";
|
||||
}
|
||||
|
||||
if (location.InstanceId is not 0)
|
||||
{
|
||||
str += ((SeIconChar)(57520 + location.InstanceId)).ToIconString();
|
||||
}
|
||||
// if (location.InstanceId is not 0)
|
||||
// {
|
||||
// str += ((SeIconChar)(57520 + location.InstanceId)).ToIconString();
|
||||
// }
|
||||
|
||||
if (location.WardId is not 0)
|
||||
{
|
||||
|
||||
@@ -23,6 +23,7 @@ using Pictomancy;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
@@ -41,6 +42,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly PairUiService _pairUiService;
|
||||
private readonly LightlessMediator _mediator;
|
||||
|
||||
public LightlessMediator Mediator => _mediator;
|
||||
|
||||
private readonly IUiBuilder _uiBuilder;
|
||||
@@ -61,16 +63,22 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
|
||||
// / Overlay window flags
|
||||
private const ImGuiWindowFlags _overlayFlags =
|
||||
ImGuiWindowFlags.NoDecoration |
|
||||
ImGuiWindowFlags.NoBackground |
|
||||
ImGuiWindowFlags.NoMove |
|
||||
ImGuiWindowFlags.NoSavedSettings |
|
||||
ImGuiWindowFlags.NoNav |
|
||||
ImGuiWindowFlags.NoInputs;
|
||||
ImGuiWindowFlags.NoDecoration |
|
||||
ImGuiWindowFlags.NoBackground |
|
||||
ImGuiWindowFlags.NoMove |
|
||||
ImGuiWindowFlags.NoSavedSettings |
|
||||
ImGuiWindowFlags.NoNav |
|
||||
ImGuiWindowFlags.NoInputs;
|
||||
|
||||
private readonly List<RectF> _uiRects = new(128);
|
||||
private ImmutableHashSet<string> _activeBroadcastingCids = [];
|
||||
|
||||
#if DEBUG
|
||||
// Debug controls
|
||||
|
||||
// Debug counters (read-only from UI)
|
||||
#endif
|
||||
|
||||
private bool IsPictomancyRenderer => _configService.Current.LightfinderLabelRenderer == LightfinderLabelRenderer.Pictomancy;
|
||||
|
||||
public LightFinderPlateHandler(
|
||||
@@ -96,7 +104,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
_uiBuilder = pluginInterface.UiBuilder ?? throw new ArgumentNullException(nameof(pluginInterface));
|
||||
_ = pictomancyService ?? throw new ArgumentNullException(nameof(pictomancyService));
|
||||
_lastRenderer = _configService.Current.LightfinderLabelRenderer;
|
||||
|
||||
}
|
||||
|
||||
private void RefreshRendererState()
|
||||
@@ -187,8 +194,6 @@ 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)
|
||||
{
|
||||
RefreshRendererState();
|
||||
@@ -199,6 +204,16 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide our overlay when the user hides the entire game UI (ScrollLock).
|
||||
if (_gameGui.GameUiHidden)
|
||||
{
|
||||
ClearLabelBuffer();
|
||||
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
|
||||
_lastNamePlateDrawFrame = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// gpose: do not draw.
|
||||
if (_clientState.IsGPosing)
|
||||
{
|
||||
ClearLabelBuffer();
|
||||
@@ -218,6 +233,10 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
if (fw != null)
|
||||
_lastNamePlateDrawFrame = fw->FrameCounter;
|
||||
|
||||
#if DEBUG
|
||||
DebugLastNameplateFrame = _lastNamePlateDrawFrame;
|
||||
#endif
|
||||
|
||||
var pNameplateAddon = (AddonNamePlate*)args.Addon.Address;
|
||||
|
||||
if (_mpNameplateAddon != pNameplateAddon)
|
||||
@@ -234,6 +253,13 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
/// </summary>
|
||||
private void UpdateNameplateNodes()
|
||||
{
|
||||
// If the user has hidden the UI, don't compute any labels.
|
||||
if (_gameGui.GameUiHidden)
|
||||
{
|
||||
ClearLabelBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
var currentHandle = _gameGui.GetAddonByName("NamePlate");
|
||||
if (currentHandle.Address == nint.Zero)
|
||||
{
|
||||
@@ -297,7 +323,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
|
||||
for (int i = 0; i < safeCount; ++i)
|
||||
{
|
||||
|
||||
var objectInfoPtr = vec[i];
|
||||
if (objectInfoPtr == null)
|
||||
continue;
|
||||
@@ -314,7 +339,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player)
|
||||
continue;
|
||||
|
||||
// CID gating
|
||||
// CID gating - only show for active broadcasters
|
||||
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject);
|
||||
if (cid == null || !_activeBroadcastingCids.Contains(cid))
|
||||
continue;
|
||||
@@ -350,12 +375,12 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
if (!currentConfig.LightfinderLabelShowHidden && !isNameplateVisible)
|
||||
continue;
|
||||
|
||||
// Prepare label content and scaling
|
||||
var scaleMultiplier = System.Math.Clamp(currentConfig.LightfinderLabelScale, 0.5f, 2.0f);
|
||||
// Prepare label content and scaling factors
|
||||
var scaleMultiplier = 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 targetFontSize = (int)Math.Round(baseFontSize * scaleMultiplier);
|
||||
var labelContent = currentConfig.LightfinderLabelUseIcon
|
||||
? NormalizeIconGlyph(currentConfig.LightfinderLabelIconGlyph)
|
||||
: _defaultLabelText;
|
||||
@@ -363,8 +388,8 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
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);
|
||||
var nodeWidth = (int)Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
||||
var nodeHeight = (int)Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
|
||||
AlignmentType alignment;
|
||||
|
||||
var textScaleY = nameText->AtkResNode.ScaleY;
|
||||
@@ -374,7 +399,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
var blockHeight = ResolveCache(
|
||||
_buffers.TextHeights,
|
||||
nameplateIndex,
|
||||
System.Math.Abs((int)nameplateObject.TextH),
|
||||
Math.Abs((int)nameplateObject.TextH),
|
||||
() => GetScaledTextHeight(nameText),
|
||||
nodeHeight);
|
||||
|
||||
@@ -384,7 +409,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
(int)nameContainer->Height,
|
||||
() =>
|
||||
{
|
||||
var computed = blockHeight + (int)System.Math.Round(8 * textScaleY);
|
||||
var computed = blockHeight + (int)Math.Round(8 * textScaleY);
|
||||
return computed <= blockHeight ? blockHeight + 1 : computed;
|
||||
},
|
||||
blockHeight + 1);
|
||||
@@ -392,7 +417,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
var blockTop = containerHeight - blockHeight;
|
||||
if (blockTop < 0)
|
||||
blockTop = 0;
|
||||
var verticalPadding = (int)System.Math.Round(4 * effectiveScale);
|
||||
var verticalPadding = (int)Math.Round(4 * effectiveScale);
|
||||
|
||||
var positionY = blockTop - verticalPadding;
|
||||
|
||||
@@ -400,21 +425,14 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
var textWidth = ResolveCache(
|
||||
_buffers.TextWidths,
|
||||
nameplateIndex,
|
||||
System.Math.Abs(rawTextWidth),
|
||||
Math.Abs(rawTextWidth),
|
||||
() => GetScaledTextWidth(nameText),
|
||||
nodeWidth);
|
||||
|
||||
// Text offset caching
|
||||
var textOffset = (int)System.Math.Round(nameText->AtkResNode.X);
|
||||
var textOffset = (int)Math.Round(nameText->AtkResNode.X);
|
||||
var hasValidOffset = TryCacheTextOffset(nameplateIndex, rawTextWidth, textOffset);
|
||||
|
||||
if (nameContainer == null)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("Nameplate {Index} container became unavailable during update, skipping.", nameplateIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
var res = nameContainer;
|
||||
|
||||
// X scale
|
||||
@@ -450,7 +468,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
|
||||
var offsetXScreen = currentConfig.LightfinderLabelOffsetX * worldScaleX;
|
||||
|
||||
// alignment based on config
|
||||
// alignment based on config setting
|
||||
switch (currentConfig.LabelAlignment)
|
||||
{
|
||||
case LabelAlignment.Left:
|
||||
@@ -469,7 +487,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
}
|
||||
else
|
||||
{
|
||||
// manual X positioning
|
||||
// manual X positioning with optional cached offset
|
||||
var cachedTextOffset = _buffers.TextOffsets[nameplateIndex];
|
||||
var hasCachedOffset = cachedTextOffset != int.MinValue;
|
||||
var baseOffsetXLocal = (!currentConfig.LightfinderLabelUseIcon && hasValidOffset && hasCachedOffset)
|
||||
@@ -489,16 +507,16 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
|
||||
// final position before smoothing
|
||||
var finalPosition = new Vector2(finalX, res->ScreenY + positionYScreen);
|
||||
var dpiScale = ImGui.GetIO().DisplayFramebufferScale.X; // often same for Y
|
||||
var dpiScale = ImGui.GetIO().DisplayFramebufferScale.X;
|
||||
var fw = Framework.Instance();
|
||||
float dt = fw->RealFrameDeltaTime;
|
||||
|
||||
//smoothing..
|
||||
//smoothing.. snap.. smooth.. snap
|
||||
finalPosition = SnapToPixels(finalPosition, dpiScale);
|
||||
finalPosition = SmoothPosition(nameplateIndex, finalPosition, dt);
|
||||
finalPosition = SnapToPixels(finalPosition, dpiScale);
|
||||
|
||||
// prepare label info
|
||||
// prepare label info for rendering
|
||||
var pivot = (currentConfig.LightfinderAutoAlign || currentConfig.LightfinderLabelUseIcon)
|
||||
? AlignmentToPivot(alignment)
|
||||
: _defaultPivot;
|
||||
@@ -545,7 +563,23 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
if (fw == null)
|
||||
return;
|
||||
|
||||
// Frame skip check
|
||||
// If UI is hidden, do not render.
|
||||
if (_gameGui.GameUiHidden)
|
||||
{
|
||||
ClearLabelBuffer();
|
||||
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
|
||||
_lastNamePlateDrawFrame = 0;
|
||||
|
||||
#if DEBUG
|
||||
DebugLabelCountLastFrame = 0;
|
||||
DebugUiRectCountLastFrame = 0;
|
||||
DebugOccludedCountLastFrame = 0;
|
||||
DebugLastNameplateFrame = 0;
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
// Frame skip check - skip if more than 1 frame has passed since last nameplate draw.
|
||||
var frame = fw->FrameCounter;
|
||||
|
||||
if (_lastNamePlateDrawFrame == 0 || (frame - _lastNamePlateDrawFrame) > 1)
|
||||
@@ -553,34 +587,62 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
ClearLabelBuffer();
|
||||
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
|
||||
|
||||
#if DEBUG
|
||||
DebugLabelCountLastFrame = 0;
|
||||
DebugUiRectCountLastFrame = 0;
|
||||
DebugOccludedCountLastFrame = 0;
|
||||
DebugLastNameplateFrame = _lastNamePlateDrawFrame;
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
//Gpose Check
|
||||
// Gpose Check - do not render.
|
||||
if (_clientState.IsGPosing)
|
||||
{
|
||||
ClearLabelBuffer();
|
||||
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
|
||||
_lastNamePlateDrawFrame = 0;
|
||||
|
||||
#if DEBUG
|
||||
DebugLabelCountLastFrame = 0;
|
||||
DebugUiRectCountLastFrame = 0;
|
||||
DebugOccludedCountLastFrame = 0;
|
||||
DebugLastNameplateFrame = 0;
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
// If nameplate addon is not visible, skip rendering
|
||||
// If nameplate addon is not visible, skip rendering entirely.
|
||||
if (!IsNamePlateAddonVisible())
|
||||
{
|
||||
#if DEBUG
|
||||
DebugLabelCountLastFrame = 0;
|
||||
DebugUiRectCountLastFrame = 0;
|
||||
DebugOccludedCountLastFrame = 0;
|
||||
DebugLastNameplateFrame = _lastNamePlateDrawFrame;
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
int copyCount;
|
||||
lock (_labelLock)
|
||||
{
|
||||
copyCount = _labelRenderCount;
|
||||
if (copyCount == 0)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugLabelCountLastFrame = 0;
|
||||
DebugUiRectCountLastFrame = 0;
|
||||
DebugOccludedCountLastFrame = 0;
|
||||
DebugLastNameplateFrame = _lastNamePlateDrawFrame;
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
Array.Copy(_buffers.LabelRender, _buffers.LabelCopy, copyCount);
|
||||
}
|
||||
|
||||
var uiModule = fw != null ? fw->GetUIModule() : null;
|
||||
|
||||
var uiModule = fw->GetUIModule();
|
||||
if (uiModule != null)
|
||||
{
|
||||
var rapture = uiModule->GetRaptureAtkModule();
|
||||
@@ -599,7 +661,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
var vpPos = vp.Pos;
|
||||
|
||||
ImGuiHelpers.ForceNextWindowMainViewport();
|
||||
|
||||
ImGui.SetNextWindowPos(vp.Pos);
|
||||
ImGui.SetNextWindowSize(vp.Size);
|
||||
|
||||
@@ -610,54 +671,118 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
|
||||
ImGui.PopStyleVar(2);
|
||||
|
||||
using var drawList = PictoService.Draw();
|
||||
if (drawList == null)
|
||||
// Debug flags
|
||||
bool dbgEnabled = false;
|
||||
bool dbgDisableOcc = false;
|
||||
bool dbgDrawUiRects = false;
|
||||
bool dbgDrawLabelRects = false;
|
||||
#if DEBUG
|
||||
dbgEnabled = DebugEnabled;
|
||||
dbgDisableOcc = DebugDisableOcclusion;
|
||||
dbgDrawUiRects = DebugDrawUiRects;
|
||||
dbgDrawLabelRects = DebugDrawLabelRects;
|
||||
#endif
|
||||
|
||||
int occludedThisFrame = 0;
|
||||
|
||||
try
|
||||
{
|
||||
using var drawList = PictoService.Draw();
|
||||
if (drawList == null)
|
||||
return;
|
||||
|
||||
// Debug drawing uses the window drawlist (so it always draws in the correct viewport).
|
||||
var dbgDl = ImGui.GetWindowDrawList();
|
||||
var useViewportOffset = ImGui.GetIO().ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable);
|
||||
|
||||
for (int i = 0; i < copyCount; ++i)
|
||||
{
|
||||
ref var info = ref _buffers.LabelCopy[i];
|
||||
|
||||
// final draw position with viewport offset (only when viewports are enabled)
|
||||
var drawPos = info.ScreenPosition;
|
||||
if (useViewportOffset)
|
||||
drawPos += vpPos;
|
||||
|
||||
var font = default(ImFontPtr);
|
||||
if (info.UseIcon)
|
||||
{
|
||||
var ioFonts = ImGui.GetIO().Fonts;
|
||||
font = ioFonts.Fonts.Size > 1 ? new ImFontPtr(ioFonts.Fonts[1]) : ImGui.GetFont();
|
||||
}
|
||||
else
|
||||
{
|
||||
font = ImGui.GetFont();
|
||||
}
|
||||
|
||||
if (!font.IsNull)
|
||||
ImGui.PushFont(font);
|
||||
|
||||
// calculate size for occlusion checking
|
||||
var baseSize = ImGui.CalcTextSize(info.Text);
|
||||
var baseFontSize = ImGui.GetFontSize();
|
||||
|
||||
if (!font.IsNull)
|
||||
ImGui.PopFont();
|
||||
|
||||
// scale size based on font size
|
||||
var scale = baseFontSize > 0 ? (info.FontSize / baseFontSize) : 1f;
|
||||
var size = baseSize * scale;
|
||||
|
||||
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);
|
||||
|
||||
bool wouldOcclude = IsOccludedByAnyUi(labelRect);
|
||||
if (wouldOcclude)
|
||||
occludedThisFrame++;
|
||||
|
||||
// Debug: draw label rects
|
||||
if (dbgEnabled && dbgDrawLabelRects)
|
||||
{
|
||||
var tl = new Vector2(labelRect.L, labelRect.T);
|
||||
var br = new Vector2(labelRect.R, labelRect.B);
|
||||
|
||||
if (useViewportOffset) { tl += vpPos; br += vpPos; }
|
||||
|
||||
// green = visible, red = would be occluded (even if forced)
|
||||
var col = wouldOcclude
|
||||
? ImGui.GetColorU32(new Vector4(1f, 0f, 0f, 0.6f))
|
||||
: ImGui.GetColorU32(new Vector4(0f, 1f, 0f, 0.6f));
|
||||
|
||||
dbgDl.AddRect(tl, br, col);
|
||||
}
|
||||
|
||||
// occlusion check (allow debug to disable)
|
||||
if (!dbgDisableOcc && wouldOcclude)
|
||||
continue;
|
||||
|
||||
drawList.AddScreenText(drawPos, info.Text, info.TextColor, info.FontSize, info.Pivot, info.EdgeColor, font);
|
||||
}
|
||||
|
||||
// Debug: draw UI rects if any
|
||||
if (dbgEnabled && dbgDrawUiRects && _uiRects.Count > 0)
|
||||
{
|
||||
var useOff = useViewportOffset ? vpPos : Vector2.Zero;
|
||||
var col = ImGui.GetColorU32(new Vector4(1f, 1f, 1f, 0.35f));
|
||||
|
||||
for (int i = 0; i < _uiRects.Count; i++)
|
||||
{
|
||||
var r = _uiRects[i];
|
||||
dbgDl.AddRect(new Vector2(r.L, r.T) + useOff, new Vector2(r.R, r.B) + useOff, col);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ImGui.End();
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < copyCount; ++i)
|
||||
{
|
||||
ref var info = ref _buffers.LabelCopy[i];
|
||||
|
||||
// final draw position with viewport offset
|
||||
var drawPos = info.ScreenPosition + vpPos;
|
||||
var font = default(ImFontPtr);
|
||||
if (info.UseIcon)
|
||||
{
|
||||
var ioFonts = ImGui.GetIO().Fonts;
|
||||
font = ioFonts.Fonts.Size > 1 ? new ImFontPtr(ioFonts.Fonts[1]) : ImGui.GetFont();
|
||||
}
|
||||
else
|
||||
{
|
||||
font = ImGui.GetFont();
|
||||
}
|
||||
|
||||
if (!font.IsNull)
|
||||
ImGui.PushFont(font);
|
||||
|
||||
// calculate size for occlusion checking
|
||||
var baseSize = ImGui.CalcTextSize(info.Text);
|
||||
var baseFontSize = ImGui.GetFontSize();
|
||||
|
||||
if (!font.IsNull)
|
||||
ImGui.PopFont();
|
||||
|
||||
// scale size based on font size
|
||||
var scale = baseFontSize > 0 ? (info.FontSize / baseFontSize) : 1f;
|
||||
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 labelRect = new RectF(topLeft.X, topLeft.Y, topLeft.X + size.X, topLeft.Y + size.Y);
|
||||
|
||||
// occlusion check
|
||||
if (IsOccludedByAnyUi(labelRect))
|
||||
continue;
|
||||
|
||||
drawList.AddScreenText(drawPos, info.Text, info.TextColor, info.FontSize, info.Pivot, info.EdgeColor, font);
|
||||
}
|
||||
#if DEBUG
|
||||
DebugLabelCountLastFrame = copyCount;
|
||||
DebugUiRectCountLastFrame = _uiRects.Count;
|
||||
DebugOccludedCountLastFrame = occludedThisFrame;
|
||||
DebugLastNameplateFrame = _lastNamePlateDrawFrame;
|
||||
#endif
|
||||
}
|
||||
|
||||
private static Vector2 AlignmentToPivot(AlignmentType alignment) => alignment switch
|
||||
@@ -705,8 +830,8 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
if (scale <= 0f)
|
||||
scale = 1f;
|
||||
|
||||
var computed = (int)System.Math.Round(rawHeight * scale);
|
||||
return System.Math.Max(1, computed);
|
||||
var computed = (int)Math.Round(rawHeight * scale);
|
||||
return Math.Max(1, computed);
|
||||
}
|
||||
|
||||
private static unsafe int GetScaledTextWidth(AtkTextNode* node)
|
||||
@@ -730,12 +855,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
/// <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(
|
||||
int[] cache,
|
||||
int index,
|
||||
@@ -775,9 +894,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
/// <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)
|
||||
{
|
||||
// snap to pixel grid
|
||||
@@ -786,15 +902,9 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
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)
|
||||
{
|
||||
// exponential smoothing
|
||||
@@ -812,7 +922,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
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;
|
||||
|
||||
// lerp towards target
|
||||
@@ -821,73 +931,193 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
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>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsFinite(float f) => !(float.IsNaN(f) || float.IsInfinity(f));
|
||||
|
||||
private static bool TryGetAddonRect(AtkUnitBase* addon, Vector2 screen, out RectF rect)
|
||||
{
|
||||
// Addon existence
|
||||
rect = default;
|
||||
if (addon == null)
|
||||
return false;
|
||||
|
||||
// Visibility check
|
||||
// Addon must be visible
|
||||
if (!addon->IsVisible)
|
||||
return false;
|
||||
|
||||
// Root must be visible
|
||||
var root = addon->RootNode;
|
||||
if (root == null || !root->IsVisible())
|
||||
return false;
|
||||
|
||||
// Size check
|
||||
float w = root->Width;
|
||||
float h = root->Height;
|
||||
if (w <= 0 || h <= 0)
|
||||
// Must have multiple nodes to be useful
|
||||
var nodeCount = addon->UldManager.NodeListCount;
|
||||
var nodeList = addon->UldManager.NodeList;
|
||||
if (nodeCount <= 1 || nodeList == null)
|
||||
return false;
|
||||
|
||||
// Local scale
|
||||
float sx = root->ScaleX; if (sx <= 0f) sx = 1f;
|
||||
float sy = root->ScaleY; if (sy <= 0f) sy = 1f;
|
||||
float rsx = GetWorldScaleX(root);
|
||||
float rsy = GetWorldScaleY(root);
|
||||
if (!IsFinite(rsx) || rsx <= 0f) rsx = 1f;
|
||||
if (!IsFinite(rsy) || rsy <= 0f) rsy = 1f;
|
||||
|
||||
// World/composed scale from Transform
|
||||
float wsx = GetWorldScaleX(root);
|
||||
float wsy = GetWorldScaleY(root);
|
||||
if (wsx <= 0f) wsx = 1f;
|
||||
if (wsy <= 0f) wsy = 1f;
|
||||
// clamp insane root scales (rare but prevents explosions)
|
||||
rsx = MathF.Min(rsx, 6f);
|
||||
rsy = MathF.Min(rsy, 6f);
|
||||
|
||||
// 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)
|
||||
float rw = root->Width * rsx;
|
||||
float rh = root->Height * rsy;
|
||||
if (!IsFinite(rw) || !IsFinite(rh) || rw <= 2f || rh <= 2f)
|
||||
return false;
|
||||
|
||||
// Screen coords
|
||||
float l = root->ScreenX;
|
||||
float t = root->ScreenY;
|
||||
float r = l + w;
|
||||
float b = t + h;
|
||||
|
||||
// Drop fullscreen-ish / insane rects
|
||||
if (w >= screen.X * 0.98f && h >= screen.Y * 0.98f)
|
||||
float rl = root->ScreenX;
|
||||
float rt = root->ScreenY;
|
||||
if (!IsFinite(rl) || !IsFinite(rt))
|
||||
return false;
|
||||
|
||||
// Drop offscreen rects
|
||||
if (l < -screen.X || t < -screen.Y || r > screen.X * 2f || b > screen.Y * 2f)
|
||||
float rr = rl + rw;
|
||||
float rb = rt + rh;
|
||||
|
||||
// If root is basically fullscreen, it<69>s not a useful occluder for our purpose.
|
||||
if (rw >= screen.X * 0.98f && rh >= screen.Y * 0.98f)
|
||||
return false;
|
||||
|
||||
// Clip root to screen so it stays sane
|
||||
float rootL = MathF.Max(0f, rl);
|
||||
float rootT = MathF.Max(0f, rt);
|
||||
float rootR = MathF.Min(screen.X, rr);
|
||||
float rootB = MathF.Min(screen.Y, rb);
|
||||
if (rootR <= rootL || rootB <= rootT)
|
||||
return false;
|
||||
|
||||
// Root dimensions
|
||||
var rootW = rootR - rootL;
|
||||
var rootH = rootB - rootT;
|
||||
|
||||
// Find union of all probably-drawable nodes intersecting root
|
||||
bool any = false;
|
||||
float l = float.MaxValue, t = float.MaxValue, r = float.MinValue, b = float.MinValue;
|
||||
|
||||
// Allow a small bleed outside root; some addons draw small bits outside their root container.
|
||||
const float rootPad = 24f;
|
||||
float padL = rootL - rootPad;
|
||||
float padT = rootT - rootPad;
|
||||
float padR = rootR + rootPad;
|
||||
float padB = rootB + rootPad;
|
||||
|
||||
for (int i = 1; i < nodeCount; i++)
|
||||
{
|
||||
var n = nodeList[i];
|
||||
if (!IsProbablyDrawableNode(n))
|
||||
continue;
|
||||
|
||||
float w = n->Width;
|
||||
float h = n->Height;
|
||||
if (!IsFinite(w) || !IsFinite(h) || w <= 1f || h <= 1f)
|
||||
continue;
|
||||
|
||||
float sx = GetWorldScaleX(n);
|
||||
float sy = GetWorldScaleY(n);
|
||||
|
||||
if (!IsFinite(sx) || sx <= 0f) sx = 1f;
|
||||
if (!IsFinite(sy) || sy <= 0f) sy = 1f;
|
||||
|
||||
sx = MathF.Min(sx, 6f);
|
||||
sy = MathF.Min(sy, 6f);
|
||||
|
||||
w *= sx;
|
||||
h *= sy;
|
||||
|
||||
if (!IsFinite(w) || !IsFinite(h) || w < 2f || h < 2f)
|
||||
continue;
|
||||
|
||||
float nl = n->ScreenX;
|
||||
float nt = n->ScreenY;
|
||||
if (!IsFinite(nl) || !IsFinite(nt))
|
||||
continue;
|
||||
|
||||
float nr = nl + w;
|
||||
float nb = nt + h;
|
||||
|
||||
// Must intersect root (with padding). This is the big mitigation.
|
||||
if (nr <= padL || nb <= padT || nl >= padR || nt >= padB)
|
||||
continue;
|
||||
|
||||
// Reject nodes that are wildly larger than the root (common on targeting).
|
||||
if (w > rootW * 2.0f || h > rootH * 2.0f)
|
||||
continue;
|
||||
|
||||
// Clip node to root and then to screen (prevents offscreen junk stretching union)
|
||||
float cl = MathF.Max(rootL, nl);
|
||||
float ct = MathF.Max(rootT, nt);
|
||||
float cr = MathF.Min(rootR, nr);
|
||||
float cb = MathF.Min(rootB, nb);
|
||||
|
||||
cl = MathF.Max(0f, cl);
|
||||
ct = MathF.Max(0f, ct);
|
||||
cr = MathF.Min(screen.X, cr);
|
||||
cb = MathF.Min(screen.Y, cb);
|
||||
|
||||
if (cr <= cl || cb <= ct)
|
||||
continue;
|
||||
|
||||
any = true;
|
||||
if (cl < l) l = cl;
|
||||
if (ct < t) t = ct;
|
||||
if (cr > r) r = cr;
|
||||
if (cb > b) b = cb;
|
||||
}
|
||||
|
||||
// If nothing usable, fallback to root rect (still a sane occluder)
|
||||
if (!any)
|
||||
{
|
||||
rect = new RectF(rootL, rootT, rootR, rootB);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validate final union rect
|
||||
var uw = r - l;
|
||||
var uh = b - t;
|
||||
if (uw < 4f || uh < 4f)
|
||||
{
|
||||
rect = new RectF(rootL, rootT, rootR, rootB);
|
||||
return true;
|
||||
}
|
||||
|
||||
// If union is excessively larger than root, fallback to root rect
|
||||
if (uw > rootW * 1.35f || uh > rootH * 1.35f)
|
||||
{
|
||||
rect = new RectF(rootL, rootT, rootR, rootB);
|
||||
return true;
|
||||
}
|
||||
|
||||
rect = new RectF(l, t, r, b);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsProbablyDrawableNode(AtkResNode* n)
|
||||
{
|
||||
if (n == null || !n->IsVisible())
|
||||
return false;
|
||||
|
||||
// Check alpha
|
||||
if (n->Color.A == 16)
|
||||
return false;
|
||||
|
||||
// Check node type
|
||||
return n->Type switch
|
||||
{
|
||||
NodeType.Text => true,
|
||||
NodeType.Image => true,
|
||||
NodeType.NineGrid => true,
|
||||
NodeType.Counter => true,
|
||||
NodeType.Component => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the cached UI rects for occlusion checking.
|
||||
/// </summary>
|
||||
/// <param name="unitMgr">Unit Manager</param>
|
||||
private void RefreshUiRects(RaptureAtkUnitManager* unitMgr)
|
||||
{
|
||||
_uiRects.Clear();
|
||||
@@ -911,13 +1141,15 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
if (TryGetAddonRect(addon, screen, out var r))
|
||||
_uiRects.Add(r);
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
DebugUiRectCountLastFrame = _uiRects.Count;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
for (int i = 0; i < _uiRects.Count; i++)
|
||||
@@ -931,8 +1163,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
/// <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)
|
||||
{
|
||||
var t = n->Transform;
|
||||
@@ -942,8 +1172,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
/// <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)
|
||||
{
|
||||
var t = n->Transform;
|
||||
@@ -953,8 +1181,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
/// <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)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawInput))
|
||||
@@ -982,7 +1208,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
/// <summary>
|
||||
/// Is the nameplate addon visible?
|
||||
/// </summary>
|
||||
/// <returns>Is it visible?</returns>
|
||||
private bool IsNamePlateAddonVisible()
|
||||
{
|
||||
if (_mpNameplateAddon == null)
|
||||
@@ -992,20 +1217,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
return root != null && root->IsVisible();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts raw icon glyph input into an icon editor string.
|
||||
/// </summary>
|
||||
/// <param name="rawInput">Raw icon glyph input</param>
|
||||
/// <returns>Icon editor string</returns>
|
||||
internal static string ToIconEditorString(string? rawInput)
|
||||
{
|
||||
var normalized = NormalizeIconGlyph(rawInput);
|
||||
var runeEnumerator = normalized.EnumerateRunes();
|
||||
return runeEnumerator.MoveNext()
|
||||
? runeEnumerator.Current.Value.ToString("X4", CultureInfo.InvariantCulture)
|
||||
: _defaultIconGlyph;
|
||||
}
|
||||
|
||||
private readonly struct NameplateLabelInfo
|
||||
{
|
||||
public NameplateLabelInfo(
|
||||
@@ -1043,6 +1254,15 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
||||
.Select(u => (ulong)u.PlayerCharacterId)];
|
||||
|
||||
public int DebugLabelCountLastFrame { get; set; }
|
||||
public int DebugUiRectCountLastFrame { get; set; }
|
||||
public int DebugOccludedCountLastFrame { get; set; }
|
||||
public uint DebugLastNameplateFrame { get; set; }
|
||||
public bool DebugDrawUiRects { get; set; }
|
||||
public bool DebugDrawLabelRects { get; set; } = true;
|
||||
public bool DebugDisableOcclusion { get; set; }
|
||||
public bool DebugEnabled { get; set; }
|
||||
|
||||
public void FlagRefresh()
|
||||
{
|
||||
_needsLabelRefresh = true;
|
||||
@@ -1066,7 +1286,6 @@ 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)
|
||||
{
|
||||
var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal);
|
||||
@@ -1096,7 +1315,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
public NameplateBuffers()
|
||||
{
|
||||
TextOffsets = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
System.Array.Fill(TextOffsets, int.MinValue);
|
||||
Array.Fill(TextOffsets, int.MinValue);
|
||||
}
|
||||
|
||||
public int[] TextWidths { get; } = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
@@ -1108,23 +1327,20 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
public NameplateLabelInfo[] LabelCopy { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects];
|
||||
|
||||
public Vector2[] SmoothedPos = new Vector2[AddonNamePlate.NumNamePlateObjects];
|
||||
|
||||
public bool[] HasSmoothed = new bool[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);
|
||||
Array.Clear(TextWidths, 0, TextWidths.Length);
|
||||
Array.Clear(TextHeights, 0, TextHeights.Length);
|
||||
Array.Clear(ContainerHeights, 0, ContainerHeights.Length);
|
||||
Array.Fill(TextOffsets, int.MinValue);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the LightFinder Plate Handler.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation Token</param>
|
||||
/// <returns>Task Completed</returns>
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Init();
|
||||
@@ -1134,8 +1350,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
/// <summary>
|
||||
/// Stops the LightFinder Plate Handler.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation Token</param>
|
||||
/// <returns>Task Completed</returns>
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Uninit();
|
||||
@@ -1154,4 +1368,4 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
public bool Intersects(in RectF o) =>
|
||||
!(R <= o.L || o.R <= L || B <= o.T || o.B <= T);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ public class UiFactory
|
||||
groupData: groupData,
|
||||
isLightfinderContext: isLightfinderContext,
|
||||
lightfinderCid: lightfinderCid,
|
||||
performanceCollector: _performanceCollectorService);
|
||||
performanceCollector: _performanceCollectorService,
|
||||
_apiController);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user