diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 542d9a1..1842989 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -340,8 +340,8 @@ public sealed class Plugin : IDalamudPlugin s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), pluginInterface, textureProvider, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); - collection.AddScoped((s) => new NameplateService(s.GetRequiredService>(), s.GetRequiredService(), namePlateGui, clientState, - s.GetRequiredService(), s.GetRequiredService())); + collection.AddScoped((s) => new NameplateService(s.GetRequiredService>(), s.GetRequiredService(), clientState, gameGui, objectTable, gameInteropProvider, + s.GetRequiredService(),s.GetRequiredService())); collection.AddScoped((s) => new NameplateHandler(s.GetRequiredService>(), addonLifecycle, gameGui, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), clientState, s.GetRequiredService())); diff --git a/LightlessSync/Services/NameplateService.cs b/LightlessSync/Services/NameplateService.cs index 84b6d64..4ca8a2f 100644 --- a/LightlessSync/Services/NameplateService.cs +++ b/LightlessSync/Services/NameplateService.cs @@ -1,115 +1,254 @@ using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Game.Gui.NamePlate; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.NativeWrapper; using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Hooking; using Dalamud.Plugin.Services; using Dalamud.Utility; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Component.GUI; using LightlessSync.LightlessConfiguration; using LightlessSync.Services.Mediator; -using LightlessSync.UI; using LightlessSync.UI.Services; using Microsoft.Extensions.Logging; +using System.Numerics; +using static LightlessSync.UI.DtrEntry; +using LSeStringBuilder = Lumina.Text.SeStringBuilder; namespace LightlessSync.Services; -public class NameplateService : DisposableMediatorSubscriberBase +/// +/// NameplateService is used for coloring our nameplates based on the settings of the user. +/// +public unsafe class NameplateService : DisposableMediatorSubscriberBase { + private delegate nint UpdateNameplateDelegate(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo, NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex); + + // Glyceri, Thanks :bow: + [Signature("40 53 55 57 41 56 48 81 EC ?? ?? ?? ?? 48 8B 84 24", DetourName = nameof(UpdateNameplateDetour))] + private readonly Hook? _nameplateHook = null; + private readonly ILogger _logger; private readonly LightlessConfigService _configService; private readonly IClientState _clientState; - private readonly INamePlateGui _namePlateGui; + private readonly IGameGui _gameGui; + private readonly IObjectTable _objectTable; private readonly PairUiService _pairUiService; public NameplateService(ILogger logger, LightlessConfigService configService, - INamePlateGui namePlateGui, IClientState clientState, - PairUiService pairUiService, - LightlessMediator lightlessMediator) : base(logger, lightlessMediator) + IGameGui gameGui, + IObjectTable objectTable, + IGameInteropProvider interop, + LightlessMediator lightlessMediator, + PairUiService pairUiService) : base(logger, lightlessMediator) { _logger = logger; _configService = configService; - _namePlateGui = namePlateGui; _clientState = clientState; + _gameGui = gameGui; + _objectTable = objectTable; _pairUiService = pairUiService; - _namePlateGui.OnNamePlateUpdate += OnNamePlateUpdate; - _namePlateGui.RequestRedraw(); - Mediator.Subscribe(this, (_) => _namePlateGui.RequestRedraw()); + interop.InitializeFromAttributes(this); + _nameplateHook?.Enable(); + Refresh(); + + Mediator.Subscribe(this, (_) => Refresh()); } - private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList handlers) + /// + /// Detour for the game's internal nameplate update function. + /// This will be called whenever the client updates any nameplate. + /// + /// We hook into it to apply our own nameplate coloring logic via , + /// + private nint UpdateNameplateDetour(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo, NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex) { - if (!_configService.Current.IsNameplateColorsEnabled || (_configService.Current.IsNameplateColorsEnabled && _clientState.IsPvPExcludingDen)) + try + { + SetNameplate(namePlateInfo, battleChara); + } + catch (Exception e) + { + _logger.LogError(e, "Error in NameplateService UpdateNameplateDetour"); + } + + return _nameplateHook!.Original(raptureAtkModule, namePlateInfo, numArray, stringArray, battleChara, numArrayIndex, stringArrayIndex); + } + + /// + /// Determine if the player should be colored based on conditions (isFriend, IsInParty) + /// + /// Player character that will be checked + /// All visible users in the current object table + /// PLayer should or shouldnt be colored based on the result. True means colored + private bool ShouldColorPlayer(IPlayerCharacter playerCharacter, HashSet visibleUserIds) + { + if (!visibleUserIds.Contains(playerCharacter.GameObjectId)) + return false; + + var isInParty = playerCharacter.StatusFlags.HasFlag(StatusFlags.PartyMember); + var isFriend = playerCharacter.StatusFlags.HasFlag(StatusFlags.Friend); + + bool partyColorAllowed = _configService.Current.overridePartyColor && isInParty; + bool friendColorAllowed = _configService.Current.overrideFriendColor && isFriend; + + if ((isInParty && !partyColorAllowed) || (isFriend && !friendColorAllowed)) + return false; + + return true; + } + + /// + /// Setting up the nameplate of the user to be colored + /// + /// Information given from the Signature to be updated + /// Character from FF + private void SetNameplate(RaptureAtkModule.NamePlateInfo* namePlateInfo, BattleChara* battleChara) + { + if (!_configService.Current.IsNameplateColorsEnabled || _clientState.IsPvPExcludingDen) + return; + if (namePlateInfo == null || battleChara == null) + return; + + var obj = _objectTable.FirstOrDefault(o => o.Address == (nint)battleChara); + if (obj is not IPlayerCharacter player) return; var snapshot = _pairUiService.GetSnapshot(); var visibleUsersIds = snapshot.PairsByUid.Values - .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) - .Select(u => (ulong)u.PlayerCharacterId) - .ToHashSet(); + .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) + .Select(u => (ulong)u.PlayerCharacterId) + .ToHashSet(); - var now = DateTime.UtcNow; - var colors = _configService.Current.NameplateColors; + //Check if player should be colored + if (!ShouldColorPlayer(player, visibleUsersIds)) + return; - foreach (var handler in handlers) - { - var playerCharacter = handler.PlayerCharacter; - if (playerCharacter == null) - continue; + var originalName = player.Name.ToString(); - var isInParty = playerCharacter.StatusFlags.HasFlag(StatusFlags.PartyMember); - var isFriend = playerCharacter.StatusFlags.HasFlag(StatusFlags.Friend); - bool partyColorAllowed = (_configService.Current.overridePartyColor && isInParty); - bool friendColorAllowed = (_configService.Current.overrideFriendColor && isFriend); + //Check if not null of the name + if (string.IsNullOrEmpty(originalName)) + return; - if (visibleUsersIds.Contains(handler.GameObjectId) && - !( - (isInParty && !partyColorAllowed) || - (isFriend && !friendColorAllowed) - )) - { - handler.NameParts.TextWrap = CreateTextWrap(colors); + //Check if any characters/symbols are forbidden + if (HasForbiddenSeStringChars(originalName)) + return; - if (_configService.Current.overrideFcTagColor) - { - bool hasActualFcTag = playerCharacter.CompanyTag.TextValue.Length > 0; - bool isFromDifferentRealm = playerCharacter.HomeWorld.RowId != playerCharacter.CurrentWorld.RowId; - bool shouldColorFcArea = hasActualFcTag || (!hasActualFcTag && isFromDifferentRealm); + //Swap color channels as we store them in BGR format as FF loves that + var cfgColors = SwapColorChannels(_configService.Current.NameplateColors); + var coloredName = WrapStringInColor(originalName, cfgColors.Glow, cfgColors.Foreground); - if (shouldColorFcArea) - { - handler.FreeCompanyTagParts.OuterWrap = CreateTextWrap(colors); - handler.FreeCompanyTagParts.TextWrap = CreateTextWrap(colors); - } - } - } - } + //Replace string of nameplate with our colored one + namePlateInfo->Name.SetString(coloredName.EncodeWithNullTerminator()); } + /// + /// Converts Uint code to Vector4 as we store Colors in Uint in our config, needed for lumina + /// + /// Color code + /// Vector4 Color + private static Vector4 RgbUintToVector4(uint rgb) + { + float r = ((rgb >> 16) & 0xFF) / 255f; + float g = ((rgb >> 8) & 0xFF) / 255f; + float b = (rgb & 0xFF) / 255f; + return new Vector4(r, g, b, 1f); + } + + /// + /// Checks if the string has any forbidden characters/symbols as the string builder wouldnt append. + /// + /// String that has to be checked + /// Contains forbidden characters/symbols or not + private static bool HasForbiddenSeStringChars(string s) + { + if (string.IsNullOrEmpty(s)) + return false; + + foreach (var ch in s) + { + if (ch == '\0' || ch == '\u0002') + return true; + } + + return false; + } + + /// + /// Wraps the given string with the given edge and text color. + /// + /// String that has to be wrapped + /// Edge(border) color + /// Text color + /// Color wrapped SeString + public static SeString WrapStringInColor(string text, uint? edgeColor = null, uint? textColor = null) + { + if (string.IsNullOrEmpty(text)) + return SeString.Empty; + + var builder = new LSeStringBuilder(); + + if (textColor is uint tc) + builder.PushColorRgba(RgbUintToVector4(tc)); + + if (edgeColor is uint ec) + builder.PushEdgeColorRgba(RgbUintToVector4(ec)); + + builder.Append(text); + + if (edgeColor != null) + builder.PopEdgeColor(); + + if (textColor != null) + builder.PopColor(); + + return builder.ToReadOnlySeString().ToDalamudString(); + } + + /// + /// Request redraw of nameplates + /// public void RequestRedraw() { - _namePlateGui.RequestRedraw(); + Refresh(); } - private static (SeString, SeString) CreateTextWrap(DtrEntry.Colors color) + /// + /// Toggles the refresh of the Nameplate addon + /// + protected void Refresh() { - var left = new Lumina.Text.SeStringBuilder(); - var right = new Lumina.Text.SeStringBuilder(); + AtkUnitBasePtr namePlateAddon = _gameGui.GetAddonByName("NamePlate"); - left.PushColorRgba(color.Foreground); - right.PopColor(); + if (namePlateAddon.IsNull) + { + _logger.LogInformation("NamePlate addon is null, cannot refresh nameplates."); + return; + } - left.PushEdgeColorRgba(color.Glow); - right.PopEdgeColor(); + var addonNamePlate = (AddonNamePlate*)namePlateAddon.Address; - return (left.ToReadOnlySeString().ToDalamudString(), right.ToReadOnlySeString().ToDalamudString()); + if (addonNamePlate == null) + { + _logger.LogInformation("addonNamePlate addon is null, cannot refresh nameplates."); + return; + } + + addonNamePlate->DoFullUpdate = 1; } protected override void Dispose(bool disposing) { - base.Dispose(disposing); + if (disposing) + { + _nameplateHook?.Dispose(); + } - _namePlateGui.OnNamePlateUpdate -= OnNamePlateUpdate; - _namePlateGui.RequestRedraw(); + base.Dispose(disposing); } } \ No newline at end of file diff --git a/LightlessSync/UI/DtrEntry.cs b/LightlessSync/UI/DtrEntry.cs index f965181..8251fb2 100644 --- a/LightlessSync/UI/DtrEntry.cs +++ b/LightlessSync/UI/DtrEntry.cs @@ -490,7 +490,7 @@ public sealed class DtrEntry : IDisposable, IHostedService private const byte _colorTypeForeground = 0x13; private const byte _colorTypeGlow = 0x14; - private static Colors SwapColorChannels(Colors colors) + internal static Colors SwapColorChannels(Colors colors) => new(SwapColorComponent(colors.Foreground), SwapColorComponent(colors.Glow)); private static uint SwapColorComponent(uint color) diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 0e391ad..1934d85 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -2244,7 +2244,6 @@ public class SettingsUi : WindowMediatorSubscriberBase var nameColors = _configService.Current.NameplateColors; var isFriendOverride = _configService.Current.overrideFriendColor; var isPartyOverride = _configService.Current.overridePartyColor; - var isFcTagOverride = _configService.Current.overrideFcTagColor; if (ImGui.Checkbox("Override name color of visible paired players", ref nameColorsEnabled)) { @@ -2278,13 +2277,6 @@ public class SettingsUi : WindowMediatorSubscriberBase _configService.Save(); _nameplateService.RequestRedraw(); } - - if (ImGui.Checkbox("Override FC tag color", ref isFcTagOverride)) - { - _configService.Current.overrideFcTagColor = isFcTagOverride; - _configService.Save(); - _nameplateService.RequestRedraw(); - } } ImGui.Spacing();