Redone nameplate service (#93)
Co-authored-by: cake <admin@cakeandbanana.nl> Reviewed-on: #93 Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
This commit was merged in pull request #93.
This commit is contained in:
@@ -340,8 +340,8 @@ public sealed class Plugin : IDalamudPlugin
|
||||
s.GetRequiredService<CacheMonitor>(), s.GetRequiredService<FileDialogManager>(), s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<DalamudUtilService>(),
|
||||
pluginInterface, textureProvider, s.GetRequiredService<Dalamud.Localization>(), s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<TokenProvider>(),
|
||||
s.GetRequiredService<LightlessMediator>()));
|
||||
collection.AddScoped((s) => new NameplateService(s.GetRequiredService<ILogger<NameplateService>>(), s.GetRequiredService<LightlessConfigService>(), namePlateGui, clientState,
|
||||
s.GetRequiredService<PairUiService>(), s.GetRequiredService<LightlessMediator>()));
|
||||
collection.AddScoped((s) => new NameplateService(s.GetRequiredService<ILogger<NameplateService>>(), s.GetRequiredService<LightlessConfigService>(), clientState, gameGui, objectTable, gameInteropProvider,
|
||||
s.GetRequiredService<LightlessMediator>(),s.GetRequiredService<PairUiService>()));
|
||||
collection.AddScoped((s) => new NameplateHandler(s.GetRequiredService<ILogger<NameplateHandler>>(), addonLifecycle, gameGui, s.GetRequiredService<DalamudUtilService>(),
|
||||
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<LightlessMediator>(), clientState, s.GetRequiredService<PairUiService>()));
|
||||
|
||||
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// NameplateService is used for coloring our nameplates based on the settings of the user.
|
||||
/// </summary>
|
||||
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<UpdateNameplateDelegate>? _nameplateHook = null;
|
||||
|
||||
private readonly ILogger<NameplateService> _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<NameplateService> 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<VisibilityChange>(this, (_) => _namePlateGui.RequestRedraw());
|
||||
interop.InitializeFromAttributes(this);
|
||||
_nameplateHook?.Enable();
|
||||
Refresh();
|
||||
|
||||
Mediator.Subscribe<VisibilityChange>(this, (_) => Refresh());
|
||||
}
|
||||
|
||||
private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
||||
/// <summary>
|
||||
/// 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 <see cref="SetNameplate"/>,
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine if the player should be colored based on conditions (isFriend, IsInParty)
|
||||
/// </summary>
|
||||
/// <param name="playerCharacter">Player character that will be checked</param>
|
||||
/// <param name="visibleUserIds">All visible users in the current object table</param>
|
||||
/// <returns>PLayer should or shouldnt be colored based on the result. True means colored</returns>
|
||||
private bool ShouldColorPlayer(IPlayerCharacter playerCharacter, HashSet<ulong> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setting up the nameplate of the user to be colored
|
||||
/// </summary>
|
||||
/// <param name="namePlateInfo">Information given from the Signature to be updated</param>
|
||||
/// <param name="battleChara">Character from FF</param>
|
||||
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());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts Uint code to Vector4 as we store Colors in Uint in our config, needed for lumina
|
||||
/// </summary>
|
||||
/// <param name="rgb">Color code</param>
|
||||
/// <returns>Vector4 Color</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the string has any forbidden characters/symbols as the string builder wouldnt append.
|
||||
/// </summary>
|
||||
/// <param name="s">String that has to be checked</param>
|
||||
/// <returns>Contains forbidden characters/symbols or not</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps the given string with the given edge and text color.
|
||||
/// </summary>
|
||||
/// <param name="text">String that has to be wrapped</param>
|
||||
/// <param name="edgeColor">Edge(border) color</param>
|
||||
/// <param name="textColor">Text color</param>
|
||||
/// <returns>Color wrapped SeString</returns>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request redraw of nameplates
|
||||
/// </summary>
|
||||
public void RequestRedraw()
|
||||
{
|
||||
_namePlateGui.RequestRedraw();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private static (SeString, SeString) CreateTextWrap(DtrEntry.Colors color)
|
||||
/// <summary>
|
||||
/// Toggles the refresh of the Nameplate addon
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user