All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m27s
2.0.0 Changes: - Reworked shell finder UI with compact or list view with profile tags showing with the listing, allowing moderators to broadcast the syncshell as well to have it be used more. - Reworked user list in syncshell admin screen to have filter visible and moved away from table to its own thing, allowing to copy uid/note/alias when clicking on the name. - Reworked download bars and download box to make it look more modern, removed the jitter around, so it shouldn't vibrate around much. - Chat has been added to the top menu, working in Zone or in Syncshells to be used there. - Paired system has been revamped to make pausing and unpausing faster, and loading people should be faster as well. - Moved to the internal object table to have faster load times for users; people should load in faster - Compactor is running on a multi-threaded level instead of single-threaded; this should increase the speed of compacting files - Nameplate Service has been reworked so it wouldn't use the nameplate handler anymore. - Files can be resized when downloading to reduce load on users if they aren't compressed. (can be toggled to resize all). - Penumbra Collections are now only made when people are visible, reducing the load on boot-up when having many syncshells in your list. - Lightfinder plates have been moved away from using Nameplates, but will use an overlay. - Main UI has been changed a bit with a gradient, and on hover will glow up now. - Reworked Profile UI for Syncshell and Users to be more user-facing with more customizable items. - Reworked Settings UI to look more modern. - Performance should be better due to new systems that would dispose of the collections and better caching of items. Co-authored-by: defnotken <itsdefnotken@gmail.com> Co-authored-by: azyges <aaaaaa@aaa.aaa> Co-authored-by: choco <choco@patat.nl> Co-authored-by: cake <admin@cakeandbanana.nl> Co-authored-by: Minmoose <KennethBohr@outlook.com> Reviewed-on: #92
254 lines
9.0 KiB
C#
254 lines
9.0 KiB
C#
using Dalamud.Game.ClientState.Objects.Enums;
|
|
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.Services;
|
|
using Microsoft.Extensions.Logging;
|
|
using System.Numerics;
|
|
using static LightlessSync.UI.DtrEntry;
|
|
using LSeStringBuilder = Lumina.Text.SeStringBuilder;
|
|
|
|
namespace LightlessSync.Services;
|
|
|
|
/// <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 IGameGui _gameGui;
|
|
private readonly IObjectTable _objectTable;
|
|
private readonly PairUiService _pairUiService;
|
|
|
|
public NameplateService(ILogger<NameplateService> logger,
|
|
LightlessConfigService configService,
|
|
IClientState clientState,
|
|
IGameGui gameGui,
|
|
IObjectTable objectTable,
|
|
IGameInteropProvider interop,
|
|
LightlessMediator lightlessMediator,
|
|
PairUiService pairUiService) : base(logger, lightlessMediator)
|
|
{
|
|
_logger = logger;
|
|
_configService = configService;
|
|
_clientState = clientState;
|
|
_gameGui = gameGui;
|
|
_objectTable = objectTable;
|
|
_pairUiService = pairUiService;
|
|
|
|
interop.InitializeFromAttributes(this);
|
|
_nameplateHook?.Enable();
|
|
Refresh();
|
|
|
|
Mediator.Subscribe<VisibilityChange>(this, (_) => Refresh());
|
|
}
|
|
|
|
/// <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)
|
|
{
|
|
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();
|
|
|
|
//Check if player should be colored
|
|
if (!ShouldColorPlayer(player, visibleUsersIds))
|
|
return;
|
|
|
|
var originalName = player.Name.ToString();
|
|
|
|
//Check if not null of the name
|
|
if (string.IsNullOrEmpty(originalName))
|
|
return;
|
|
|
|
//Check if any characters/symbols are forbidden
|
|
if (HasForbiddenSeStringChars(originalName))
|
|
return;
|
|
|
|
//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);
|
|
|
|
//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()
|
|
{
|
|
Refresh();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Toggles the refresh of the Nameplate addon
|
|
/// </summary>
|
|
protected void Refresh()
|
|
{
|
|
AtkUnitBasePtr namePlateAddon = _gameGui.GetAddonByName("NamePlate");
|
|
|
|
if (namePlateAddon.IsNull)
|
|
{
|
|
_logger.LogInformation("NamePlate addon is null, cannot refresh nameplates.");
|
|
return;
|
|
}
|
|
|
|
var addonNamePlate = (AddonNamePlate*)namePlateAddon.Address;
|
|
|
|
if (addonNamePlate == null)
|
|
{
|
|
_logger.LogInformation("addonNamePlate addon is null, cannot refresh nameplates.");
|
|
return;
|
|
}
|
|
|
|
addonNamePlate->DoFullUpdate = 1;
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
_nameplateHook?.Dispose();
|
|
}
|
|
|
|
base.Dispose(disposing);
|
|
}
|
|
} |