PartyDamage/Plugin.cs

802 lines
28 KiB
C#

using System.Diagnostics;
using System.Globalization;
using System.Text;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.Gui.ContextMenu;
using Dalamud.Game.Text;
using Dalamud.IoC;
using Dalamud.Memory;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Group;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.Graphics;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Info;
using FFXIVClientStructs.FFXIV.Component.GUI;
using FFXIVClientStructs.Interop;
using Lumina.Excel.GeneratedSheets;
namespace PartyDamage;
public class Plugin : IDalamudPlugin {
[PluginService]
internal static IPluginLog Log { get; private set; }
[PluginService]
private IAddonLifecycle AddonLifecycle { get; init; }
[PluginService]
private IClientState ClientState { get; init; }
[PluginService]
private IFramework Framework { get; init; }
[PluginService]
private IPartyList PartyList { get; init; }
[PluginService]
internal IDalamudPluginInterface Interface { get; init; }
[PluginService]
internal ICommandManager CommandManager { get; init; }
[PluginService]
internal IContextMenu ContextMenu { get; init; }
[PluginService]
internal IDataManager DataManager { get; init; }
[PluginService]
internal ICondition Condition { get; init; }
// [PluginService]
// internal ITextureProvider TextureProvider { get; init; }
internal Configuration Config { get; }
private Client Client { get; }
internal PluginUi Ui { get; }
private Commands Commands { get; }
private Stopwatch AlternateWatch { get; } = Stopwatch.StartNew();
private Stopwatch DelayWatch { get; } = new();
private bool _ranLastTick;
private bool _showDps = true;
private bool _reset;
private bool _wasActive;
private bool _manuallyReset = true;
[Signature("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? FE C2")]
internal unsafe delegate* unmanaged<byte*, byte, void> PlaySound;
internal readonly byte[] ManaUsers = [
6, // cnj
7, // thm
19, // pld
24, // whm
25, // blm
26, // acn
27, // smn
28, // sch
32, // drk
33, // ast
35, // rdm
36, // blu
40, // sge
42, // pct
];
[PluginService]
private IGameInteropProvider GameInteropProvider { get; init; }
public Plugin() {
this.Config = this.Interface!.GetPluginConfig() as Configuration ?? new Configuration();
this.Client = new Client();
this.Ui = new PluginUi(this);
this.Commands = new Commands(this);
if (this.Config.ManaModeAlternateOnlyManaUsers) {
this.Config.ManaModeAlternateOnlyManaUsers = false;
this.Config.AlternateJobs.AddRange(this.ManaUsers.Select(cj => (uint) cj));
this.SaveConfig();
}
this.AddonLifecycle!.RegisterListener(AddonEvent.PostRequestedUpdate, "_PartyList", this.UpdateList);
this.AddonLifecycle!.RegisterListener(AddonEvent.PreDraw, "_PartyList", this.UpdateList);
this.ContextMenu!.OnMenuOpened += this.MenuOpened;
this.GameInteropProvider!.InitializeFromAttributes(this);
}
public void Dispose() {
this.ContextMenu.OnMenuOpened -= this.MenuOpened;
this.AddonLifecycle.UnregisterListener(AddonEvent.PreDraw, "_PartyList", this.UpdateList);
this.AddonLifecycle.UnregisterListener(AddonEvent.PostRequestedUpdate, "_PartyList", this.UpdateList);
this.Commands.Dispose();
this.Ui.Dispose();
this.Client.Dispose();
}
internal void SaveConfig() {
this.Interface.SavePluginConfig(this.Config);
}
private unsafe void MenuOpened(IMenuOpenedArgs args) {
var add = args.AddonName == "_PartyList";
if (!add) {
var ctx = AgentContext.Instance();
if (args.AgentPtr != (nint) ctx) {
return;
}
var targetId = ctx->TargetObjectId.ObjectId;
if (targetId == 0xE000_0000) {
return;
}
if (this.ClientState.LocalPlayer is { } player && player.GameObjectId == targetId) {
add = true;
} else {
var group = GroupManager.Instance()->GetGroup();
if (group != null && group->GetPartyMemberByEntityId(targetId) != null) {
add = true;
}
}
}
if (!add) {
return;
}
args.AddMenuItem(new MenuItem {
Name = "Clear parse results",
Prefix = SeIconChar.BoxedLetterP,
PrefixColor = 37,
IsEnabled = this.Client.Data?.IsActive == false,
OnClicked = _ => {
this._reset = true;
this._manuallyReset = true;
},
});
}
private unsafe void UpdateList(AddonEvent type, AddonArgs args) {
var ranLast = this._ranLastTick;
this._ranLastTick = false;
var list = (AddonPartyList*) AtkStage.Instance()->RaptureAtkUnitManager->GetAddonByName("_PartyList");
if (list == null) {
return;
}
var shouldUpdate = false;
var wasActive = this._wasActive;
var becameInactive = false;
if (this.Client.Data is { } data) {
if (wasActive && !data.IsActive) {
becameInactive = true;
}
this._wasActive = data.IsActive;
}
if (this.Client.Data is { IsActive: true }) {
shouldUpdate = true;
this._manuallyReset = false;
} else if (becameInactive) {
if (this.Config.ClearResultsOnInactive) {
this.DelayWatch.Restart();
}
this.HandleEvaluation();
}
if (this.Config.ClearResultsOnInactive && this.DelayWatch.IsRunning && this.DelayWatch.Elapsed >= TimeSpan.FromSeconds(this.Config.ClearDelaySeconds)) {
this.DelayWatch.Reset();
this._reset = true;
}
if (!this._reset && !this._manuallyReset) {
// only do these checks if we shouldn't reset and if we're waiting for active data
// keep running if we're in the delay period
if (this.DelayWatch.IsRunning) {
shouldUpdate = true;
}
// keep running if the user wants to manually clear results
if (!this.Config.ClearResultsOnInactive) {
shouldUpdate = true;
}
}
if (this._reset) {
this.DelayWatch.Reset();
this._reset = false;
this._manuallyReset = true;
this.ResetMembers(list);
}
if (shouldUpdate && this.UpdateListInner(list) && ranLast) {
this.ResetMembers(list);
}
}
private unsafe void HandleEvaluation() {
if (
!this.Config.UseEvaluatorNpc
|| this.ClientState.LocalPlayer is not { } player
|| this.Client.Data == null
) {
return;
}
var evaluator = this.GetEvaluator();
if (evaluator == null) {
return;
}
if (this.Client.Data.Combatants.Count < this.Config.EvaluationMinCombatants) {
return;
}
var combatants = this.Client.Data.Combatants.Values
.Where(combatant => {
var job = this.DataManager.GetExcelSheet<ClassJob>()!
.FirstOrDefault(j => j.Abbreviation.RawString.Equals(combatant.JobAbbr, StringComparison.InvariantCultureIgnoreCase));
return job?.Role == player.ClassJob.GameData?.Role;
})
.ToList();
if (combatants.Count < this.Config.EvaluationMinSameRole) {
return;
}
combatants.Sort((a, b) => a.EncDps.CompareTo(b.EncDps));
var youIndex = combatants.FindIndex(combatant => combatant.Name == "YOU");
if (youIndex == -1) {
return;
}
var rank = (float) (youIndex + 1) / combatants.Count;
var thresholds = this.Config.EvaluationThresholds.ToList();
thresholds.Sort((a, b) => b.Value.CompareTo(a.Value));
foreach (var threshold in thresholds) {
if (rank < threshold.Value) {
continue;
}
var msg = evaluator.GetLineFor(threshold.Key, this.Config.BlendEvaluations, youIndex + 1);
if (msg != null) {
var bytes = msg.EncodeWithNullTerminator();
UIModule.Instance()->ShowBattleTalkImage(
Encoding.UTF8.GetBytes(evaluator.Name),
bytes,
this.Config.EvaluationLength,
evaluator.BattleTalkImage,
6
);
}
break;
}
}
internal Evaluator? GetEvaluator() {
if (this.Config.EvaluatorId != Evaluator.RandomId) {
return Evaluator.Evaluators.FirstOrDefault(e => e.Id == this.Config.EvaluatorId);
}
var pool = Evaluator.Evaluators
.Where(e => this.Config.EvaluatorsRandomEnabled.GetValueOrDefault(e.Id))
.ToArray();
return pool.Length == 0
? null
: Random.Shared.GetItems(pool, 1).GetIndexOrDefault(0);
}
/// <summary>
/// Update the party list.
/// </summary>
/// <returns>true if the list should be reset immediately</returns>
private unsafe bool UpdateListInner(AddonPartyList* list) {
if (this.Client.Data is not { } data) {
return false;
}
if (this.ClientState.LocalPlayer is not { } player) {
return false;
}
var chara = (Character*) player.Address;
var playerName = player.Name.TextValue;
if (list->HoveredIndex >= 0 || list->TargetedIndex >= 0) {
return true;
}
var names = new List<ListInfo>();
var members = AgentHUD.Instance()->PartyMembers;
for (var i = 0; i < members.Length && i < list->PartyMembers.Length; i++) {
var member = members[i];
if (member.Name == null) {
continue;
}
// controller soft target not handled by hoveredindex above
if (chara->GetSoftTargetId() == member.EntityId) {
return true;
}
var name = MemoryHelper.ReadStringNullTerminated((nint) member.Name);
var isChocobo = member.Object != null
&& member.Object->GetObjectKind() == ObjectKind.BattleNpc
&& member.Object->CompanionOwnerId == player.GameObjectId;
if (isChocobo) {
name += " (YOU)";
}
var info = new ListInfo {
Name = name,
IsChocobo = isChocobo,
Mana = member.Object != null ? member.Object->Mana : null,
PartyListIndex = i,
ClassJob = member.Object != null ? member.Object->ClassJob : null,
IsFullMember = member.Object != null,
};
names.Add(info);
}
// check cross realm party for any members we may have missed
var crossInfo = InfoProxyCrossRealm.Instance();
if (crossInfo->IsInCrossRealmParty != 0) {
var crossGroup = crossInfo->CrossRealmGroups[crossInfo->LocalPlayerGroupIndex];
var fullMembers = AgentHUD.Instance()->PartyMembers;
var fullMemberIds = new uint[fullMembers.Length];
for (var i = 0; i < fullMembers.Length; i++) {
fullMemberIds[i] = fullMembers[i].EntityId;
}
for (var i = 0; i < crossGroup.GroupMemberCount && i < list->PartyMembers.Length; i++) {
var member = crossGroup.GroupMembers[i];
var name = member.NameString;
if (string.IsNullOrWhiteSpace(name)) {
continue;
}
if (chara->GetSoftTargetId() == member.EntityId) {
return true;
}
if (names.FirstOrDefault(info => info.Name == name) is { } info) {
info.PartyListIndex = member.MemberIndex;
continue;
}
info = new ListInfo {
Name = name,
IsChocobo = false,
Mana = null,
PartyListIndex = member.MemberIndex,
ClassJob = member.ClassJobId,
IsFullMember = Array.IndexOf(fullMemberIds, member.EntityId) != -1,
};
names.Add(info);
}
}
this._ranLastTick = true;
if (this.AlternateWatch.Elapsed.TotalMilliseconds > this.Config.AlternateSeconds * 1_000) {
this.AlternateWatch.Restart();
this._showDps ^= true;
}
names.Sort((a, b) => a.PartyListIndex.CompareTo(b.PartyListIndex));
for (var i = 0; i < names.Count; i++) {
var info = names[i];
var lookupName = info.Name == playerName
? "YOU"
: info.Name;
if (!data.Combatants.TryGetValue(lookupName, out var combatant)) {
continue;
}
this.UpdateMember(list, info, data.Encounter, combatant, data.Combatants.Values);
}
return false;
}
private static unsafe (Pointer<AtkTextNode>, Pointer<AtkTextNode>) GetTextNodes(AddonPartyList.PartyListMemberStruct unit) {
if (unit.MPGaugeBar == null) {
return (null, null);
}
var left = (AtkTextNode*) unit.MPGaugeBar->GetTextNodeById(2);
var right = (AtkTextNode*) unit.MPGaugeBar->GetTextNodeById(3);
var hasLeft = left != null && left->IsVisible();
var hasRight = right != null && right->IsVisible();
return (
hasLeft ? (Pointer<AtkTextNode>) left : null,
hasRight ? (Pointer<AtkTextNode>) right : null
);
}
private static unsafe void ResetTextColour(AddonPartyList.PartyListMemberStruct unit) {
var (left, right) = GetTextNodes(unit);
if (left.Value == null) {
return;
}
left.Value->TextColor = new ByteColor {
RGBA = 0xFFFFFFFF,
};
left.Value->AddRed = 0;
left.Value->AddGreen = 0;
left.Value->AddBlue = 0;
left.Value->MultiplyRed = 100;
left.Value->MultiplyGreen = 100;
left.Value->MultiplyBlue = 100;
if (right.Value == null) {
return;
}
right.Value->TextColor = left.Value->TextColor;
right.Value->AddRed = left.Value->AddRed;
right.Value->AddGreen = left.Value->AddGreen;
right.Value->AddBlue = left.Value->AddBlue;
right.Value->MultiplyRed = left.Value->MultiplyRed;
right.Value->MultiplyGreen = left.Value->MultiplyGreen;
right.Value->MultiplyBlue = left.Value->MultiplyBlue;
}
private static unsafe void ResetManaText(AddonPartyList.PartyListMemberStruct unit, uint? mana) {
var (left, right) = GetTextNodes(unit);
if (left.Value == null) {
return;
}
if (mana == null) {
if (right.Value != null) {
left.Value->SetText("");
right.Value->SetText("???");
} else {
left.Value->SetText("???");
}
} else {
var manaString = mana.Value.ToString(CultureInfo.InvariantCulture);
if (right.Value != null) {
if (manaString.Length <= 1) {
left.Value->SetText("");
right.Value->SetText(manaString);
} else {
left.Value->SetText(manaString[..^2]);
right.Value->SetText(manaString[^2..]);
}
} else {
left.Value->SetText(manaString);
}
}
}
private unsafe void ResetMember(
AddonPartyList* list,
ListInfo info,
bool includeBar,
bool includeTargeted
) {
var unit = info.IsChocobo
? list->Chocobo
: list->PartyMembers[info.PartyListIndex];
if (includeBar && unit.TargetGlow != null && (includeTargeted || list->TargetedIndex != info.PartyListIndex)) {
unit.TargetGlow->SetAlpha(255);
unit.TargetGlow->SetScaleX(0);
unit.TargetGlow->AddRed = 0;
unit.TargetGlow->AddGreen = 0;
unit.TargetGlow->AddBlue = 0;
unit.TargetGlow->MultiplyRed = 100;
unit.TargetGlow->MultiplyGreen = 100;
unit.TargetGlow->MultiplyBlue = 100;
unit.TargetGlow->ToggleVisibility(true);
unit.TargetGlow->SetWidth(320);
}
if (info.IsChocobo) {
var chocoboTimer = list->GetTextNodeById(5);
var chocoboTimerIcon = list->GetTextNodeById(4);
var hasTimer = chocoboTimer != null && chocoboTimer->IsVisible();
var hasTimerIcon = chocoboTimerIcon != null && chocoboTimerIcon->IsVisible();
if (hasTimer) {
var companion = UIState.Instance()->Buddy.CompanionInfo;
var timeLeft = TimeSpan.FromSeconds(Math.Ceiling(companion.TimeLeft));
chocoboTimer->SetText($"{timeLeft.Minutes:00}:{timeLeft.Seconds:00}");
}
if (hasTimerIcon) {
chocoboTimerIcon->SetText(SeIconChar.Clock.ToIconString());
}
}
ResetTextColour(unit);
ResetManaText(unit, info.Mana);
}
private unsafe void ResetMembers(AddonPartyList* list) {
foreach (var member in list->PartyMembers) {
if (member.TargetGlow != null) {
member.TargetGlow->SetWidth(320);
}
ResetTextColour(member);
}
if (list->Chocobo.TargetGlow != null) {
list->Chocobo.TargetGlow->SetWidth(220);
}
list->OnRequestedUpdate(AtkStage.Instance()->GetNumberArrayData(), AtkStage.Instance()->GetStringArrayData());
// var members = AgentHUD.Instance()->PartyMembers;
// for (var i = 0; i < members.Length && i < list->PartyMembers.Length; i++) {
// var member = members[i];
// var name = MemoryHelper.ReadStringNullTerminated((nint) member.Name);
// var isChocobo = member.Object != null
// && this.ClientState.LocalPlayer is { } player
// && member.Object->GetObjectKind() == ObjectKind.BattleNpc
// && member.Object->CompanionOwnerId == player.GameObjectId;
// if (isChocobo) {
// name += " (YOU)";
// }
//
// var info = new ListInfo {
// Name = name,
// IsChocobo = isChocobo,
// Mana = member.Object != null ? member.Object->Mana : null,
// PartyListIndex = i,
// ClassJob = member.Object != null ? member.Object->ClassJob : null,
// };
//
// this.ResetMember(list, info, true, false);
// }
}
private unsafe string[] GetPartyNames(bool alliance) {
var names = new HashSet<string>();
var cross = InfoProxyCrossRealm.Instance();
if (cross->IsInCrossRealmParty != 0) {
var crossGroup = cross->CrossRealmGroups[cross->LocalPlayerGroupIndex];
foreach (var member in crossGroup.GroupMembers) {
names.Add(member.NameString);
}
}
var group = GroupManager.Instance()->GetGroup();
if (group == null) {
return names.ToArray();
}
foreach (var member in group->PartyMembers) {
names.Add(member.NameString);
}
if (alliance) {
foreach (var member in group->AllianceMembers) {
names.Add(member.NameString);
}
}
return names.ToArray();
}
private unsafe void UpdateMember(
AddonPartyList* list,
ListInfo info,
Encounter encounter,
Combatant combatant,
IEnumerable<Combatant> combatants
) {
var member = info.IsChocobo
? list->Chocobo
: list->PartyMembers[info.PartyListIndex];
if (this.Config.UseDpsBar && member.TargetGlow != null) {
float denominator;
switch (this.Config.DpsBarMode) {
case DpsBarMode.Encounter: {
denominator = combatants
.Select(c => c.EncDps)
.Aggregate(0f, (acc, val) => val > acc ? val : acc);
break;
}
case DpsBarMode.Party: {
var party = this.GetPartyNames(false);
denominator = combatants
.Where(c => c.Name == "YOU" || party.Contains(c.Name) || c.Name.EndsWith(')'))
.Select(c => c.EncDps)
.Aggregate(0f, (acc, val) => val > acc ? val : acc);
break;
}
case DpsBarMode.Alliance: {
var alliance = this.GetPartyNames(true);
denominator = combatants
.Where(c => c.Name == "YOU" || alliance.Contains(c.Name) || c.Name.EndsWith(')'))
.Select(c => c.EncDps)
.Aggregate(0f, (acc, val) => val > acc ? val : acc);
break;
}
default: {
denominator = encounter.EncDps;
break;
}
}
member.TargetGlow->ToggleVisibility(true);
member.TargetGlow->SetAlpha((byte) Math.Round(this.Config.BarAlpha * 255));
// member.TargetGlow->SetScaleX(
// denominator == 0
// ? 0
// : combatant.EncDps / denominator
// );
member.TargetGlow->SetScaleX(1);
member.TargetGlow->AddRed = (byte) Math.Clamp(this.Config.BarAddRed, 0, 255);
member.TargetGlow->AddGreen = (byte) Math.Clamp(this.Config.BarAddGreen, 0, 255);
member.TargetGlow->AddBlue = (byte) Math.Clamp(this.Config.BarAddBlue, 0, 255);
member.TargetGlow->MultiplyRed = (byte) Math.Clamp(this.Config.BarMulRed, 0, 100);
member.TargetGlow->MultiplyGreen = (byte) Math.Clamp(this.Config.BarMulGreen, 0, 100);
member.TargetGlow->MultiplyBlue = (byte) Math.Clamp(this.Config.BarMulBlue, 0, 100);
member.TargetGlow->PartId = 3;
// member.TargetGlow->SetWidth(320);
member.TargetGlow->SetWidth(
(ushort) Math.Clamp(
Math.Round(320 * (denominator == 0
? 1
: combatant.EncDps / denominator)),
10,
320
)
);
member.TargetGlow->SetPositionShort(47, 21);
}
var inCombat = this.Condition[ConditionFlag.InCombat];
if (this.Config.Mode == MeterMode.Mana && info.IsChocobo) {
var chocoboTimer = list->GetTextNodeById(5);
var chocoboTimerIcon = list->GetTextNodeById(4);
var hasTimer = chocoboTimer != null && chocoboTimer->IsVisible();
var hasTimerIcon = chocoboTimerIcon != null && chocoboTimerIcon->IsVisible();
if (this.Config is { Alternate: true, AlternateChocobo: true } && (!this.Config.OnlyAlternateInCombat || inCombat) && !this._showDps) {
this.ResetMember(list, info, false, false);
} else {
if (hasTimer) {
var (leftText, rightText) = GetDpsText(combatant);
chocoboTimer->SetText($"{leftText}{rightText}");
}
if (hasTimerIcon) {
chocoboTimerIcon->SetText("\ue05e");
}
}
}
if (this.Config.Mode == MeterMode.Mana && member.MPGaugeBar != null) {
var left = (AtkTextNode*) member.MPGaugeBar->GetTextNodeById(2);
var right = (AtkTextNode*) member.MPGaugeBar->GetTextNodeById(3);
if (left == null) {
return;
}
if (info is { IsFullMember: false, IsChocobo: false } && InfoProxyCrossRealm.Instance()->IsInCrossRealmParty != 0) {
var gauge = left->ParentNode;
gauge->ToggleVisibility(true);
left->ToggleVisibility(true);
if (right != null) {
right->ToggleVisibility(true);
}
for (var sibling = left->NextSiblingNode; sibling != null; sibling = sibling->NextSiblingNode) {
if (sibling->NodeId is not (6 or 7)) {
continue;
}
sibling->ToggleVisibility(false);
}
}
var hasLeft = left->IsVisible();
var hasRight = right != null && right->IsVisible();
if (!hasLeft) {
return;
}
if (this.Config.Alternate && (!this.Config.OnlyAlternateInCombat || inCombat) && !this._showDps && info.ClassJob is { } classJob) {
var shouldAlternate = info.IsFullMember && this.Config.AlternateJobs.Contains(classJob);
if (shouldAlternate) {
this.ResetMember(list, info, false, false);
return;
}
}
SetTextNodeColor(left);
if (hasRight) {
SetTextNodeColor(right);
}
var (leftText, rightText) = GetDpsText(combatant);
if (hasRight) {
left->SetText(leftText);
right->SetText(rightText);
} else {
left->SetText($"{leftText}{rightText}");
}
}
return;
(string, string) GetDpsText(Combatant combatant) {
string leftText, rightText;
if (combatant.EncDps == 0 || float.IsInfinity(combatant.EncDps) || float.IsNaN(combatant.EncDps)) {
leftText = "0.";
rightText = "00";
} else if (combatant.EncDps < 1_000) {
var dps = Math.Round(combatant.EncDps * 100).ToString(CultureInfo.InvariantCulture);
leftText = $"{dps[..^2]}.";
rightText = dps[^2..];
} else if (combatant.EncDps < 1_000_000) {
var dps = Math.Round(combatant.EncDps / 100).ToString(CultureInfo.InvariantCulture);
leftText = $"{dps[..^1]}.";
rightText = $"{dps[^1..]}K";
} else if (combatant.EncDps < 1_000_000_000) {
var dps = Math.Round(combatant.EncDps / 100_000).ToString(CultureInfo.InvariantCulture);
leftText = $"{dps[..^1]}.";
rightText = $"{dps[^1..]}M";
} else {
var dps = Math.Round(combatant.EncDps / 100_000_000).ToString(CultureInfo.InvariantCulture);
leftText = $"{dps[..^1]}.";
rightText = $"{dps[^1..]}B";
}
return (leftText, rightText);
}
void SetTextNodeColor(AtkTextNode* node) {
node->TextColor = new ByteColor {
RGBA = this.Config.TextColour,
};
node->AddRed = (byte) Math.Clamp(this.Config.TextAddRed, 0, 255);
node->AddGreen = (byte) Math.Clamp(this.Config.TextAddGreen, 0, 255);
node->AddBlue = (byte) Math.Clamp(this.Config.TextAddBlue, 0, 255);
node->MultiplyRed = (byte) Math.Clamp(this.Config.TextMulRed, 0, 100);
node->MultiplyGreen = (byte) Math.Clamp(this.Config.TextMulGreen, 0, 100);
node->MultiplyBlue = (byte) Math.Clamp(this.Config.TextMulBlue, 0, 100);
}
}
}
public class ListInfo {
public required string Name { get; init; }
public required bool IsChocobo { get; init; }
public required int PartyListIndex { get; set; }
public required uint? Mana { get; init; }
public required byte? ClassJob { get; init; }
public required bool IsFullMember { get; init; }
}