29 Commits

Author SHA1 Message Date
86686bf0ca version bump 2024-07-20 09:49:22 +02:00
8d92c79822 fix framed colors 2024-07-20 09:48:34 +02:00
9900230520 version bump 2024-07-20 09:25:55 +02:00
a9264fdd21 add missing icons 2024-07-20 09:25:10 +02:00
331bd63e2e generate package on build 2024-07-18 06:35:16 +02:00
dfc85d7890 add nuget publication script 2024-07-18 06:32:34 +02:00
ddd6954142 update readme 2024-07-18 06:31:29 +02:00
22c3497fec update readme 2024-07-18 06:31:07 +02:00
675225658c don't apply changes when no changes 2024-07-17 19:37:36 +02:00
8273c68318 use one single instance for all plugins 2024-07-17 15:08:16 +02:00
7f41e593c3 make constructor public 2024-07-17 15:08:05 +02:00
ab3bf20ffa update readme 2024-07-17 15:02:08 +02:00
4b21e569a0 temporary add NamePlateGui service
from https://github.com/goatcorp/Dalamud/pull/1915
2024-07-17 14:40:53 +02:00
bfd5cd1b12 use OuterWrap property for title and fc tag 2024-07-11 14:46:13 +02:00
83fdf5613e bump version 2024-07-10 13:51:39 +02:00
545a812b66 migrate to new api & remove nameplate feature 2024-07-10 13:40:33 +02:00
cb2cd8e32c apply code formatting 2024-07-10 13:40:32 +02:00
e699a758f3 use IDatamudPluginInterface 2024-07-10 13:40:22 +02:00
e6e90cdcd3 Merge branch 'master' of https://github.com/Pilzinsel64/Pilz.Dalamud 2024-03-23 12:46:59 +01:00
e7ce31dc0b target .net 8 2024-03-23 12:41:07 +01:00
1e9bd2484a Update .gitignore 2023-11-20 13:01:25 +01:00
a4d3e8b0f3 v0.5.2.1 2023-10-07 11:25:28 +02:00
5695a0823b Dispose, not Disable!!! 2023-10-07 11:25:16 +02:00
22475d7223 v0.5.2 2023-10-07 11:18:21 +02:00
348d8f50e6 fix hook leak 2023-10-07 11:18:05 +02:00
5f78d24c78 v0.4.1 2023-10-06 21:00:44 +02:00
0af7217e63 optimize handling of disposed hooks 2023-10-06 21:00:17 +02:00
e01ebcde37 migrate to GameConfig & cleanup warnings 2023-10-03 09:43:09 +02:00
d127662959 some adjustments for API9 2023-10-03 09:43:09 +02:00
60 changed files with 2362 additions and 1815 deletions

5
.gitignore vendored
View File

@@ -360,4 +360,7 @@ MigrationBackup/
.ionide/ .ionide/
# Fody - auto-generated XML schema # Fody - auto-generated XML schema
FodyWeavers.xsd FodyWeavers.xsd
# Idea
.idea

View File

@@ -1,25 +1,18 @@
using System; namespace Pilz.Dalamud.ActivityContexts;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.ActivityContexts public class ActivityContext
{ {
public class ActivityContext public ActivityType ActivityType { get; init; }
public ZoneType ZoneType { get; init; }
public ActivityContext(ActivityType activityType, ZoneType zoneType)
{ {
public ActivityType ActivityType { get; init; } ActivityType = activityType;
public ZoneType ZoneType { get; init; } ZoneType = zoneType;
}
public ActivityContext(ActivityType activityType, ZoneType zoneType) public bool IsInDuty
{ {
ActivityType = activityType; get => ZoneType != ZoneType.Overworld;
ZoneType = zoneType;
}
public bool IsInDuty
{
get => ZoneType != ZoneType.Overworld;
}
} }
} }

View File

@@ -1,89 +1,82 @@
using Dalamud.Logging; using Lumina.Excel;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.ActivityContexts namespace Pilz.Dalamud.ActivityContexts;
public class ActivityContextManager : IDisposable
{ {
public class ActivityContextManager : IDisposable public delegate void ActivityContextChangedEventHandler(ActivityContextManager sender, ActivityContext activityContext);
public event ActivityContextChangedEventHandler ActivityContextChanged;
private readonly ExcelSheet<ContentFinderCondition> contentFinderConditionsSheet;
public ActivityContext CurrentActivityContext { get; protected set; }
public ActivityContextManager()
{ {
public delegate void ActivityContextChangedEventHandler(ActivityContextManager sender, ActivityContext activityContext); // Get condition sheet
public event ActivityContextChangedEventHandler ActivityContextChanged; contentFinderConditionsSheet = PluginServices.DataManager.GameData.GetExcelSheet<ContentFinderCondition>();
private readonly ExcelSheet<ContentFinderCondition> contentFinderConditionsSheet; // Checks current territory type (if enabled/installed during a dutiy e.g.)
CheckCurrentTerritory();
public ActivityContext CurrentActivityContext { get; protected set; } // Enable event for automatic checks
PluginServices.ClientState.TerritoryChanged += ClientState_TerritoryChanged;
}
public ActivityContextManager() public void Dispose()
{
PluginServices.ClientState.TerritoryChanged -= ClientState_TerritoryChanged;
}
private void ClientState_TerritoryChanged(ushort obj)
{
CheckCurrentTerritory();
}
private void CheckCurrentTerritory()
{
var content = contentFinderConditionsSheet.FirstOrDefault(c => c.TerritoryType.Row == PluginServices.ClientState.TerritoryType);
ActivityType newActivityContext;
ZoneType newZoneType;
if (content == null)
{ {
// Get condition sheet // No content found, so we must be on the overworld
contentFinderConditionsSheet = PluginServices.DataManager.GameData.GetExcelSheet<ContentFinderCondition>(); newActivityContext = ActivityType.None;
newZoneType = ZoneType.Overworld;
// Checks current territory type (if enabled/installed during a dutiy e.g.)
CheckCurrentTerritory();
// Enable event for automatic checks
PluginServices.ClientState.TerritoryChanged += ClientState_TerritoryChanged;
} }
else
public void Dispose()
{ {
PluginServices.ClientState.TerritoryChanged -= ClientState_TerritoryChanged; if (content.PvP)
}
private void ClientState_TerritoryChanged(object? sender, ushort e)
{
CheckCurrentTerritory();
}
private void CheckCurrentTerritory()
{
var content = contentFinderConditionsSheet.FirstOrDefault(c => c.TerritoryType.Row == PluginServices.ClientState.TerritoryType);
ActivityType newActivityContext;
ZoneType newZoneType;
if (content == null)
{ {
// No content found, so we must be on the overworld newActivityContext = ActivityType.PvpDuty;
newActivityContext = ActivityType.None; newZoneType = ZoneType.Pvp;
newZoneType = ZoneType.Overworld;
} }
else else
{ {
if (content.PvP) newActivityContext = ActivityType.PveDuty;
{
newActivityContext = ActivityType.PvpDuty;
newZoneType = ZoneType.Pvp;
}
else
{
newActivityContext = ActivityType.PveDuty;
// Find correct member type // Find correct member type
var memberType = content.ContentMemberType.Row; var memberType = content.ContentMemberType.Row;
if (content.RowId == 16 || content.RowId == 15) if (content.RowId == 16 || content.RowId == 15)
memberType = 2; // Praetorium and Castrum Meridianum memberType = 2; // Praetorium and Castrum Meridianum
else if (content.RowId == 735 || content.RowId == 778) else if (content.RowId == 735 || content.RowId == 778)
memberType = 127; // Bozja memberType = 127; // Bozja
// Check for ZoneType // Check for ZoneType
newZoneType = memberType switch newZoneType = memberType switch
{ {
2 => ZoneType.Doungen, 2 => ZoneType.Doungen,
3 => ZoneType.Raid, 3 => ZoneType.Raid,
4 => ZoneType.AllianceRaid, 4 => ZoneType.AllianceRaid,
127 => ZoneType.Foray, 127 => ZoneType.Foray,
_ => ZoneType.Doungen, _ => ZoneType.Doungen,
}; };
}
} }
CurrentActivityContext = new(newActivityContext, newZoneType);
ActivityContextChanged?.Invoke(this, CurrentActivityContext);
} }
CurrentActivityContext = new(newActivityContext, newZoneType);
ActivityContextChanged?.Invoke(this, CurrentActivityContext);
} }
} }

View File

@@ -1,18 +1,12 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.ActivityContexts namespace Pilz.Dalamud.ActivityContexts;
[JsonConverter(typeof(StringEnumConverter))]
public enum ActivityType
{ {
[JsonConverter(typeof(StringEnumConverter))] None = 0x0,
public enum ActivityType PveDuty = 0x1,
{ PvpDuty = 0x2
None = 0x0,
PveDuty = 0x1,
PvpDuty = 0x2
}
} }

View File

@@ -1,22 +1,16 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.ActivityContexts namespace Pilz.Dalamud.ActivityContexts;
[Flags, JsonConverter(typeof(StringEnumConverter))]
public enum ZoneType
{ {
[Flags, JsonConverter(typeof(StringEnumConverter))] Overworld = 1,
public enum ZoneType Doungen = 2,
{ Raid = 4,
Overworld = 1, AllianceRaid = 8,
Doungen = 2, Foray = 16,
Raid = 4, Pvp = 32,
AllianceRaid = 8, Everywhere = int.MaxValue
Foray = 16,
Pvp = 32,
Everywhere = int.MaxValue
}
} }

View File

@@ -1,40 +1,34 @@
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud namespace Pilz.Dalamud;
public static class Extensions
{ {
public static class Extensions /// <summary>
/// Removes a Payload from a given SeString.
/// Using <code>SeString.Payloads.Remove()</code> does not use the reference to compare for some reason. Tis is a workaround.
/// </summary>
/// <param name="seString"></param>
/// <param name="payload"></param>
public static void Remove(this SeString seString, Payload payload)
{ {
/// <summary> Remove(seString.Payloads, payload);
/// Removes a Payload from a given SeString. }
/// Using <code>SeString.Payloads.Remove()</code> does not use the reference to compare for some reason. Tis is a workaround.
/// </summary>
/// <param name="seString"></param>
/// <param name="payload"></param>
public static void Remove(this SeString seString, Payload payload)
{
Remove(seString.Payloads, payload);
}
/// <summary> /// <summary>
/// Removes a Payload from a given list. /// Removes a Payload from a given list.
/// Using <code>List.Remove()</code> does not use the reference to compare for some reason. Tis is a workaround. /// Using <code>List.Remove()</code> does not use the reference to compare for some reason. Tis is a workaround.
/// </summary> /// </summary>
/// <param name="seString"></param> /// <param name="payloads"></param>
/// <param name="payload"></param> /// <param name="payload"></param>
public static void Remove(this List<Payload> payloads, Payload payload) public static void Remove(this List<Payload> payloads, Payload payload)
{
for (int i = 0; i < payloads.Count; i++)
{ {
for (int i = 0; i < payloads.Count; i++) if (ReferenceEquals(payloads[i], payload))
{ {
if (ReferenceEquals(payloads[i], payload)) payloads.RemoveAt(i);
{ break;
payloads.RemoveAt(i);
break;
}
} }
} }
} }

View File

@@ -1,153 +1,148 @@
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using FFXIVClientStructs.FFXIV.Client.System.Memory; using FFXIVClientStructs.FFXIV.Client.System.Memory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud namespace Pilz.Dalamud;
public static class GameInterfaceHelper
{ {
public static class GameInterfaceHelper public static SeString ReadSeString(IntPtr ptr)
{ {
public static SeString ReadSeString(IntPtr ptr) if (ptr == IntPtr.Zero)
{ {
if (ptr == IntPtr.Zero)
{
return new SeString();
}
if (TryReadStringBytes(ptr, out var bytes) && bytes != null)
{
return SeString.Parse(bytes);
}
return new SeString(); return new SeString();
} }
public static bool TryReadSeString(IntPtr ptr, out SeString? seString) if (TryReadStringBytes(ptr, out var bytes) && bytes != null)
{ {
seString = null; return SeString.Parse(bytes);
if (ptr == IntPtr.Zero) }
{
return false;
}
if (TryReadStringBytes(ptr, out var bytes) && bytes != null) return new SeString();
{ }
seString = SeString.Parse(bytes);
return true;
}
public static bool TryReadSeString(IntPtr ptr, out SeString? seString)
{
seString = null;
if (ptr == IntPtr.Zero)
{
return false; return false;
} }
public static string? ReadString(IntPtr ptr) if (TryReadStringBytes(ptr, out var bytes) && bytes != null)
{ {
if (ptr == IntPtr.Zero) seString = SeString.Parse(bytes);
{
return null;
}
if (TryReadStringBytes(ptr, out var bytes) && bytes != null)
{
return Encoding.UTF8.GetString(bytes);
}
return null;
}
public static bool TryReadString(IntPtr ptr, out string? str)
{
str = null;
if (ptr == IntPtr.Zero)
{
return false;
}
if (TryReadStringBytes(ptr, out var bytes) && bytes != null)
{
str = Encoding.UTF8.GetString(bytes);
return true;
}
return false;
}
public static bool TryReadStringBytes(IntPtr ptr, out byte[]? bytes)
{
bytes = null;
if (ptr == IntPtr.Zero)
{
return false;
}
var size = 0;
while (Marshal.ReadByte(ptr, size) != 0)
{
size++;
}
bytes = new byte[size];
Marshal.Copy(ptr, bytes, 0, size);
return true; return true;
} }
public static IntPtr PluginAllocate(byte[] bytes) return false;
{ }
IntPtr pointer = Marshal.AllocHGlobal(bytes.Length + 1);
Marshal.Copy(bytes, 0, pointer, bytes.Length);
Marshal.WriteByte(pointer, bytes.Length, 0);
return pointer; public static string? ReadString(IntPtr ptr)
{
if (ptr == IntPtr.Zero)
{
return null;
} }
public static IntPtr PluginAllocate(SeString seString) if (TryReadStringBytes(ptr, out var bytes) && bytes != null)
{ {
return PluginAllocate(seString.Encode()); return Encoding.UTF8.GetString(bytes);
} }
public static void PluginFree(IntPtr ptr) return null;
}
public static bool TryReadString(IntPtr ptr, out string? str)
{
str = null;
if (ptr == IntPtr.Zero)
{ {
Marshal.FreeHGlobal(ptr); return false;
} }
public static void PluginFree(ref IntPtr ptr) if (TryReadStringBytes(ptr, out var bytes) && bytes != null)
{ {
PluginFree(ptr); str = Encoding.UTF8.GetString(bytes);
ptr = IntPtr.Zero; return true;
} }
public static byte[] NullTerminate(this byte[] bytes) return false;
}
public static bool TryReadStringBytes(IntPtr ptr, out byte[]? bytes)
{
bytes = null;
if (ptr == IntPtr.Zero)
{ {
if (bytes.Length == 0 || bytes[bytes.Length - 1] != 0) return false;
{
var newBytes = new byte[bytes.Length + 1];
Array.Copy(bytes, newBytes, bytes.Length);
newBytes[^1] = 0;
return newBytes;
}
return bytes;
} }
public static unsafe IntPtr GameUIAllocate(ulong size) var size = 0;
while (Marshal.ReadByte(ptr, size) != 0)
{ {
return (IntPtr)IMemorySpace.GetUISpace()->Malloc(size, 0); size++;
} }
public static unsafe void GameFree(ref IntPtr ptr, ulong size) bytes = new byte[size];
{ Marshal.Copy(ptr, bytes, 0, size);
if (ptr == IntPtr.Zero)
{
return;
}
IMemorySpace.Free((void*)ptr, size); return true;
ptr = IntPtr.Zero; }
public static IntPtr PluginAllocate(byte[] bytes)
{
IntPtr pointer = Marshal.AllocHGlobal(bytes.Length + 1);
Marshal.Copy(bytes, 0, pointer, bytes.Length);
Marshal.WriteByte(pointer, bytes.Length, 0);
return pointer;
}
public static IntPtr PluginAllocate(SeString seString)
{
return PluginAllocate(seString.Encode());
}
public static void PluginFree(IntPtr ptr)
{
Marshal.FreeHGlobal(ptr);
}
public static void PluginFree(ref IntPtr ptr)
{
PluginFree(ptr);
ptr = IntPtr.Zero;
}
public static byte[] NullTerminate(this byte[] bytes)
{
if (bytes.Length == 0 || bytes[bytes.Length - 1] != 0)
{
var newBytes = new byte[bytes.Length + 1];
Array.Copy(bytes, newBytes, bytes.Length);
newBytes[^1] = 0;
return newBytes;
} }
return bytes;
}
public static unsafe IntPtr GameUIAllocate(ulong size)
{
return (IntPtr)IMemorySpace.GetUISpace()->Malloc(size, 0);
}
public static unsafe void GameFree(ref IntPtr ptr, ulong size)
{
if (ptr == IntPtr.Zero)
{
return;
}
IMemorySpace.Free((void*)ptr, size);
ptr = IntPtr.Zero;
} }
} }

View File

@@ -1,27 +1,19 @@
using System; namespace Pilz.Dalamud.Icons;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.Icons public class JobIconSet
{ {
public class JobIconSet private readonly int[] icons;
public float IconScale { get; init; }
public JobIconSet(int[] icons, float iconScale)
{ {
private readonly int[] icons; this.icons = icons;
IconScale = iconScale;
}
public float IconScale { get; init; } public int GetIcon(uint jobID)
{
public JobIconSet(int[] icons, float iconScale) return icons[jobID - 1];
{
this.icons = icons;
IconScale = iconScale;
}
public int GetIcon(uint jobID)
{
return icons[jobID - 1];
}
} }
} }

View File

@@ -1,24 +1,17 @@
using System; namespace Pilz.Dalamud.Icons;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.Icons public enum JobIconSetName
{ {
public enum JobIconSetName Gold,
{ Framed,
Gold, Glowing,
Framed, Blue,
Glowing, Red,
Blue, Purple,
Red, Black,
Purple, Yellow,
Black, Orange,
Yellow, Green,
Orange, Grey,
Green, Role
Grey,
Role
}
} }

View File

@@ -1,128 +1,132 @@
using Lumina.Excel.GeneratedSheets; namespace Pilz.Dalamud.Icons;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.Icons public class JobIconSets
{ {
public class JobIconSets private readonly Dictionary<JobIconSetName, JobIconSet> iconSets = [];
public JobIconSets()
{ {
private readonly Dictionary<JobIconSetName, JobIconSet> iconSets = new(); Add(JobIconSetName.Gold,
[
62001, 62002, 62003, 62004, 62005, 62006, 62007, 62008, 62009, 62010,
62011, 62012, 62013, 62014, 62015, 62016, 62017, 62018, 62019, 62020,
62021, 62022, 62023, 62024, 62025, 62026, 62027, 62028, 62029, 62030,
62031, 62032, 62033, 62034, 62035, 62036, 62037, 62038, 62039, 62040,
62041, 62042,
], 1);
public JobIconSets() Add(JobIconSetName.Framed,
[
62101, 62102, 62103, 62104, 62105, 62106, 62107, 62108, 62109, 62110,
62111, 62112, 62113, 62114, 62115, 62116, 62117, 62118, 62119, 62120,
62121, 62122, 62123, 62124, 62125, 62126, 62127, 62128, 62129, 62130,
62131, 62132, 62133, 62134, 62135, 62136, 62137, 62138, 62139, 62140,
62141, 62142,
]);
Add(JobIconSetName.Glowing,
[
62301, 62302, 62303, 62304, 62305, 62306, 62307, 62310, 62311, 62312,
62313, 62314, 62315, 62316, 62317, 62318, 62319, 62320, 62401, 62402,
62403, 62404, 62405, 62406, 62407, 62308, 62408, 62409, 62309, 62410,
62411, 62412, 62413, 62414, 62415, 62416, 62417, 62418, 62419, 62420,
62421, 62422,
]);
Add(JobIconSetName.Grey,
[
91022, 91023, 91024, 91025, 91026, 91028, 91029, 91031, 91032, 91033,
91034, 91035, 91036, 91037, 91038, 91039, 91040, 91041, 91079, 91080,
91081, 91082, 91083, 91084, 91085, 91030, 91086, 91087, 91121, 91122,
91125, 91123, 91124, 91127, 91128, 91129, 91130, 91131, 91132, 91133,
91185, 91186,
], 2);
Add(JobIconSetName.Black,
[
91522, 91523, 91524, 91525, 91526, 91528, 91529, 91531, 91532, 91533,
91534, 91535, 91536, 91537, 91538, 91539, 91540, 91541, 91579, 91580,
91581, 91582, 91583, 91584, 91585, 91530, 91586, 91587, 91621, 91622,
91625, 91623, 91624, 91627, 91628, 91629, 91630, 91631, 91632, 91633,
91685, 91686,
], 2);
Add(JobIconSetName.Yellow,
[
92022, 92023, 92024, 92025, 92026, 92028, 92029, 92031, 92032, 92033,
92034, 92035, 92036, 92037, 92038, 92039, 92040, 92041, 92079, 92080,
92081, 92082, 92083, 92084, 92085, 92030, 92086, 92087, 92121, 92122,
92125, 92123, 92124, 92127, 92128, 92129, 92130, 92131, 92132, 92133,
92185, 92186,
], 2);
Add(JobIconSetName.Orange,
[
92522, 92523, 92524, 92525, 92526, 92528, 92529, 92531, 92532, 92533,
92534, 92535, 92536, 92537, 92538, 92539, 92540, 92541, 92579, 92580,
92581, 92582, 92583, 92584, 92585, 92530, 92586, 92587, 92621, 92622,
92625, 92623, 92624, 92627, 92628, 92629, 92630, 92631, 92632, 92633,
92685, 92686,
], 2);
Add(JobIconSetName.Red,
[
93022, 93023, 93024, 93025, 93026, 93028, 93029, 93031, 93032, 93033,
93034, 93035, 93036, 93037, 93038, 93039, 93040, 93041, 93079, 93080,
93081, 93082, 93083, 93084, 93085, 93030, 93086, 93087, 93121, 93122,
93125, 93123, 93124, 93127, 93128, 93129, 93130, 93131, 93132, 93133,
93185, 93186,
], 2);
Add(JobIconSetName.Purple, icons: new[]
{ {
Add(JobIconSetName.Gold, new[] 93522, 93523, 93524, 93525, 93526, 93528, 93529, 93531, 93532, 93533,
{ 93534, 93535, 93536, 93537, 93538, 93539, 93540, 93541, 93579, 93580,
62001, 62002, 62003, 62004, 62005, 62006, 62007, 62008, 62009, 62010, 93581, 93582, 93583, 93584, 93585, 93530, 93586, 93587, 93621, 93622,
62011, 62012, 62013, 62014, 62015, 62016, 62017, 62018, 62019, 62020, 93625, 93623, 93624, 93627, 93628, 93629, 93630, 93631, 93632, 93633,
62021, 62022, 62023, 62024, 62025, 62026, 62027, 62028, 62029, 62030, 93685, 93686,
62031, 62032, 62033, 62034, 62035, 62036, 62037, 62038, 62039, 62040 }, scale: 2);
}, 1);
Add(JobIconSetName.Framed, new[] Add(JobIconSetName.Blue,
{ [
62101, 62102, 62103, 62104, 62105, 62106, 62107, 62108, 62109, 62110, 94022, 94023, 94024, 94025, 94026, 94028, 94029, 94031, 94032, 94033,
62111, 62112, 62113, 62114, 62115, 62116, 62117, 62118, 62119, 62120, 94034, 94035, 94036, 94037, 94038, 94039, 94040, 94041, 94079, 94080,
62121, 62122, 62123, 62124, 62125, 62126, 62127, 62128, 62129, 62130, 94081, 94082, 94083, 94084, 94085, 94030, 94086, 94087, 94121, 94122,
62131, 62132, 62133, 62134, 62135, 62136, 62137, 62138, 62139, 62140 94125, 94123, 94124, 94127, 94128, 94129, 94130, 94131, 94132, 94133,
}); 94185, 94186,
], 2);
Add(JobIconSetName.Glowing, new[] Add(JobIconSetName.Green,
{ [
62301, 62302, 62303, 62304, 62305, 62306, 62307, 62310, 62311, 62312, 94522, 94523, 94524, 94525, 94526, 94528, 94529, 94531, 94532, 94533,
62313, 62314, 62315, 62316, 62317, 62318, 62319, 62320, 62401, 62402, 94534, 94535, 94536, 94537, 94538, 94539, 94540, 94541, 94579, 94580,
62403, 62404, 62405, 62406, 62407, 62308, 62408, 62409, 62309, 62410, 94581, 94582, 94583, 94584, 94585, 94530, 94586, 94587, 94621, 94622,
62411, 62412, 62413, 62414, 62415, 62416, 62417, 62418, 62419, 62420 94625, 94623, 94624, 94627, 94628, 94629, 94630, 94631, 94632, 94633,
}); 94685, 94686,
], 2);
Add(JobIconSetName.Grey, new[] Add(JobIconSetName.Role,
{ [
91022, 91023, 91024, 91025, 91026, 91028, 91029, 91031, 91032, 91033, 62581, 62584, 62581, 62584, 62586, 62582, 62502, 62502, 62503, 62504,
91034, 91035, 91036, 91037, 91038, 91039, 91040, 91041, 91079, 91080, 62505, 62506, 62507, 62508, 62509, 62510, 62511, 62512, 62581, 62584,
91081, 91082, 91083, 91084, 91085, 91030, 91086, 91087, 91121, 91122, 62581, 62584, 62586, 62582, 62587, 62587, 62587, 62582, 62584, 62584,
91125, 91123, 91124, 91127, 91128, 91129, 91130, 91131, 91132, 91133 62586, 62581, 62582, 62584, 62587, 62587, 62581, 62586, 62584, 62582,
}, 2); 62584, 62584,
]);
}
Add(JobIconSetName.Black, new[] private void Add(JobIconSetName id, int[] icons, float scale = 1f)
{ {
91522, 91523, 91524, 91525, 91526, 91528, 91529, 91531, 91532, 91533, iconSets[id] = new JobIconSet(icons, scale);
91534, 91535, 91536, 91537, 91538, 91539, 91540, 91541, 91579, 91580, }
91581, 91582, 91583, 91584, 91585, 91530, 91586, 91587, 91621, 91622,
91625, 91623, 91624, 91627, 91628, 91629, 91630, 91631, 91632, 91633
}, 2);
Add(JobIconSetName.Yellow, new[] public int GetJobIcon(JobIconSetName set, uint jobId)
{ {
92022, 92023, 92024, 92025, 92026, 92028, 92029, 92031, 92032, 92033, return iconSets[set].GetIcon(jobId);
92034, 92035, 92036, 92037, 92038, 92039, 92040, 92041, 92079, 92080, }
92081, 92082, 92083, 92084, 92085, 92030, 92086, 92087, 92121, 92122,
92125, 92123, 92124, 92127, 92128, 92129, 92130, 92131, 92132, 92133
}, 2);
Add(JobIconSetName.Orange, new[] public float GetJobIconSale(JobIconSetName set)
{ {
92522, 92523, 92524, 92525, 92526, 92528, 92529, 92531, 92532, 92533, return iconSets[set].IconScale;
92534, 92535, 92536, 92537, 92538, 92539, 92540, 92541, 92579, 92580,
92581, 92582, 92583, 92584, 92585, 92530, 92586, 92587, 92621, 92622,
92625, 92623, 92624, 92627, 92628, 92629, 92630, 92631, 92632, 92633
}, 2);
Add(JobIconSetName.Red, new[]
{
93022, 93023, 93024, 93025, 93026, 93028, 93029, 93031, 93032, 93033,
93034, 93035, 93036, 93037, 93038, 93039, 93040, 93041, 93079, 93080,
93081, 93082, 93083, 93084, 93085, 93030, 93086, 93087, 93121, 93122,
93125, 93123, 93124, 93127, 93128, 93129, 93130, 93131, 93132, 93133
}, 2);
Add(JobIconSetName.Purple, new[]
{
93522, 93523, 93524, 93525, 93526, 93528, 93529, 93531, 93532, 93533,
93534, 93535, 93536, 93537, 93538, 93539, 93540, 93541, 93579, 93580,
93581, 93582, 93583, 93584, 93585, 93530, 93586, 93587, 93621, 93622,
93625, 93623, 93624, 93627, 93628, 93629, 93630, 93631, 93632, 93633
}, 2);
Add(JobIconSetName.Blue, new[]
{
94022, 94023, 94024, 94025, 94026, 94028, 94029, 94031, 94032, 94033,
94034, 94035, 94036, 94037, 94038, 94039, 94040, 94041, 94079, 94080,
94081, 94082, 94083, 94084, 94085, 94030, 94086, 94087, 94121, 94122,
94125, 94123, 94124, 94127, 94128, 94129, 94130, 94131, 94132, 94133
}, 2);
Add(JobIconSetName.Green, new[]
{
94522, 94523, 94524, 94525, 94526, 94528, 94529, 94531, 94532, 94533,
94534, 94535, 94536, 94537, 94538, 94539, 94540, 94541, 94579, 94580,
94581, 94582, 94583, 94584, 94585, 94530, 94586, 94587, 94621, 94622,
94625, 94623, 94624, 94627, 94628, 94629, 94630, 94631, 94632, 94633
}, 2);
Add(JobIconSetName.Role, new[]
{
62581, 62584, 62581, 62584, 62586, 62582, 62502, 62502, 62503, 62504,
62505, 62506, 62507, 62508, 62509, 62510, 62511, 62512, 62581, 62584,
62581, 62584, 62586, 62582, 62587, 62587, 62587, 62582, 62584, 62584,
62586, 62581, 62582, 62584, 62587, 62587, 62581, 62586, 62584, 62582
});
}
private void Add(JobIconSetName id, int[] icons, float scale = 1f)
{
iconSets[id] = new JobIconSet(icons, scale);
}
public int GetJobIcon(JobIconSetName set, uint jobId)
{
return iconSets[set].GetIcon(jobId);
}
public float GetJobIconSale(JobIconSetName set)
{
return iconSets[set].IconScale;
}
} }
} }

View File

@@ -0,0 +1,25 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace Pilz.Dalamud.Icons;
[JsonConverter(typeof(StringEnumConverter))]
public enum StatusIcons
{
Disconnecting = 061503,
InDuty = 061506,
ViewingCutscene = 061508,
Busy = 061509,
Idle = 061511,
DutyFinder = 061517,
PartyLeader = 061521,
PartyMember = 061522,
RolePlaying = 061545,
GroupPose = 061546,
NewAdventurer = 061523,
Mentor = 061540,
MentorPvE = 061542,
MentorCrafting = 061543,
MentorPvP = 061544,
Returner = 061547,
}

View File

@@ -0,0 +1,43 @@
using System.Collections.Generic;
namespace Pilz.Dalamud.NamePlate;
/// <summary>
/// Class used to modify the data used when rendering nameplates.
/// </summary>
public interface INamePlateGui
{
/// <summary>
/// The delegate used for receiving nameplate update events.
/// </summary>
/// <param name="context">An object containing information about the pending data update.</param>
/// <param name="handlers>">A list of handlers used for updating nameplate data.</param>
public delegate void OnPlateUpdateDelegate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers);
/// <summary>
/// An event which fires when nameplate data is updated and at least one nameplate has important updates. The
/// subscriber is provided with a list of handlers for nameplates with important updates.
/// </summary>
/// <remarks>
/// Fires after <see cref="OnDataUpdate"/>.
/// </remarks>
event OnPlateUpdateDelegate? OnNamePlateUpdate;
/// <summary>
/// An event which fires when nameplate data is updated. The subscriber is provided with a list of handlers for all
/// nameplates.
/// </summary>
/// <remarks>
/// This event is likely to fire every frame even when no nameplates are actually updated, so in most cases
/// <see cref="OnNamePlateUpdate"/> is preferred. Fires before <see cref="OnNamePlateUpdate"/>.
/// </remarks>
event OnPlateUpdateDelegate? OnDataUpdate;
/// <summary>
/// Requests that all nameplates should be redrawn on the following frame.
/// </summary>
void RequestRedraw();
private static NamePlateGui instance;
public static INamePlateGui Instance => instance ??= new ();
}

View File

@@ -0,0 +1,202 @@
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using System.Runtime.InteropServices;
namespace Pilz.Dalamud.NamePlate;
/// <summary>
/// Class used to modify the data used when rendering nameplates.
/// </summary>
public sealed class NamePlateGui : IDisposable, INamePlateGui
{
/// <summary>
/// The index for the number array used by the NamePlate addon.
/// </summary>
public const int NumberArrayIndex = 5;
/// <summary>
/// The index for the string array used by the NamePlate addon.
/// </summary>
public const int StringArrayIndex = 4;
/// <summary>
/// The index for of the FullUpdate entry in the NamePlate number array.
/// </summary>
internal const int NumberArrayFullUpdateIndex = 4;
/// <summary>
/// An empty null-terminated string pointer allocated in unmanaged memory, used to tag removed fields.
/// </summary>
internal static readonly nint EmptyStringPointer = CreateEmptyStringPointer();
private readonly IAddonLifecycle addonLifecycle = PluginServices.AddonLifecycle;
private readonly IGameGui gameGui = PluginServices.GameGui;
private readonly IObjectTable objectTable = PluginServices.ObjectTable;
private NamePlateUpdateContext? context;
private NamePlateUpdateHandler[] updateHandlers = [];
internal NamePlateGui()
{
this.addonLifecycle.RegisterListener(AddonEvent.PreRequestedUpdate, "NamePlate", this.OnPreRequestedUpdate);
}
/// <inheritdoc/>
public event INamePlateGui.OnPlateUpdateDelegate? OnNamePlateUpdate;
/// <inheritdoc/>
public event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdate;
/// <inheritdoc/>
public unsafe void RequestRedraw()
{
var addon = this.gameGui.GetAddonByName("NamePlate");
if (addon != 0)
{
var raptureAtkModule = RaptureAtkModule.Instance();
if (raptureAtkModule == null)
{
return;
}
((AddonNamePlate*)addon)->DoFullUpdate = 1;
var namePlateNumberArrayData = raptureAtkModule->AtkArrayDataHolder.NumberArrays[NumberArrayIndex];
namePlateNumberArrayData->SetValue(NumberArrayFullUpdateIndex, 1);
}
}
/// <inheritdoc/>
public void Dispose()
{
this.addonLifecycle.UnregisterListener(AddonEvent.PreRequestedUpdate, "NamePlate", this.OnPreRequestedUpdate);
}
/// <summary>
/// Strips the surrounding quotes from a free company tag. If the quotes are not present in the expected location,
/// no modifications will be made.
/// </summary>
/// <param name="text">A quoted free company tag.</param>
/// <returns>A span containing the free company tag without its surrounding quote characters.</returns>
internal static ReadOnlySpan<byte> StripFreeCompanyTagQuotes(ReadOnlySpan<byte> text)
{
if (text.Length > 4 && text.StartsWith(" «"u8) && text.EndsWith("»"u8))
{
return text[3..^2];
}
return text;
}
/// <summary>
/// Strips the surrounding quotes from a title. If the quotes are not present in the expected location, no
/// modifications will be made.
/// </summary>
/// <param name="text">A quoted title.</param>
/// <returns>A span containing the title without its surrounding quote characters.</returns>
internal static ReadOnlySpan<byte> StripTitleQuotes(ReadOnlySpan<byte> text)
{
if (text.Length > 5 && text.StartsWith("《"u8) && text.EndsWith("》"u8))
{
return text[3..^3];
}
return text;
}
private static nint CreateEmptyStringPointer()
{
var pointer = Marshal.AllocHGlobal(1);
Marshal.WriteByte(pointer, 0, 0);
return pointer;
}
private void CreateHandlers(NamePlateUpdateContext createdContext)
{
var handlers = new List<NamePlateUpdateHandler>();
for (var i = 0; i < AddonNamePlate.NumNamePlateObjects; i++)
{
handlers.Add(new NamePlateUpdateHandler(createdContext, i));
}
this.updateHandlers = handlers.ToArray();
}
private void OnPreRequestedUpdate(AddonEvent type, AddonArgs args)
{
if (this.OnDataUpdate == null && this.OnNamePlateUpdate == null)
{
return;
}
var reqArgs = (AddonRequestedUpdateArgs)args;
if (this.context == null)
{
this.context = new NamePlateUpdateContext(this.objectTable, reqArgs);
this.CreateHandlers(this.context);
}
else
{
this.context.ResetState(reqArgs);
}
var activeNamePlateCount = this.context.ActiveNamePlateCount;
if (activeNamePlateCount == 0)
return;
var activeHandlers = this.updateHandlers[..activeNamePlateCount];
if (this.context.IsFullUpdate)
{
foreach (var handler in activeHandlers)
{
handler.ResetState();
}
this.OnDataUpdate?.Invoke(this.context, activeHandlers);
this.OnNamePlateUpdate?.Invoke(this.context, activeHandlers);
if (this.context.HasParts)
this.ApplyBuilders(activeHandlers);
}
else
{
var udpatedHandlers = new List<NamePlateUpdateHandler>(activeNamePlateCount);
foreach (var handler in activeHandlers)
{
handler.ResetState();
if (handler.IsUpdating)
udpatedHandlers.Add(handler);
}
if (this.OnDataUpdate is not null)
{
this.OnDataUpdate?.Invoke(this.context, activeHandlers);
this.OnNamePlateUpdate?.Invoke(this.context, udpatedHandlers);
if (this.context.HasParts)
this.ApplyBuilders(activeHandlers);
}
else if (udpatedHandlers.Count != 0)
{
var changedHandlersSpan = udpatedHandlers.ToArray().AsSpan();
this.OnNamePlateUpdate?.Invoke(this.context, udpatedHandlers);
if (this.context.HasParts)
this.ApplyBuilders(changedHandlersSpan);
}
}
}
private void ApplyBuilders(Span<NamePlateUpdateHandler> handlers)
{
foreach (var handler in handlers)
{
if (handler.PartsContainer is { } container)
{
container.ApplyBuilders(handler);
}
}
}
}

View File

@@ -0,0 +1,105 @@
using Dalamud.Game.Text.SeStringHandling;
using FFXIVClientStructs.FFXIV.Client.UI;
namespace Pilz.Dalamud.NamePlate;
/// <summary>
/// Provides a read-only view of the nameplate info object data for a nameplate. Modifications to
/// <see cref="NamePlateUpdateHandler"/> fields do not affect this data.
/// </summary>
public interface INamePlateInfoView
{
/// <summary>
/// Gets the displayed name for this nameplate according to the nameplate info object.
/// </summary>
SeString Name { get; }
/// <summary>
/// Gets the displayed free company tag for this nameplate according to the nameplate info object. For this field,
/// the quote characters which appear on either side of the title are NOT included.
/// </summary>
SeString FreeCompanyTag { get; }
/// <summary>
/// Gets the displayed free company tag for this nameplate according to the nameplate info object. For this field,
/// the quote characters which appear on either side of the title ARE included.
/// </summary>
SeString QuotedFreeCompanyTag { get; }
/// <summary>
/// Gets the displayed title for this nameplate according to the nameplate info object. For this field, the quote
/// characters which appear on either side of the title are NOT included.
/// </summary>
SeString Title { get; }
/// <summary>
/// Gets the displayed title for this nameplate according to the nameplate info object. For this field, the quote
/// characters which appear on either side of the title ARE included.
/// </summary>
SeString QuotedTitle { get; }
/// <summary>
/// Gets the displayed level text for this nameplate according to the nameplate info object.
/// </summary>
SeString LevelText { get; }
/// <summary>
/// Gets the flags for this nameplate according to the nameplate info object.
/// </summary>
int Flags { get; }
/// <summary>
/// Gets a value indicating whether this nameplate is considered 'dirty' or not according to the nameplate
/// info object.
/// </summary>
bool IsDirty { get; }
/// <summary>
/// Gets a value indicating whether the title for this nameplate is a prefix title or not according to the nameplate
/// info object. This value is derived from the <see cref="Flags"/> field.
/// </summary>
bool IsPrefixTitle { get; }
}
/// <summary>
/// Provides a read-only view of the nameplate info object data for a nameplate. Modifications to
/// <see cref="NamePlateUpdateHandler"/> fields do not affect this data.
/// </summary>
internal unsafe class NamePlateInfoView(RaptureAtkModule.NamePlateInfo* info) : INamePlateInfoView
{
private SeString? name;
private SeString? freeCompanyTag;
private SeString? quotedFreeCompanyTag;
private SeString? title;
private SeString? quotedTitle;
private SeString? levelText;
/// <inheritdoc/>
public SeString Name => this.name ??= SeString.Parse(info->Name);
/// <inheritdoc/>
public SeString FreeCompanyTag => this.freeCompanyTag ??=
SeString.Parse(NamePlateGui.StripFreeCompanyTagQuotes(info->FcName));
/// <inheritdoc/>
public SeString QuotedFreeCompanyTag => this.quotedFreeCompanyTag ??= SeString.Parse(info->FcName);
/// <inheritdoc/>
public SeString Title => this.title ??= SeString.Parse(info->Title);
/// <inheritdoc/>
public SeString QuotedTitle => this.quotedTitle ??= SeString.Parse(info->DisplayTitle);
/// <inheritdoc/>
public SeString LevelText => this.levelText ??= SeString.Parse(info->LevelText);
/// <inheritdoc/>
public int Flags => info->Flags;
/// <inheritdoc/>
public bool IsDirty => info->IsDirty;
/// <inheritdoc/>
public bool IsPrefixTitle => ((info->Flags >> (8 * 3)) & 0xFF) == 1;
}

View File

@@ -0,0 +1,57 @@
namespace Pilz.Dalamud.NamePlate;
/// <summary>
/// An enum describing what kind of game object this nameplate represents.
/// </summary>
public enum NamePlateKind : byte
{
/// <summary>
/// A player character.
/// </summary>
PlayerCharacter = 0,
/// <summary>
/// An event NPC or companion.
/// </summary>
EventNpcCompanion = 1,
/// <summary>
/// A retainer.
/// </summary>
Retainer = 2,
/// <summary>
/// An enemy battle NPC.
/// </summary>
BattleNpcEnemy = 3,
/// <summary>
/// A friendly battle NPC.
/// </summary>
BattleNpcFriendly = 4,
/// <summary>
/// An event object.
/// </summary>
EventObject = 5,
/// <summary>
/// Treasure.
/// </summary>
Treasure = 6,
/// <summary>
/// A gathering point.
/// </summary>
GatheringPoint = 7,
/// <summary>
/// A battle NPC with subkind 6.
/// </summary>
BattleNpcSubkind6 = 8,
/// <summary>
/// Something else.
/// </summary>
Other = 9,
}

View File

@@ -0,0 +1,46 @@
namespace Pilz.Dalamud.NamePlate;
/// <summary>
/// A container for parts.
/// </summary>
internal class NamePlatePartsContainer
{
private NamePlateSimpleParts? nameParts;
private NamePlateQuotedParts? titleParts;
private NamePlateQuotedParts? freeCompanyTagParts;
/// <summary>
/// Initializes a new instance of the <see cref="NamePlatePartsContainer"/> class.
/// </summary>
/// <param name="context">The currently executing update context.</param>
public NamePlatePartsContainer(NamePlateUpdateContext context)
{
context.HasParts = true;
}
/// <summary>
/// Gets a parts object for constructing a nameplate name.
/// </summary>
internal NamePlateSimpleParts Name => this.nameParts ??= new NamePlateSimpleParts(NamePlateStringField.Name);
/// <summary>
/// Gets a parts object for constructing a nameplate title.
/// </summary>
internal NamePlateQuotedParts Title => this.titleParts ??= new NamePlateQuotedParts(NamePlateStringField.Title, false);
/// <summary>
/// Gets a parts object for constructing a nameplate free company tag.
/// </summary>
internal NamePlateQuotedParts FreeCompanyTag => this.freeCompanyTagParts ??= new NamePlateQuotedParts(NamePlateStringField.FreeCompanyTag, true);
/// <summary>
/// Applies all container parts.
/// </summary>
/// <param name="handler">The handler to apply the builders to.</param>
internal void ApplyBuilders(NamePlateUpdateHandler handler)
{
this.nameParts?.Apply(handler);
this.freeCompanyTagParts?.Apply(handler);
this.titleParts?.Apply(handler);
}
}

View File

@@ -0,0 +1,104 @@
using Dalamud.Game.Text.SeStringHandling;
namespace Pilz.Dalamud.NamePlate;
/// <summary>
/// A part builder for constructing and setting quoted nameplate fields (i.e. free company tag and title).
/// </summary>
/// <param name="field">The field type which should be set.</param>
/// <remarks>
/// This class works as a lazy writer initialized with empty parts, where an empty part signifies no change should be
/// performed. Only after all handler processing is complete does it write out any parts which were set to the
/// associated field. Reading fields from this class is usually not what you want to do, as you'll only be reading the
/// contents of parts which other plugins have written to. Prefer reading from the base handler's properties or using
/// <see cref="NamePlateInfoView"/>.
/// </remarks>
public class NamePlateQuotedParts(NamePlateStringField field, bool isFreeCompany)
{
/// <summary>
/// Gets or sets the opening and closing SeStrings which will wrap the entire contents, which can be used to apply
/// colors or styling to the entire field.
/// </summary>
public (SeString, SeString)? OuterWrap { get; set; }
/// <summary>
/// Gets or sets the opening quote string which appears before the text and opening text-wrap.
/// </summary>
public SeString? LeftQuote { get; set; }
/// <summary>
/// Gets or sets the closing quote string which appears after the text and closing text-wrap.
/// </summary>
public SeString? RightQuote { get; set; }
/// <summary>
/// Gets or sets the opening and closing SeStrings which will wrap the text, which can be used to apply colors or
/// styling to the field's text.
/// </summary>
public (SeString, SeString)? TextWrap { get; set; }
/// <summary>
/// Gets or sets this field's text.
/// </summary>
public SeString? Text { get; set; }
/// <summary>
/// Applies the changes from this builder to the actual field.
/// </summary>
/// <param name="handler">The handler to perform the changes on.</param>
internal unsafe void Apply(NamePlateUpdateHandler handler)
{
if ((nint)handler.GetFieldAsPointer(field) == NamePlateGui.EmptyStringPointer)
return;
var sb = new SeStringBuilder();
if (this.OuterWrap is { Item1: var outerLeft })
{
sb.Append(outerLeft);
}
if (this.LeftQuote is not null)
{
sb.Append(this.LeftQuote);
}
else
{
sb.Append(isFreeCompany ? " «" : "《");
}
if (this.TextWrap is { Item1: var left, Item2: var right })
{
sb.Append(left);
sb.Append(this.Text ?? this.GetStrippedField(handler));
sb.Append(right);
}
else
{
sb.Append(this.Text ?? this.GetStrippedField(handler));
}
if (this.RightQuote is not null)
{
sb.Append(this.RightQuote);
}
else
{
sb.Append(isFreeCompany ? "»" : "》");
}
if (this.OuterWrap is { Item2: var outerRight })
{
sb.Append(outerRight);
}
handler.SetField(field, sb.Build());
}
private SeString GetStrippedField(NamePlateUpdateHandler handler)
{
return SeString.Parse(
isFreeCompany
? NamePlateGui.StripFreeCompanyTagQuotes(handler.GetFieldAsSpan(field))
: NamePlateGui.StripTitleQuotes(handler.GetFieldAsSpan(field)));
}
}

View File

@@ -0,0 +1,51 @@
using Dalamud.Game.Text.SeStringHandling;
namespace Pilz.Dalamud.NamePlate;
/// <summary>
/// A part builder for constructing and setting a simple (unquoted) nameplate field.
/// </summary>
/// <param name="field">The field type which should be set.</param>
/// <remarks>
/// This class works as a lazy writer initialized with empty parts, where an empty part signifies no change should be
/// performed. Only after all handler processing is complete does it write out any parts which were set to the
/// associated field. Reading fields from this class is usually not what you want to do, as you'll only be reading the
/// contents of parts which other plugins have written to. Prefer reading from the base handler's properties or using
/// <see cref="NamePlateInfoView"/>.
/// </remarks>
public class NamePlateSimpleParts(NamePlateStringField field)
{
/// <summary>
/// Gets or sets the opening and closing SeStrings which will wrap the text, which can be used to apply colors or
/// styling to the field's text.
/// </summary>
public (SeString, SeString)? TextWrap { get; set; }
/// <summary>
/// Gets or sets this field's text.
/// </summary>
public SeString? Text { get; set; }
/// <summary>
/// Applies the changes from this builder to the actual field.
/// </summary>
/// <param name="handler">The handler to perform the changes on.</param>
internal unsafe void Apply(NamePlateUpdateHandler handler)
{
if ((nint)handler.GetFieldAsPointer(field) == NamePlateGui.EmptyStringPointer)
return;
if (this.TextWrap is { Item1: var left, Item2: var right })
{
var sb = new SeStringBuilder();
sb.Append(left);
sb.Append(this.Text ?? handler.GetFieldAsSeString(field));
sb.Append(right);
handler.SetField(field, sb.Build());
}
else if (this.Text is not null)
{
handler.SetField(field, this.Text);
}
}
}

View File

@@ -0,0 +1,38 @@
namespace Pilz.Dalamud.NamePlate;
/// <summary>
/// An enum describing the string fields available in nameplate data. The <see cref="NamePlateKind"/> and various flags
/// determine which fields will actually be rendered.
/// </summary>
public enum NamePlateStringField
{
/// <summary>
/// The object's name.
/// </summary>
Name = 0,
/// <summary>
/// The object's title.
/// </summary>
Title = 50,
/// <summary>
/// The object's free company tag.
/// </summary>
FreeCompanyTag = 100,
/// <summary>
/// The object's status prefix.
/// </summary>
StatusPrefix = 150,
/// <summary>
/// The object's target suffix.
/// </summary>
TargetSuffix = 200,
/// <summary>
/// The object's level prefix.
/// </summary>
LevelPrefix = 250,
}

View File

@@ -0,0 +1,152 @@
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Pilz.Dalamud.NamePlate;
/// <summary>
/// Contains information related to the pending nameplate data update. This is only valid for a single frame and should
/// not be kept across frames.
/// </summary>
public interface INamePlateUpdateContext
{
/// <summary>
/// Gets the number of active nameplates. The actual number visible may be lower than this in cases where some
/// nameplates are hidden by default (based on in-game "Display Name Settings" and so on).
/// </summary>
int ActiveNamePlateCount { get; }
/// <summary>
/// Gets a value indicating whether the game is currently performing a full update of all active nameplates.
/// </summary>
bool IsFullUpdate { get; }
/// <summary>
/// Gets the address of the NamePlate addon.
/// </summary>
nint AddonAddress { get; }
/// <summary>
/// Gets the address of the NamePlate addon's number array data container.
/// </summary>
nint NumberArrayDataAddress { get; }
/// <summary>
/// Gets the address of the NamePlate addon's string array data container.
/// </summary>
nint StringArrayDataAddress { get; }
/// <summary>
/// Gets the address of the first entry in the NamePlate addon's int array.
/// </summary>
nint NumberArrayDataEntryAddress { get; }
}
/// <summary>
/// Contains information related to the pending nameplate data update. This is only valid for a single frame and should
/// not be kept across frames.
/// </summary>
internal unsafe class NamePlateUpdateContext : INamePlateUpdateContext
{
/// <summary>
/// Initializes a new instance of the <see cref="NamePlateUpdateContext"/> class.
/// </summary>
/// <param name="objectTable">An object table.</param>
/// <param name="args">The addon lifecycle arguments for the update request.</param>
internal NamePlateUpdateContext(IObjectTable objectTable, AddonRequestedUpdateArgs args)
{
this.ObjectTable = objectTable;
this.RaptureAtkModule = FFXIVClientStructs.FFXIV.Client.UI.RaptureAtkModule.Instance();
this.Ui3DModule = UIModule.Instance()->GetUI3DModule();
this.ResetState(args);
}
/// <summary>
/// Gets the number of active nameplates. The actual number visible may be lower than this in cases where some
/// nameplates are hidden by default (based on in-game "Display Name Settings" and so on).
/// </summary>
public int ActiveNamePlateCount { get; private set; }
/// <summary>
/// Gets a value indicating whether the game is currently performing a full update of all active nameplates.
/// </summary>
public bool IsFullUpdate { get; private set; }
/// <summary>
/// Gets the address of the NamePlate addon.
/// </summary>
public nint AddonAddress => (nint)this.Addon;
/// <summary>
/// Gets the address of the NamePlate addon's number array data container.
/// </summary>
public nint NumberArrayDataAddress => (nint)this.NumberData;
/// <summary>
/// Gets the address of the NamePlate addon's string array data container.
/// </summary>
public nint StringArrayDataAddress => (nint)this.StringData;
/// <summary>
/// Gets the address of the first entry in the NamePlate addon's int array.
/// </summary>
public nint NumberArrayDataEntryAddress => (nint)this.NumberStruct;
/// <summary>
/// Gets the RaptureAtkModule.
/// </summary>
internal RaptureAtkModule* RaptureAtkModule { get; }
/// <summary>
/// Gets the Ui3DModule.
/// </summary>
internal UI3DModule* Ui3DModule { get; }
/// <summary>
/// Gets the ObjectTable.
/// </summary>
internal IObjectTable ObjectTable { get; }
/// <summary>
/// Gets a pointer to the NamePlate addon.
/// </summary>
internal AddonNamePlate* Addon { get; private set; }
/// <summary>
/// Gets a pointer to the NamePlate addon's number array data container.
/// </summary>
internal NumberArrayData* NumberData { get; private set; }
/// <summary>
/// Gets a pointer to the NamePlate addon's string array data container.
/// </summary>
internal StringArrayData* StringData { get; private set; }
/// <summary>
/// Gets a pointer to the NamePlate addon's number array entries as a struct.
/// </summary>
internal AddonNamePlate.NamePlateIntArrayData* NumberStruct { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether any handler in the current context has instantiated a part builder.
/// </summary>
internal bool HasParts { get; set; }
/// <summary>
/// Resets the state of the context based on the provided addon lifecycle arguments.
/// </summary>
/// <param name="args">The addon lifecycle arguments for the update request.</param>
internal void ResetState(AddonRequestedUpdateArgs args)
{
this.Addon = (AddonNamePlate*)args.Addon;
this.NumberData = ((NumberArrayData**)args.NumberArrayData)![NamePlateGui.NumberArrayIndex];
this.NumberStruct = (AddonNamePlate.NamePlateIntArrayData*)this.NumberData->IntArray;
this.StringData = ((StringArrayData**)args.StringArrayData)![NamePlateGui.StringArrayIndex];
this.HasParts = false;
this.ActiveNamePlateCount = this.NumberStruct->ActiveNamePlateCount;
this.IsFullUpdate = this.Addon->DoFullUpdate != 0;
}
}

View File

@@ -0,0 +1,606 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.Text.SeStringHandling;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.Interop;
namespace Pilz.Dalamud.NamePlate;
/// <summary>
/// A class representing a single nameplate. Provides mechanisms to look up the game object associated with the
/// nameplate and allows for modification of various backing fields in number and string array data, which in turn
/// affect aspects of the nameplate's appearance when drawn. Instances of this class are only valid for a single frame
/// and should not be kept across frames.
/// </summary>
public interface INamePlateUpdateHandler
{
/// <summary>
/// Gets the GameObjectId of the game object associated with this nameplate.
/// </summary>
ulong GameObjectId { get; }
/// <summary>
/// Gets the <see cref="IGameObject"/> associated with this nameplate, if possible. Performs an object table scan
/// and caches the result if successful.
/// </summary>
IGameObject? GameObject { get; }
/// <summary>
/// Gets a read-only view of the nameplate info object data for a nameplate. Modifications to
/// <see cref="NamePlateUpdateHandler"/> fields do not affect fields in the returned view.
/// </summary>
INamePlateInfoView InfoView { get; }
/// <summary>
/// Gets the index for this nameplate data in the backing number and string array data. This is not the same as the
/// rendered or object index, which can be retrieved from <see cref="NamePlateIndex"/>.
/// </summary>
int ArrayIndex { get; }
/// <summary>
/// Gets the <see cref="IBattleChara"/> associated with this nameplate, if possible. Returns null if the nameplate
/// has an associated <see cref="IGameObject"/>, but that object cannot be assigned to <see cref="IBattleChara"/>.
/// </summary>
IBattleChara? BattleChara { get; }
/// <summary>
/// Gets the <see cref="IPlayerCharacter"/> associated with this nameplate, if possible. Returns null if the
/// nameplate has an associated <see cref="IGameObject"/>, but that object cannot be assigned to
/// <see cref="IPlayerCharacter"/>.
/// </summary>
IPlayerCharacter? PlayerCharacter { get; }
/// <summary>
/// Gets the address of the nameplate info struct.
/// </summary>
nint NamePlateInfoAddress { get; }
/// <summary>
/// Gets the address of the first entry associated with this nameplate in the NamePlate addon's int array.
/// </summary>
nint NamePlateObjectAddress { get; }
/// <summary>
/// Gets a value indicating what kind of nameplate this is, based on the kind of object it is associated with.
/// </summary>
NamePlateKind NamePlateKind { get; }
/// <summary>
/// Gets the update flags for this nameplate.
/// </summary>
int UpdateFlags { get; }
/// <summary>
/// Gets or sets the overall text color for this nameplate. If this value is changed, the appropriate update flag
/// will be set so that the game will reflect this change immediately.
/// </summary>
uint TextColor { get; set; }
/// <summary>
/// Gets or sets the overall text edge color for this nameplate. If this value is changed, the appropriate update
/// flag will be set so that the game will reflect this change immediately.
/// </summary>
uint EdgeColor { get; set; }
/// <summary>
/// Gets or sets the icon ID for the nameplate's marker icon, which is the large icon used to indicate quest
/// availability and so on. This value is read from and reset by the game every frame, not just when a nameplate
/// changes. Setting this to 0 disables the icon.
/// </summary>
int MarkerIconId { get; set; }
/// <summary>
/// Gets or sets the icon ID for the nameplate's name icon, which is the small icon shown to the left of the name.
/// Setting this to -1 disables the icon.
/// </summary>
int NameIconId { get; set; }
/// <summary>
/// Gets the nameplate index, which is the index used for rendering and looking up entries in the object array. For
/// number and string array data, <see cref="ArrayIndex"/> is used.
/// </summary>
int NamePlateIndex { get; }
/// <summary>
/// Gets the draw flags for this nameplate.
/// </summary>
int DrawFlags { get; }
/// <summary>
/// Gets or sets the visibility flags for this nameplate.
/// </summary>
int VisibilityFlags { get; set; }
/// <summary>
/// Gets a value indicating whether this nameplate is undergoing a major update or not. This is usually true when a
/// nameplate has just appeared or something meaningful about the entity has changed (e.g. its job or status). This
/// flag is reset by the game during the update process (during requested update and before draw).
/// </summary>
bool IsUpdating { get; }
/// <summary>
/// Gets or sets a value indicating whether the title (when visible) will be displayed above the object's name (a
/// prefix title) instead of below the object's name (a suffix title).
/// </summary>
bool IsPrefixTitle { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the title should be displayed at all.
/// </summary>
bool DisplayTitle { get; set; }
/// <summary>
/// Gets or sets the name for this nameplate.
/// </summary>
SeString Name { get; set; }
/// <summary>
/// Gets a builder which can be used to help cooperatively build a new name for this nameplate even when other
/// plugins modifying the name are present. Specifically, this builder allows setting text and text-wrapping
/// payloads (e.g. for setting text color) separately.
/// </summary>
NamePlateSimpleParts NameParts { get; }
/// <summary>
/// Gets or sets the title for this nameplate.
/// </summary>
SeString Title { get; set; }
/// <summary>
/// Gets a builder which can be used to help cooperatively build a new title for this nameplate even when other
/// plugins modifying the title are present. Specifically, this builder allows setting text, text-wrapping
/// payloads (e.g. for setting text color), and opening and closing quote sequences separately.
/// </summary>
NamePlateQuotedParts TitleParts { get; }
/// <summary>
/// Gets or sets the free company tag for this nameplate.
/// </summary>
SeString FreeCompanyTag { get; set; }
/// <summary>
/// Gets a builder which can be used to help cooperatively build a new FC tag for this nameplate even when other
/// plugins modifying the FC tag are present. Specifically, this builder allows setting text, text-wrapping
/// payloads (e.g. for setting text color), and opening and closing quote sequences separately.
/// </summary>
NamePlateQuotedParts FreeCompanyTagParts { get; }
/// <summary>
/// Gets or sets the status prefix for this nameplate. This prefix is used by the game to add BitmapFontIcon-based
/// online status icons to player nameplates.
/// </summary>
SeString StatusPrefix { get; set; }
/// <summary>
/// Gets or sets the target suffix for this nameplate. This suffix is used by the game to add the squared-letter
/// target tags to the end of combat target nameplates.
/// </summary>
SeString TargetSuffix { get; set; }
/// <summary>
/// Gets or sets the level prefix for this nameplate. This "Lv60" style prefix is added to enemy and friendly battle
/// NPC nameplates to indicate the NPC level.
/// </summary>
SeString LevelPrefix { get; set; }
/// <summary>
/// Removes the contents of the name field for this nameplate. This differs from simply setting the field
/// to an empty string because it writes a special value to memory, and other setters (except SetField variants)
/// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed.
/// </summary>
void RemoveName();
/// <summary>
/// Removes the contents of the title field for this nameplate. This differs from simply setting the field
/// to an empty string because it writes a special value to memory, and other setters (except SetField variants)
/// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed.
/// </summary>
void RemoveTitle();
/// <summary>
/// Removes the contents of the FC tag field for this nameplate. This differs from simply setting the field
/// to an empty string because it writes a special value to memory, and other setters (except SetField variants)
/// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed.
/// </summary>
void RemoveFreeCompanyTag();
/// <summary>
/// Removes the contents of the status prefix field for this nameplate. This differs from simply setting the field
/// to an empty string because it writes a special value to memory, and other setters (except SetField variants)
/// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed.
/// </summary>
void RemoveStatusPrefix();
/// <summary>
/// Removes the contents of the target suffix field for this nameplate. This differs from simply setting the field
/// to an empty string because it writes a special value to memory, and other setters (except SetField variants)
/// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed.
/// </summary>
void RemoveTargetSuffix();
/// <summary>
/// Removes the contents of the level prefix field for this nameplate. This differs from simply setting the field
/// to an empty string because it writes a special value to memory, and other setters (except SetField variants)
/// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed.
/// </summary>
void RemoveLevelPrefix();
/// <summary>
/// Gets a pointer to the string array value in the provided field.
/// </summary>
/// <param name="field">The field to read from.</param>
/// <returns>A pointer to a sequence of non-null bytes.</returns>
unsafe byte* GetFieldAsPointer(NamePlateStringField field);
/// <summary>
/// Gets a byte span containing the string array value in the provided field.
/// </summary>
/// <param name="field">The field to read from.</param>
/// <returns>A ReadOnlySpan containing a sequence of non-null bytes.</returns>
ReadOnlySpan<byte> GetFieldAsSpan(NamePlateStringField field);
/// <summary>
/// Gets a UTF8 string copy of the string array value in the provided field.
/// </summary>
/// <param name="field">The field to read from.</param>
/// <returns>A copy of the string array value as a string.</returns>
string GetFieldAsString(NamePlateStringField field);
/// <summary>
/// Gets a parsed SeString copy of the string array value in the provided field.
/// </summary>
/// <param name="field">The field to read from.</param>
/// <returns>A copy of the string array value as a parsed SeString.</returns>
SeString GetFieldAsSeString(NamePlateStringField field);
/// <summary>
/// Sets the string array value for the provided field.
/// </summary>
/// <param name="field">The field to write to.</param>
/// <param name="value">The string to write.</param>
void SetField(NamePlateStringField field, string value);
/// <summary>
/// Sets the string array value for the provided field.
/// </summary>
/// <param name="field">The field to write to.</param>
/// <param name="value">The SeString to write.</param>
void SetField(NamePlateStringField field, SeString value);
/// <summary>
/// Sets the string array value for the provided field.
/// </summary>
/// <param name="field">The field to write to.</param>
/// <param name="value">The ReadOnlySpan of bytes to write.</param>
void SetField(NamePlateStringField field, ReadOnlySpan<byte> value);
/// <summary>
/// Sets the string array value for the provided field.
/// </summary>
/// <param name="field">The field to write to.</param>
/// <param name="value">The pointer to a null-terminated sequence of bytes to write.</param>
unsafe void SetField(NamePlateStringField field, byte* value);
/// <summary>
/// Sets the string array value for the provided field to a fixed pointer to an empty string in unmanaged memory.
/// Other methods may notice this fixed pointer and refuse to overwrite it, preserving the emptiness of the field.
/// </summary>
/// <param name="field">The field to write to.</param>
void RemoveField(NamePlateStringField field);
}
/// <summary>
/// A class representing a single nameplate. Provides mechanisms to look up the game object associated with the
/// nameplate and allows for modification of various backing fields in number and string array data, which in turn
/// affect aspects of the nameplate's appearance when drawn. Instances of this class are only valid for a single frame
/// and should not be kept across frames.
/// </summary>
internal unsafe class NamePlateUpdateHandler : INamePlateUpdateHandler
{
private readonly NamePlateUpdateContext context;
private ulong? gameObjectId;
private IGameObject? gameObject;
private NamePlateInfoView? infoView;
private NamePlatePartsContainer? partsContainer;
/// <summary>
/// Initializes a new instance of the <see cref="NamePlateUpdateHandler"/> class.
/// </summary>
/// <param name="context">The current update context.</param>
/// <param name="arrayIndex">The index for this nameplate data in the backing number and string array data. This is
/// not the same as the rendered index, which can be retrieved from <see cref="NamePlateIndex"/>.</param>
internal NamePlateUpdateHandler(NamePlateUpdateContext context, int arrayIndex)
{
this.context = context;
this.ArrayIndex = arrayIndex;
}
/// <inheritdoc/>
public int ArrayIndex { get; }
/// <inheritdoc/>
public ulong GameObjectId => this.gameObjectId ??= this.NamePlateInfo->ObjectId;
/// <inheritdoc/>
public IGameObject? GameObject => this.gameObject ??= this.context.ObjectTable.CreateObjectReference(
(nint)this.context.Ui3DModule->NamePlateObjectInfoPointers[
this.ArrayIndex].Value->GameObject);
/// <inheritdoc/>
public IBattleChara? BattleChara => this.GameObject as IBattleChara;
/// <inheritdoc/>
public IPlayerCharacter? PlayerCharacter => this.GameObject as IPlayerCharacter;
/// <inheritdoc/>
public INamePlateInfoView InfoView => this.infoView ??= new NamePlateInfoView(this.NamePlateInfo);
/// <inheritdoc/>
public nint NamePlateInfoAddress => (nint)this.NamePlateInfo;
/// <inheritdoc/>
public nint NamePlateObjectAddress => (nint)this.NamePlateObject;
/// <inheritdoc/>
public NamePlateKind NamePlateKind => (NamePlateKind)this.ObjectData->NamePlateKind;
/// <inheritdoc/>
public int UpdateFlags
{
get => this.ObjectData->UpdateFlags;
private set => this.ObjectData->UpdateFlags = value;
}
/// <inheritdoc/>
public uint TextColor
{
get => this.ObjectData->NameTextColor;
set
{
if (value != this.TextColor) this.UpdateFlags |= 2;
this.ObjectData->NameTextColor = value;
}
}
/// <inheritdoc/>
public uint EdgeColor
{
get => this.ObjectData->NameEdgeColor;
set
{
if (value != this.EdgeColor) this.UpdateFlags |= 2;
this.ObjectData->NameEdgeColor = value;
}
}
/// <inheritdoc/>
public int MarkerIconId
{
get => this.ObjectData->MarkerIconId;
set => this.ObjectData->MarkerIconId = value;
}
/// <inheritdoc/>
public int NameIconId
{
get => this.ObjectData->NameIconId;
set => this.ObjectData->NameIconId = value;
}
/// <inheritdoc/>
public int NamePlateIndex => this.ObjectData->NamePlateObjectIndex;
/// <inheritdoc/>
public int DrawFlags
{
get => this.ObjectData->DrawFlags;
private set => this.ObjectData->DrawFlags = value;
}
/// <inheritdoc/>
public int VisibilityFlags
{
get => ObjectData->VisibilityFlags;
set => ObjectData->VisibilityFlags = value;
}
/// <inheritdoc/>
public bool IsUpdating => (this.UpdateFlags & 1) != 0;
/// <inheritdoc/>
public bool IsPrefixTitle
{
get => (this.DrawFlags & 1) != 0;
set => this.DrawFlags = value ? this.DrawFlags | 1 : this.DrawFlags & ~1;
}
/// <inheritdoc/>
public bool DisplayTitle
{
get => (this.DrawFlags & 0x80) == 0;
set => this.DrawFlags = value ? this.DrawFlags & ~0x80 : this.DrawFlags | 0x80;
}
/// <inheritdoc/>
public SeString Name
{
get => this.GetFieldAsSeString(NamePlateStringField.Name);
set => this.WeakSetField(NamePlateStringField.Name, value);
}
/// <inheritdoc/>
public NamePlateSimpleParts NameParts => this.PartsContainer.Name;
/// <inheritdoc/>
public SeString Title
{
get => this.GetFieldAsSeString(NamePlateStringField.Title);
set => this.WeakSetField(NamePlateStringField.Title, value);
}
/// <inheritdoc/>
public NamePlateQuotedParts TitleParts => this.PartsContainer.Title;
/// <inheritdoc/>
public SeString FreeCompanyTag
{
get => this.GetFieldAsSeString(NamePlateStringField.FreeCompanyTag);
set => this.WeakSetField(NamePlateStringField.FreeCompanyTag, value);
}
/// <inheritdoc/>
public NamePlateQuotedParts FreeCompanyTagParts => this.PartsContainer.FreeCompanyTag;
/// <inheritdoc/>
public SeString StatusPrefix
{
get => this.GetFieldAsSeString(NamePlateStringField.StatusPrefix);
set => this.WeakSetField(NamePlateStringField.StatusPrefix, value);
}
/// <inheritdoc/>
public SeString TargetSuffix
{
get => this.GetFieldAsSeString(NamePlateStringField.TargetSuffix);
set => this.WeakSetField(NamePlateStringField.TargetSuffix, value);
}
/// <inheritdoc/>
public SeString LevelPrefix
{
get => this.GetFieldAsSeString(NamePlateStringField.LevelPrefix);
set => this.WeakSetField(NamePlateStringField.LevelPrefix, value);
}
/// <summary>
/// Gets or (lazily) creates a part builder container for this nameplate.
/// </summary>
internal NamePlatePartsContainer PartsContainer =>
this.partsContainer ??= new NamePlatePartsContainer(this.context);
private RaptureAtkModule.NamePlateInfo* NamePlateInfo =>
this.context.RaptureAtkModule->NamePlateInfoEntries.GetPointer(this.NamePlateIndex);
private AddonNamePlate.NamePlateObject* NamePlateObject =>
&this.context.Addon->NamePlateObjectArray[this.NamePlateIndex];
private AddonNamePlate.NamePlateIntArrayData.NamePlateObjectIntArrayData* ObjectData =>
this.context.NumberStruct->ObjectData.GetPointer(this.ArrayIndex);
/// <inheritdoc/>
public void RemoveName() => this.RemoveField(NamePlateStringField.Name);
/// <inheritdoc/>
public void RemoveTitle() => this.RemoveField(NamePlateStringField.Title);
/// <inheritdoc/>
public void RemoveFreeCompanyTag() => this.RemoveField(NamePlateStringField.FreeCompanyTag);
/// <inheritdoc/>
public void RemoveStatusPrefix() => this.RemoveField(NamePlateStringField.StatusPrefix);
/// <inheritdoc/>
public void RemoveTargetSuffix() => this.RemoveField(NamePlateStringField.TargetSuffix);
/// <inheritdoc/>
public void RemoveLevelPrefix() => this.RemoveField(NamePlateStringField.LevelPrefix);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte* GetFieldAsPointer(NamePlateStringField field)
{
return this.context.StringData->StringArray[this.ArrayIndex + (int)field];
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ReadOnlySpan<byte> GetFieldAsSpan(NamePlateStringField field)
{
return MemoryMarshal.CreateReadOnlySpanFromNullTerminated(this.GetFieldAsPointer(field));
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string GetFieldAsString(NamePlateStringField field)
{
return Encoding.UTF8.GetString(this.GetFieldAsSpan(field));
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SeString GetFieldAsSeString(NamePlateStringField field)
{
return SeString.Parse(this.GetFieldAsSpan(field));
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetField(NamePlateStringField field, string value)
{
this.context.StringData->SetValue(this.ArrayIndex + (int)field, value, true, true, true);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetField(NamePlateStringField field, SeString value)
{
this.context.StringData->SetValue(this.ArrayIndex + (int)field, value.Encode(), true, true, true);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetField(NamePlateStringField field, ReadOnlySpan<byte> value)
{
this.context.StringData->SetValue(this.ArrayIndex + (int)field, value, true, true, true);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetField(NamePlateStringField field, byte* value)
{
this.context.StringData->SetValue(this.ArrayIndex + (int)field, value, true, true, true);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveField(NamePlateStringField field)
{
this.context.StringData->SetValue(
this.ArrayIndex + (int)field,
(byte*)NamePlateGui.EmptyStringPointer,
true,
false,
true);
}
/// <summary>
/// Resets the state of this handler for re-use in a new update.
/// </summary>
internal void ResetState()
{
this.gameObjectId = null;
this.gameObject = null;
this.infoView = null;
this.partsContainer = null;
}
/// <summary>
/// Sets the string array value for the provided field, unless it was already set to the special empty string
/// pointer used by the Remove methods.
/// </summary>
/// <param name="field">The field to write to.</param>
/// <param name="value">The SeString to write.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void WeakSetField(NamePlateStringField field, SeString value)
{
if ((nint)this.GetFieldAsPointer(field) == NamePlateGui.EmptyStringPointer)
return;
this.context.StringData->SetValue(this.ArrayIndex + (int)field, value.Encode(), true, true, true);
}
}

View File

@@ -1,20 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.Nameplates.EventArgs
{
public class AddonNamePlate_SetPlayerNameEventArgs : HookWithResultBaseEventArgs<IntPtr>
{
public IntPtr PlayerNameplateObjectPtr { get; set; }
public IntPtr TitlePtr { get; set; }
public IntPtr NamePtr { get; set; }
public IntPtr FreeCompanyPtr { get; set; }
public IntPtr PrefixPtr { get; set; }
public bool IsTitleAboveName { get; set; }
public bool IsTitleVisible { get; set; }
public int IconID { get; set; }
}
}

View File

@@ -1,43 +0,0 @@
using Dalamud.Game.Text.SeStringHandling;
using Pilz.Dalamud.Nameplates.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.Nameplates.EventArgs
{
public class AddonNamePlate_SetPlayerNameManagedEventArgs : HookWithResultManagedBaseEventArgs<IntPtr>
{
public new AddonNamePlate_SetPlayerNameEventArgs OriginalEventArgs
{
get => base.OriginalEventArgs as AddonNamePlate_SetPlayerNameEventArgs;
set => base.OriginalEventArgs = value;
}
public SafeNameplateObject SafeNameplateObject { get; set; }
public SeString Title { get; internal set; }
public SeString Name { get; internal set; }
public SeString FreeCompany { get; internal set; }
public SeString Prefix { get; internal set; }
public bool IsTitleAboveName
{
get => OriginalEventArgs.IsTitleAboveName;
set => OriginalEventArgs.IsTitleAboveName = value;
}
public bool IsTitleVisible
{
get => OriginalEventArgs.IsTitleVisible;
set => OriginalEventArgs.IsTitleVisible = value;
}
public int IconID
{
get => OriginalEventArgs.IconID;
set => OriginalEventArgs.IconID = value;
}
}
}

View File

@@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.Nameplates.EventArgs
{
public abstract class HookBaseEventArgs
{
internal event Action CallOriginal;
public void Original()
{
CallOriginal?.Invoke();
}
}
}

View File

@@ -1,13 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.Nameplates.EventArgs
{
public abstract class HookManagedBaseEventArgs
{
public HookBaseEventArgs OriginalEventArgs { get; internal set; }
}
}

View File

@@ -1,21 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.Nameplates.EventArgs
{
public abstract class HookWithResultBaseEventArgs<TResult>
{
internal event Func<TResult> CallOriginal;
public TResult Result { get; set; }
// Call Original based on the given properties
public TResult Original()
{
return CallOriginal.Invoke();
}
}
}

View File

@@ -1,13 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.Nameplates.EventArgs
{
public abstract class HookWithResultManagedBaseEventArgs<TResult>
{
public HookWithResultBaseEventArgs<TResult> OriginalEventArgs { get; internal set; }
}
}

View File

@@ -1,43 +0,0 @@
using Dalamud.Logging;
using Dalamud.Plugin;
using FFXIVClientStructs.FFXIV.Client.UI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.Nameplates.Model
{
public class SafeAddonNameplate
{
private readonly DalamudPluginInterface Interface;
public IntPtr Pointer => PluginServices.GameGui.GetAddonByName("NamePlate", 1);
public SafeAddonNameplate(DalamudPluginInterface pluginInterface)
{
Interface = pluginInterface;
}
public unsafe SafeNameplateObject GetNamePlateObject(int index)
{
SafeNameplateObject result = null;
if (Pointer != IntPtr.Zero)
{
var npObjectArrayPtrPtr = Pointer + Marshal.OffsetOf(typeof(AddonNamePlate), nameof(AddonNamePlate.NamePlateObjectArray)).ToInt32();
var npObjectArrayPtr = Marshal.ReadIntPtr(npObjectArrayPtrPtr);
if (npObjectArrayPtr != IntPtr.Zero)
{
var npObjectPtr = npObjectArrayPtr + Marshal.SizeOf(typeof(AddonNamePlate.NamePlateObject)) * index;
result = new(npObjectPtr, index);
}
}
return result;
}
}
}

View File

@@ -1,57 +0,0 @@
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.Nameplates.Model
{
public class SafeNameplateInfo
{
public readonly IntPtr Pointer;
public readonly RaptureAtkModule.NamePlateInfo Data;
public SafeNameplateInfo(IntPtr pointer)
{
Pointer = pointer;
Data = Marshal.PtrToStructure<RaptureAtkModule.NamePlateInfo>(Pointer);
}
internal IntPtr NameAddress => GetStringPtr(nameof(RaptureAtkModule.NamePlateInfo.Name));
internal IntPtr FcNameAddress => GetStringPtr(nameof(RaptureAtkModule.NamePlateInfo.FcName));
internal IntPtr TitleAddress => GetStringPtr(nameof(RaptureAtkModule.NamePlateInfo.Title));
internal IntPtr DisplayTitleAddress => GetStringPtr(nameof(RaptureAtkModule.NamePlateInfo.DisplayTitle));
internal IntPtr LevelTextAddress => GetStringPtr(nameof(RaptureAtkModule.NamePlateInfo.LevelText));
public string Name => GetString(NameAddress);
public string FcName => GetString(FcNameAddress);
public string Title => GetString(TitleAddress);
public string DisplayTitle => GetString(DisplayTitleAddress);
public string LevelText => GetString(LevelTextAddress);
//public bool IsPlayerCharacter() => XivApi.IsPlayerCharacter(Data.ObjectID.ObjectID);
//public bool IsPartyMember() => XivApi.IsPartyMember(Data.ObjectID.ObjectID);
//public bool IsAllianceMember() => XivApi.IsAllianceMember(Data.ObjectID.ObjectID);
//public uint GetJobID() => GetJobId(Data.ObjectID.ObjectID);
private unsafe IntPtr GetStringPtr(string name)
{
var namePtr = Pointer + Marshal.OffsetOf(typeof(RaptureAtkModule.NamePlateInfo), name).ToInt32();
var stringPtrPtr = namePtr + Marshal.OffsetOf(typeof(Utf8String), nameof(Utf8String.StringPtr)).ToInt32();
var stringPtr = Marshal.ReadIntPtr(stringPtrPtr);
return stringPtr;
}
private string GetString(IntPtr stringPtr)
{
return Marshal.PtrToStringUTF8(stringPtr);
}
}
}

View File

@@ -1,128 +0,0 @@
using Dalamud.Logging;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.Nameplates.Model
{
public class SafeNameplateObject
{
public IntPtr Pointer { get; }
public AddonNamePlate.NamePlateObject Data { get; }
private int _Index;
private SafeNameplateInfo _NamePlateInfo;
public SafeNameplateObject(IntPtr pointer, int index = -1)
{
Pointer = pointer;
Data = Marshal.PtrToStructure<AddonNamePlate.NamePlateObject>(pointer);
_Index = index;
}
public int Index
{
get
{
int result = _Index;
if (_Index == -1)
{
var addon = XivApi.GetSafeAddonNamePlate();
var npObject0 = addon.GetNamePlateObject(0);
if (npObject0 == null)
result = -1; // NamePlateObject0 was null
else
{
var npObjectBase = npObject0.Pointer;
var npObjectSize = Marshal.SizeOf(typeof(AddonNamePlate.NamePlateObject));
var index = (Pointer.ToInt64() - npObjectBase.ToInt64()) / npObjectSize;
if (index < 0 || index >= 50)
result = -2; // NamePlateObject index was out of bounds
else
result = _Index = (int)index;
}
}
return result;
}
}
public SafeNameplateInfo NamePlateInfo
{
get
{
SafeNameplateInfo result = null;
if (_NamePlateInfo != null)
{
var rapturePtr = XivApi.RaptureAtkModulePtr;
if (rapturePtr != IntPtr.Zero)
{
var npInfoArrayPtr = rapturePtr + Marshal.OffsetOf(typeof(RaptureAtkModule), nameof(RaptureAtkModule.NamePlateInfoArray)).ToInt32();
var npInfoPtr = npInfoArrayPtr + Marshal.SizeOf(typeof(RaptureAtkModule.NamePlateInfo)) * Index;
result = _NamePlateInfo = new SafeNameplateInfo(npInfoPtr);
}
}
return result;
}
}
#region Getters
public unsafe IntPtr IconImageNodeAddress => Marshal.ReadIntPtr(Pointer + Marshal.OffsetOf(typeof(AddonNamePlate.NamePlateObject), nameof(AddonNamePlate.NamePlateObject.IconImageNode)).ToInt32());
public unsafe IntPtr NameNodeAddress => Marshal.ReadIntPtr(Pointer + Marshal.OffsetOf(typeof(AddonNamePlate.NamePlateObject), nameof(AddonNamePlate.NamePlateObject.NameText)).ToInt32());
public AtkImageNode IconImageNode => Marshal.PtrToStructure<AtkImageNode>(IconImageNodeAddress);
public AtkTextNode NameTextNode => Marshal.PtrToStructure<AtkTextNode>(NameNodeAddress);
#endregion
public unsafe bool IsVisible => Data.IsVisible;
public unsafe bool IsLocalPlayer => Data.IsLocalPlayer;
public bool IsPlayer => Data.NameplateKind == 0;
//public void SetIconScale(float scale, bool force = false)
//{
// if (force || IconImageNode.AtkResNode.ScaleX != scale || IconImageNode.AtkResNode.ScaleY != scale)
// {
// Instance.SetNodeScale(IconImageNodeAddress, scale, scale);
// }
//}
//public void SetNameScale(float scale, bool force = false)
//{
// if (force || NameTextNode.AtkResNode.ScaleX != scale || NameTextNode.AtkResNode.ScaleY != scale)
// {
// Instance.SetNodeScale(NameNodeAddress, scale, scale);
// }
//}
//public unsafe void SetName(IntPtr ptr)
//{
// NameTextNode.SetText("aaa");
//}
//public void SetIcon(int icon)
//{
// IconImageNode.LoadIconTexture(icon, 1);
//}
public void SetIconPosition(short x, short y)
{
var iconXAdjustPtr = Pointer + Marshal.OffsetOf(typeof(AddonNamePlate.NamePlateObject), nameof(AddonNamePlate.NamePlateObject.IconXAdjust)).ToInt32();
var iconYAdjustPtr = Pointer + Marshal.OffsetOf(typeof(AddonNamePlate.NamePlateObject), nameof(AddonNamePlate.NamePlateObject.IconYAdjust)).ToInt32();
Marshal.WriteInt16(iconXAdjustPtr, x);
Marshal.WriteInt16(iconYAdjustPtr, y);
}
}
}

View File

@@ -1,31 +0,0 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.Nameplates.Model
{
[JsonConverter(typeof(StringEnumConverter))]
public enum StatusIcons
{
Disconnecting = 061503,
InDuty = 061506,
ViewingCutscene = 061508,
Busy = 061509,
Idle = 061511,
DutyFinder = 061517,
PartyLeader = 061521,
PartyMember = 061522,
RolePlaying = 061545,
GroupPose = 061546,
NewAdventurer = 061523,
Mentor = 061540,
MentorPvE = 061542,
MentorCrafting = 061543,
MentorPvP = 061544,
Returner = 061547,
}
}

View File

@@ -1,218 +0,0 @@
using Dalamud.Hooking;
using Pilz.Dalamud.Nameplates.EventArgs;
using Dalamud.Utility.Signatures;
using ImGuiNET;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Pilz.Dalamud.Nameplates.Model;
using Lumina.Excel.GeneratedSheets;
using System.Xml.Linq;
namespace Pilz.Dalamud.Nameplates
{
public class NameplateHooks : IDisposable
{
/// <summary>
/// Will be executed when the the Game wants to update the content of a nameplate with the details of the Player.
/// </summary>
public event AddonNamePlate_SetPlayerNameEventHandler AddonNamePlate_SetPlayerName;
public delegate void AddonNamePlate_SetPlayerNameEventHandler(AddonNamePlate_SetPlayerNameEventArgs eventArgs);
/// <summary>
/// Will be executed when the the Game wants to update the content of a nameplate with the details of the Player.
/// This will event acts on a higher level with SeString instead of IntPtr e.g.
/// </summary>
public event AddonNamePlate_SetPlayerNameManagedEventHandler AddonNamePlate_SetPlayerNameManaged;
public delegate void AddonNamePlate_SetPlayerNameManagedEventHandler(AddonNamePlate_SetPlayerNameManagedEventArgs eventArgs);
[Signature("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 48 8B 5C 24 ?? 45 38 BE", DetourName = nameof(SetPlayerNameplateDetour))]
private Hook<AddonNamePlate_SetPlayerNameplateDetour>? hook_AddonNamePlate_SetPlayerNameplateDetour = null;
private unsafe delegate IntPtr AddonNamePlate_SetPlayerNameplateDetour(IntPtr playerNameplateObjectPtr, bool isTitleAboveName, bool isTitleVisible, IntPtr titlePtr, IntPtr namePtr, IntPtr freeCompanyPtr, IntPtr prefix, int iconId);
/// <summary>
/// Defines if all hooks are enabled. If this is false, then there might be something wrong or the class already has been disposed.
/// </summary>
public bool IsValid
{
get
{
var isValid = true;
isValid &= IsHookEnabled(hook_AddonNamePlate_SetPlayerNameplateDetour);
return isValid;
}
}
/// <summary>
/// Create a new instance of NAmeplateHooks and automatically initialize and enable all Hooks.
/// </summary>
public NameplateHooks()
{
SignatureHelper.Initialise(this);
}
~NameplateHooks()
{
Dispose();
}
public void Dispose()
{
Unhook();
}
/// <summary>
/// Initialize and enable all Hooks.
/// </summary>
internal void Initialize()
{
hook_AddonNamePlate_SetPlayerNameplateDetour?.Enable();
}
/// <summary>
/// Disable all Hooks.
/// </summary>
internal void Unhook()
{
hook_AddonNamePlate_SetPlayerNameplateDetour?.Disable();
}
private static bool IsHookEnabled<T>(Hook<T> hook) where T : Delegate
{
return hook != null && hook.IsEnabled;
}
private IntPtr SetPlayerNameplateDetour(IntPtr playerNameplateObjectPtr, bool isTitleAboveName, bool isTitleVisible, IntPtr titlePtr, IntPtr namePtr, IntPtr freeCompanyPtr, IntPtr prefix, int iconId)
{
var result = IntPtr.Zero;
if (IsHookEnabled(hook_AddonNamePlate_SetPlayerNameplateDetour))
{
var eventArgs = new AddonNamePlate_SetPlayerNameEventArgs
{
PlayerNameplateObjectPtr = playerNameplateObjectPtr,
TitlePtr = titlePtr,
NamePtr = namePtr,
FreeCompanyPtr = freeCompanyPtr,
PrefixPtr = prefix,
IsTitleAboveName = isTitleAboveName,
IsTitleVisible = isTitleVisible,
IconID = iconId
};
void callOriginal()
{
eventArgs.Result = eventArgs.Original();
}
// Add handler for the Original call
eventArgs.CallOriginal += () =>
{
return hook_AddonNamePlate_SetPlayerNameplateDetour.Original(
eventArgs.PlayerNameplateObjectPtr,
eventArgs.IsTitleAboveName,
eventArgs.IsTitleVisible,
eventArgs.TitlePtr,
eventArgs.NamePtr,
eventArgs.FreeCompanyPtr,
prefix,
eventArgs.IconID);
};
// Invoke Event
var hasDefaultHookEvent = AddonNamePlate_SetPlayerName != null;
AddonNamePlate_SetPlayerName?.Invoke(eventArgs);
if (AddonNamePlate_SetPlayerNameManaged != null)
{
var freeTitle = false;
var freeName = false;
var freeFreeCompany = false;
var freePrefix = false;
// Create NamePlateObject if possible
var namePlateObj = new SafeNameplateObject(playerNameplateObjectPtr);
// Create new event
var managedEventArgs = new AddonNamePlate_SetPlayerNameManagedEventArgs
{
OriginalEventArgs = eventArgs,
SafeNameplateObject = namePlateObj,
Title = GameInterfaceHelper.ReadSeString(eventArgs.TitlePtr),
Name = GameInterfaceHelper.ReadSeString(eventArgs.NamePtr),
FreeCompany = GameInterfaceHelper.ReadSeString(eventArgs.FreeCompanyPtr),
Prefix = GameInterfaceHelper.ReadSeString(eventArgs.PrefixPtr)
};
// Get raw string content
var titleRaw = managedEventArgs.Title.Encode();
var nameRaw = managedEventArgs.Name.Encode();
var freeCompanyRaw = managedEventArgs.FreeCompany.Encode();
var prefixRaw = managedEventArgs.Prefix.Encode();
// Invoke Managed Event
AddonNamePlate_SetPlayerNameManaged.Invoke(managedEventArgs);
// Get new Title string content
var titleNewRaw = managedEventArgs.Title.Encode();
if (!titleRaw.SequenceEqual(titleNewRaw))
{
eventArgs.TitlePtr = GameInterfaceHelper.PluginAllocate(titleNewRaw);
freeTitle = true;
}
// Get new Name string content
var nameNewRaw = managedEventArgs.Name.Encode();
if (!nameRaw.SequenceEqual(nameNewRaw))
{
eventArgs.NamePtr = GameInterfaceHelper.PluginAllocate(nameNewRaw);
freeName = true;
}
// Get new Free Company string content
var freeCompanyNewRaw = managedEventArgs.FreeCompany.Encode();
if (!freeCompanyRaw.SequenceEqual(freeCompanyNewRaw))
{
eventArgs.FreeCompanyPtr = GameInterfaceHelper.PluginAllocate(freeCompanyNewRaw);
freeFreeCompany = true;
}
// Get new Prefix string content
var prefixNewRaw = managedEventArgs.Prefix.Encode();
if (!prefixRaw.SequenceEqual(prefixNewRaw))
{
eventArgs.PrefixPtr = GameInterfaceHelper.PluginAllocate(prefixNewRaw);
freePrefix = true;
}
// Call Original as we changed something
callOriginal();
// Free memory
if (freeTitle)
GameInterfaceHelper.PluginFree(eventArgs.TitlePtr);
if (freeName)
GameInterfaceHelper.PluginFree(eventArgs.NamePtr);
if (freeFreeCompany)
GameInterfaceHelper.PluginFree(eventArgs.FreeCompanyPtr);
if (freePrefix)
GameInterfaceHelper.PluginFree(eventArgs.PrefixPtr);
}
else if(!hasDefaultHookEvent)
{
// Call original in case of nothing get called, just to get secure it will not break the game when not calling it.
callOriginal();
}
// Set result
result = eventArgs.Result;
}
return result;
}
}
}

View File

@@ -1,83 +0,0 @@
using Dalamud.Hooking;
using Pilz.Dalamud.Nameplates.EventArgs;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.UI;
using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Objects.Types;
using Pilz.Dalamud.Nameplates.Model;
namespace Pilz.Dalamud.Nameplates
{
public class NameplateManager : IDisposable
{
/// <summary>
/// Provides events that you can hook to.
/// </summary>
public NameplateHooks Hooks { get; init; } = new();
/// <summary>
/// Defines if all hooks are enabled and the NameplateManager is ready to go. If this is false, then there might be something wrong or something already has been disposed.
/// </summary>
public bool IsValid => Hooks.IsValid;
/// <summary>
/// Creates a new instance of the NameplateManager.
/// </summary>
public NameplateManager()
{
Hooks.Initialize();
}
~NameplateManager()
{
Dispose();
}
public void Dispose()
{
Hooks?.Dispose();
}
public static T? GetNameplateGameObject<T>(SafeNameplateObject namePlateObject) where T : GameObject
{
return GetNameplateGameObject<T>(namePlateObject.Pointer);
}
public static T? GetNameplateGameObject<T>(IntPtr nameplateObjectPtr) where T : GameObject
{
// Get the nameplate object array
var nameplateAddonPtr = PluginServices.GameGui.GetAddonByName("NamePlate", 1);
var nameplateObjectArrayPtrPtr = nameplateAddonPtr + Marshal.OffsetOf(typeof(AddonNamePlate), nameof(AddonNamePlate.NamePlateObjectArray)).ToInt32();
var nameplateObjectArrayPtr = Marshal.ReadIntPtr(nameplateObjectArrayPtrPtr);
if (nameplateObjectArrayPtr == IntPtr.Zero)
{
return null;
}
// Determine the index of the nameplate object within the nameplate object array
var namePlateObjectSize = Marshal.SizeOf(typeof(AddonNamePlate.NamePlateObject));
var namePlateObjectPtr0 = nameplateObjectArrayPtr + namePlateObjectSize * 0;
var namePlateIndex = (nameplateObjectPtr.ToInt64() - namePlateObjectPtr0.ToInt64()) / namePlateObjectSize;
if (namePlateIndex < 0 || namePlateIndex >= AddonNamePlate.NumNamePlateObjects)
{
return null;
}
// Get the nameplate info array
IntPtr nameplateInfoArrayPtr = IntPtr.Zero;
unsafe
{
var framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance();
nameplateInfoArrayPtr = new IntPtr(&framework->GetUiModule()->GetRaptureAtkModule()->NamePlateInfoArray);
}
// Get the nameplate info for the nameplate object
var namePlateInfoPtr = new IntPtr(nameplateInfoArrayPtr.ToInt64() + Marshal.SizeOf(typeof(RaptureAtkModule.NamePlateInfo)) * namePlateIndex);
RaptureAtkModule.NamePlateInfo namePlateInfo = Marshal.PtrToStructure<RaptureAtkModule.NamePlateInfo>(namePlateInfoPtr);
// Return the object for its object id
var objectId = namePlateInfo.ObjectID.ObjectID;
return PluginServices.ObjectTable.SearchById(objectId) as T;
}
}
}

View File

@@ -1,59 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Pilz.Dalamud.Nameplates.EventArgs;
using Pilz.Dalamud.Tools.Strings;
namespace Pilz.Dalamud.Nameplates.Tools
{
public class NameplateChanges
{
private readonly Dictionary<NameplateElements, StringChangesProps> changes = new();
public NameplateChanges()
{
changes.Add(NameplateElements.Title, new());
changes.Add(NameplateElements.Name, new());
changes.Add(NameplateElements.FreeCompany, new());
}
public NameplateChanges(AddonNamePlate_SetPlayerNameManagedEventArgs eventArgs) : this()
{
GetProps(NameplateElements.Title).Destination = eventArgs.Title;
GetProps(NameplateElements.Name).Destination = eventArgs.Name;
GetProps(NameplateElements.FreeCompany).Destination = eventArgs.FreeCompany;
}
/// <summary>
/// Gets the properties with the changes of an element of your choice where you can add your payloads to a change and setup some options.
/// </summary>
/// <param name="element">The position of your choice.</param>
/// <returns></returns>
public StringChangesProps GetProps(NameplateElements element)
{
return changes[element];
}
/// <summary>
/// Gets the changes of an element of your choice where you can add your payloads to a change.
/// </summary>
/// <param name="element">The position of your choice.</param>
/// <returns></returns>
public StringChanges GetChanges(NameplateElements element)
{
return GetProps(element).StringChanges;
}
/// <summary>
/// Gets a change of the position of the element of your choice where you can add your payloads.
/// </summary>
/// <param name="element">The position of your choice.</param>
/// <returns></returns>
public StringChange GetChange(NameplateElements element, StringPosition position)
{
return GetChanges(element).GetChange(position);
}
}
}

View File

@@ -1,25 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.Nameplates.Tools
{
public class NameplateChangesProps
{
/// <summary>
/// All the changes to the nameplate that should be made.
/// </summary>
public NameplateChanges Changes { get; set; }
public NameplateChangesProps()
{
}
public NameplateChangesProps(NameplateChanges changes) : this()
{
Changes = changes;
}
}
}

View File

@@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.Nameplates.Tools
{
public enum NameplateElements
{
Name,
Title,
FreeCompany
}
}

View File

@@ -1,52 +0,0 @@
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Pilz.Dalamud.ActivityContexts;
using Pilz.Dalamud.Nameplates.Model;
using Pilz.Dalamud.Tools;
using Pilz.Dalamud.Tools.Strings;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.Nameplates.Tools
{
public static class NameplateUpdateFactory
{
public static void ApplyNameplateChanges(NameplateChangesProps props)
{
foreach (NameplateElements element in Enum.GetValues(typeof(NameplateElements)))
{
var change = props.Changes.GetProps(element);
StringUpdateFactory.ApplyStringChanges(change);
}
}
public static bool ApplyStatusIconWithPrio(ref int statusIcon, int newStatusIcon, StringChange stringChange, ActivityContext activityContext, StatusIconPriorizer priorizer, bool moveIconToNameplateIfPossible)
{
bool? isPrio = null;
var fontIcon = StatusIconFontConverter.GetBitmapFontIconFromStatusIcon((StatusIcons)statusIcon);
if (moveIconToNameplateIfPossible)
{
if (fontIcon != null)
{
// Set new font icon as string change
var iconPayload = new IconPayload(fontIcon.Value);
stringChange.Payloads.Insert(0, iconPayload);
// If we moved it, we don't need it as icon anymore, yay :D
isPrio = false;
}
}
isPrio ??= priorizer.IsPriorityIcon(statusIcon, activityContext);
if (!isPrio.Value)
statusIcon = newStatusIcon;
return isPrio.Value;
}
}
}

View File

@@ -1,67 +0,0 @@
using Lumina.Excel.GeneratedSheets;
using Pilz.Dalamud.ActivityContexts;
using Pilz.Dalamud.Nameplates.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.Nameplates.Tools
{
public class StatusIconPriorizer
{
private static StatusIconPriorizerSettings DefaultSettings { get; } = new();
public StatusIconPriorizerSettings Settings { get; init; }
public StatusIconPriorizer() : this(DefaultSettings)
{
}
public StatusIconPriorizer(StatusIconPriorizerSettings settings)
{
Settings = settings;
}
/// <summary>
/// Check for an icon that should take priority over the job icon,
/// taking into account whether or not the player is in a duty.
/// </summary>
/// <param name="iconId">The incoming icon id that is being overwritten by the plugin.</param>
/// <param name="priorityIconId">The icon id that should be used.</param>
/// <returns>Whether a priority icon was found.</returns>
public bool IsPriorityIcon(int iconId, ActivityContext activityContext)
{
bool isPrioIcon;
if (!Settings.UsePriorizedIcons && iconId != (int)StatusIcons.Disconnecting && iconId != (int)StatusIcons.Disconnecting + 50)
isPrioIcon = false;
else
{
// Select which set of priority icons to use based on whether we're in a duty
// In the future, there can be a third list used when in combat
var priorityIcons = GetPriorityIcons(activityContext);
// Determine whether the incoming icon should take priority over the job icon
// Check the id plus 50 as that's an alternately sized version
isPrioIcon = priorityIcons.Contains(iconId) || priorityIcons.Contains(iconId + 50);
}
return isPrioIcon;
}
private IEnumerable<int> GetPriorityIcons(ActivityContext activityContext)
{
StatusIconPriorizerConditionSets set;
if (activityContext.ZoneType == ZoneType.Foray)
set = StatusIconPriorizerConditionSets.InForay;
else if (activityContext.IsInDuty)
set = StatusIconPriorizerConditionSets.InDuty;
else
set = StatusIconPriorizerConditionSets.Overworld;
return Settings.GetConditionSet(set).Select(n => (int)n);
}
}
}

View File

@@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.Nameplates.Tools
{
public enum StatusIconPriorizerConditionSets
{
Overworld,
InDuty,
InForay
}
}

View File

@@ -1,110 +0,0 @@
using Newtonsoft.Json;
using Pilz.Dalamud.ActivityContexts;
using Pilz.Dalamud.Nameplates.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.Nameplates.Tools
{
public class StatusIconPriorizerSettings
{
[JsonProperty("IconConditionSets")]
private Dictionary<StatusIconPriorizerConditionSets, List<StatusIcons>> iconConditionSets = new();
public bool UsePriorizedIcons { get; set; } = true;
[JsonConstructor]
private StatusIconPriorizerSettings(JsonConstructorAttribute dummy)
{
}
public StatusIconPriorizerSettings() : this(false)
{
}
public StatusIconPriorizerSettings(bool fillWithDefaultSettings)
{
foreach (StatusIconPriorizerConditionSets set in Enum.GetValues(typeof(StatusIconPriorizerConditionSets)))
iconConditionSets.Add(set, new List<StatusIcons>());
if (fillWithDefaultSettings)
FillWithDefaultSettings();
}
public List<StatusIcons> GetConditionSet(StatusIconPriorizerConditionSets set)
{
return iconConditionSets[set];
}
public void ResetToEmpty()
{
foreach (var kvp in iconConditionSets)
kvp.Value.Clear();
}
public void ResetToDefault()
{
ResetToEmpty();
FillWithDefaultSettings();
}
private void FillWithDefaultSettings()
{
var setOverworld = GetConditionSet(StatusIconPriorizerConditionSets.Overworld);
setOverworld.AddRange(new[]
{
StatusIcons.Disconnecting, // Disconnecting
StatusIcons.InDuty, // In Duty
StatusIcons.ViewingCutscene, // Viewing Cutscene
StatusIcons.Busy, // Busy
StatusIcons.Idle, // Idle
StatusIcons.DutyFinder, // Duty Finder
StatusIcons.PartyLeader, // Party Leader
StatusIcons.PartyMember, // Party Member
StatusIcons.RolePlaying, // Role Playing
StatusIcons.GroupPose, // Group Pose
StatusIcons.Mentor,
StatusIcons.MentorCrafting,
StatusIcons.MentorPvE,
StatusIcons.MentorPvP,
StatusIcons.Returner,
StatusIcons.NewAdventurer,
});
var setInDuty = GetConditionSet(StatusIconPriorizerConditionSets.InDuty);
setInDuty.AddRange(new[]
{
StatusIcons.Disconnecting, // Disconnecting
StatusIcons.ViewingCutscene, // Viewing Cutscene
StatusIcons.Idle, // Idle
StatusIcons.GroupPose, // Group Pose
StatusIcons.Mentor,
StatusIcons.MentorCrafting,
StatusIcons.MentorPvE,
StatusIcons.MentorPvP,
StatusIcons.Returner,
StatusIcons.NewAdventurer,
});
var setInForay = GetConditionSet(StatusIconPriorizerConditionSets.InForay);
setInForay.AddRange(new[]
{
// This allows you to see which players don't have a party
StatusIcons.InDuty, // In Duty
StatusIcons.Disconnecting, // Disconnecting
StatusIcons.ViewingCutscene, // Viewing Cutscene
StatusIcons.Idle, // Idle
StatusIcons.GroupPose, // Group Pose
StatusIcons.Mentor,
StatusIcons.MentorCrafting,
StatusIcons.MentorPvE,
StatusIcons.MentorPvP,
StatusIcons.Returner,
StatusIcons.NewAdventurer,
});
}
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>annotations</Nullable> <Nullable>annotations</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@@ -9,6 +9,7 @@
<ProduceReferenceAssembly>false</ProduceReferenceAssembly> <ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<Platforms>x64</Platforms> <Platforms>x64</Platforms>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
@@ -24,9 +25,17 @@
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/Pilzinsel64/Pilz.Dalamud</RepositoryUrl> <RepositoryUrl>https://github.com/Pilzinsel64/Pilz.Dalamud</RepositoryUrl>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<Version>0.4.0</Version> <Version>0.6.1.1</Version>
<GenerateDocumentationFile>True</GenerateDocumentationFile> <GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<NoWarn>1701;1702;1591</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<NoWarn>1701;1702;1591</NoWarn>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="FFXIVClientStructs"> <Reference Include="FFXIVClientStructs">
@@ -60,7 +69,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="..\README.md" Pack="true" PackagePath="\"/> <None Include="..\README.md" Pack="true" PackagePath="\" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,28 +1,21 @@
using Dalamud.Data; using Dalamud.IoC;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.Gui;
using Dalamud.IoC;
using Dalamud.Plugin; using Dalamud.Plugin;
using System; using Dalamud.Plugin.Services;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud namespace Pilz.Dalamud;
public class PluginServices
{ {
public class PluginServices [PluginService] public static IDalamudPluginInterface PluginInterface { get; set; }
{ [PluginService] public static IGameGui GameGui { get; set; }
[PluginService] public static GameGui GameGui { get; set; } = null; [PluginService] public static IClientState ClientState { get; set; }
[PluginService] public static DalamudPluginInterface PluginInterface { get; set; } = null; [PluginService] public static IDataManager DataManager { get; set; }
[PluginService] public static ClientState ClientState { get; set; } = null; [PluginService] public static IObjectTable ObjectTable { get; set; }
[PluginService] public static DataManager DataManager { get; set; } = null; [PluginService] public static IGameInteropProvider GameInteropProvider { get; set; }
[PluginService] public static ObjectTable ObjectTable { get; set; } = null; [PluginService] public static IAddonLifecycle AddonLifecycle { get; set; }
public static void Initialize(DalamudPluginInterface dalamudPluginInterface) public static void Initialize(IDalamudPluginInterface dalamudPluginInterface)
{ {
dalamudPluginInterface.Create<PluginServices>(); dalamudPluginInterface.Create<PluginServices>();
}
} }
} }

View File

@@ -0,0 +1,25 @@
using Pilz.Dalamud.NamePlate;
namespace Pilz.Dalamud.Tools.NamePlates;
public class NameplateChanges
{
private readonly List<NameplateElementChange> changes = [];
public NameplateChanges(INamePlateUpdateHandler handler)
{
changes.Add(new(NameplateElements.Title, handler));
changes.Add(new(NameplateElements.Name, handler));
changes.Add(new(NameplateElements.FreeCompany, handler));
}
/// <summary>
/// Gets the properties with the changes of an element of your choice where you can add your payloads to a change and setup some options.
/// </summary>
/// <param name="element">The position of your choice.</param>
/// <returns></returns>
public NameplateElementChange GetChange(NameplateElements element)
{
return changes.FirstOrDefault(n => n.Element == element);
}
}

View File

@@ -0,0 +1,18 @@
namespace Pilz.Dalamud.Tools.NamePlates;
public class NameplateChangesProps
{
/// <summary>
/// All the changes to the nameplate that should be made.
/// </summary>
public NameplateChanges Changes { get; set; }
public NameplateChangesProps()
{
}
public NameplateChangesProps(NameplateChanges changes) : this()
{
Changes = changes;
}
}

View File

@@ -0,0 +1,47 @@
using Dalamud.Game.Text.SeStringHandling;
using Pilz.Dalamud.NamePlate;
using Pilz.Dalamud.Tools.Strings;
namespace Pilz.Dalamud.Tools.NamePlates;
public class NameplateElementChange(NameplateElements element, INamePlateUpdateHandler handler)
{
public NameplateElements Element => element;
public StringChanges Changes { get; set; } = new();
public void ApplyFormatting(SeString prefix, SeString postfix)
{
var parts = (prefix, postfix);
switch (element)
{
case NameplateElements.Name:
handler.NameParts.TextWrap = parts;
break;
case NameplateElements.Title:
handler.TitleParts.OuterWrap = parts;
break;
case NameplateElements.FreeCompany:
handler.FreeCompanyTagParts.OuterWrap = parts;
break;
}
}
public void ApplyChanges()
{
if (Changes.Any())
{
StringUpdateFactory.ApplyStringChanges(new()
{
StringChanges = Changes,
Destination = element switch
{
NameplateElements.Name => handler.NameParts.Text ??= handler.InfoView.Name,
NameplateElements.Title => handler.TitleParts.Text ??= handler.InfoView.Title,
NameplateElements.FreeCompany => handler.FreeCompanyTagParts.Text ??= handler.InfoView.FreeCompanyTag,
_ => null,
},
});
}
}
}

View File

@@ -0,0 +1,8 @@
namespace Pilz.Dalamud.Tools.NamePlates;
public enum NameplateElements
{
Name,
Title,
FreeCompany,
}

View File

@@ -0,0 +1,48 @@
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Pilz.Dalamud.ActivityContexts;
using Pilz.Dalamud.Icons;
using Pilz.Dalamud.NamePlate;
namespace Pilz.Dalamud.Tools.NamePlates;
public static class NameplateUpdateFactory
{
public static void ApplyNameplateChanges(NameplateChangesProps props)
{
foreach (NameplateElements element in Enum.GetValues(typeof(NameplateElements)))
{
var change = props.Changes.GetChange(element);
change.ApplyChanges();
}
}
public static bool ApplyStatusIconWithPrio(INamePlateUpdateHandler handler, int newStatusIcon, ActivityContext activityContext, StatusIconPriorizer priorizer, bool moveIconToNameplateIfPossible)
{
bool? isPrio = null;
var fontIcon = StatusIconFontConverter.GetBitmapFontIconFromStatusIcon((StatusIcons)handler.NameIconId);
if (moveIconToNameplateIfPossible)
{
if (fontIcon != null)
{
// Set new font icon as string change
var icon = new IconPayload(fontIcon.Value); ;
if (handler.StatusPrefix is SeString str)
str.Payloads.Insert(0, icon);
else
handler.StatusPrefix = SeString.Empty.Append(icon);
// If we moved it, we don't need it as icon anymore, yay :D
isPrio = false;
}
}
isPrio ??= priorizer.IsPriorityIcon(handler.NameIconId, activityContext);
if (!isPrio.Value)
handler.NameIconId = newStatusIcon;
return isPrio.Value;
}
}

View File

@@ -0,0 +1,60 @@
using Pilz.Dalamud.ActivityContexts;
using Pilz.Dalamud.Icons;
namespace Pilz.Dalamud.Tools.NamePlates;
public class StatusIconPriorizer
{
private static StatusIconPriorizerSettings DefaultSettings { get; } = new();
public StatusIconPriorizerSettings Settings { get; init; }
public StatusIconPriorizer() : this(DefaultSettings)
{
}
public StatusIconPriorizer(StatusIconPriorizerSettings settings)
{
Settings = settings;
}
/// <summary>
/// Check for an icon that should take priority over the job icon,
/// taking into account whether or not the player is in a duty.
/// </summary>
/// <param name="iconId">The incoming icon id that is being overwritten by the plugin.</param>
/// <param name="activityContext"></param>
/// <returns>Whether a priority icon was found.</returns>
public bool IsPriorityIcon(int iconId, ActivityContext activityContext)
{
bool isPrioIcon;
if (!Settings.UsePriorizedIcons && iconId != (int)StatusIcons.Disconnecting && iconId != (int)StatusIcons.Disconnecting + 50)
isPrioIcon = false;
else
{
// Select which set of priority icons to use based on whether we're in a duty
// In the future, there can be a third list used when in combat
var priorityIcons = GetPriorityIcons(activityContext);
// Determine whether the incoming icon should take priority over the job icon
// Check the id plus 50 as that's an alternately sized version
isPrioIcon = priorityIcons.Contains(iconId) || priorityIcons.Contains(iconId + 50);
}
return isPrioIcon;
}
private IEnumerable<int> GetPriorityIcons(ActivityContext activityContext)
{
StatusIconPriorizerConditionSets set;
if (activityContext.ZoneType == ZoneType.Foray)
set = StatusIconPriorizerConditionSets.InForay;
else if (activityContext.IsInDuty)
set = StatusIconPriorizerConditionSets.InDuty;
else
set = StatusIconPriorizerConditionSets.Overworld;
return Settings.GetConditionSet(set).Select(n => (int)n);
}
}

View File

@@ -0,0 +1,8 @@
namespace Pilz.Dalamud.Tools.NamePlates;
public enum StatusIconPriorizerConditionSets
{
Overworld,
InDuty,
InForay
}

View File

@@ -0,0 +1,103 @@
using Newtonsoft.Json;
using Pilz.Dalamud.Icons;
namespace Pilz.Dalamud.Tools.NamePlates;
public class StatusIconPriorizerSettings
{
[JsonProperty("IconConditionSets")]
private Dictionary<StatusIconPriorizerConditionSets, List<StatusIcons>> iconConditionSets = [];
public bool UsePriorizedIcons { get; set; } = true;
[JsonConstructor]
private StatusIconPriorizerSettings(JsonConstructorAttribute dummy)
{
}
public StatusIconPriorizerSettings() : this(false)
{
}
public StatusIconPriorizerSettings(bool fillWithDefaultSettings)
{
foreach (StatusIconPriorizerConditionSets set in Enum.GetValues(typeof(StatusIconPriorizerConditionSets)))
iconConditionSets.Add(set, []);
if (fillWithDefaultSettings)
FillWithDefaultSettings();
}
public List<StatusIcons> GetConditionSet(StatusIconPriorizerConditionSets set)
{
return iconConditionSets[set];
}
public void ResetToEmpty()
{
foreach (var kvp in iconConditionSets)
kvp.Value.Clear();
}
public void ResetToDefault()
{
ResetToEmpty();
FillWithDefaultSettings();
}
private void FillWithDefaultSettings()
{
var setOverworld = GetConditionSet(StatusIconPriorizerConditionSets.Overworld);
setOverworld.AddRange(new[]
{
StatusIcons.Disconnecting, // Disconnecting
StatusIcons.InDuty, // In Duty
StatusIcons.ViewingCutscene, // Viewing Cutscene
StatusIcons.Busy, // Busy
StatusIcons.Idle, // Idle
StatusIcons.DutyFinder, // Duty Finder
StatusIcons.PartyLeader, // Party Leader
StatusIcons.PartyMember, // Party Member
StatusIcons.RolePlaying, // Role Playing
StatusIcons.GroupPose, // Group Pose
StatusIcons.Mentor,
StatusIcons.MentorCrafting,
StatusIcons.MentorPvE,
StatusIcons.MentorPvP,
StatusIcons.Returner,
StatusIcons.NewAdventurer,
});
var setInDuty = GetConditionSet(StatusIconPriorizerConditionSets.InDuty);
setInDuty.AddRange(new[]
{
StatusIcons.Disconnecting, // Disconnecting
StatusIcons.ViewingCutscene, // Viewing Cutscene
StatusIcons.Idle, // Idle
StatusIcons.GroupPose, // Group Pose
StatusIcons.Mentor,
StatusIcons.MentorCrafting,
StatusIcons.MentorPvE,
StatusIcons.MentorPvP,
StatusIcons.Returner,
StatusIcons.NewAdventurer,
});
var setInForay = GetConditionSet(StatusIconPriorizerConditionSets.InForay);
setInForay.AddRange(new[]
{
// This allows you to see which players don't have a party
StatusIcons.InDuty, // In Duty
StatusIcons.Disconnecting, // Disconnecting
StatusIcons.ViewingCutscene, // Viewing Cutscene
StatusIcons.Idle, // Idle
StatusIcons.GroupPose, // Group Pose
StatusIcons.Mentor,
StatusIcons.MentorCrafting,
StatusIcons.MentorPvE,
StatusIcons.MentorPvP,
StatusIcons.Returner,
StatusIcons.NewAdventurer,
});
}
}

View File

@@ -1,41 +1,35 @@
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Pilz.Dalamud.Nameplates.Model; using Pilz.Dalamud.Icons;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.Tools namespace Pilz.Dalamud.Tools;
public static class StatusIconFontConverter
{ {
public static class StatusIconFontConverter public static StatusIcons? GetStatusIconFromBitmapFontIcon(BitmapFontIcon fontIcon)
{ {
public static StatusIcons? GetStatusIconFromBitmapFontIcon(BitmapFontIcon fontIcon) return fontIcon switch
{ {
return fontIcon switch BitmapFontIcon.NewAdventurer => StatusIcons.NewAdventurer,
{ BitmapFontIcon.Mentor => StatusIcons.Mentor,
BitmapFontIcon.NewAdventurer => StatusIcons.NewAdventurer, BitmapFontIcon.MentorPvE => StatusIcons.MentorPvE,
BitmapFontIcon.Mentor => StatusIcons.Mentor, BitmapFontIcon.MentorCrafting => StatusIcons.MentorCrafting,
BitmapFontIcon.MentorPvE => StatusIcons.MentorPvE, BitmapFontIcon.MentorPvP => StatusIcons.MentorPvP,
BitmapFontIcon.MentorCrafting => StatusIcons.MentorCrafting, BitmapFontIcon.Returner => StatusIcons.Returner,
BitmapFontIcon.MentorPvP => StatusIcons.MentorPvP, _ => null
BitmapFontIcon.Returner => StatusIcons.Returner, };
_ => null }
};
}
public static BitmapFontIcon? GetBitmapFontIconFromStatusIcon(StatusIcons icon) public static BitmapFontIcon? GetBitmapFontIconFromStatusIcon(StatusIcons icon)
{
return icon switch
{ {
return icon switch StatusIcons.NewAdventurer => BitmapFontIcon.NewAdventurer,
{ StatusIcons.Mentor => BitmapFontIcon.Mentor,
StatusIcons.NewAdventurer => BitmapFontIcon.NewAdventurer, StatusIcons.MentorPvE => BitmapFontIcon.MentorPvE,
StatusIcons.Mentor => BitmapFontIcon.Mentor, StatusIcons.MentorCrafting => BitmapFontIcon.MentorCrafting,
StatusIcons.MentorPvE => BitmapFontIcon.MentorPvE, StatusIcons.MentorPvP => BitmapFontIcon.MentorPvP,
StatusIcons.MentorCrafting => BitmapFontIcon.MentorCrafting, StatusIcons.Returner => BitmapFontIcon.Returner,
StatusIcons.MentorPvP => BitmapFontIcon.MentorPvP, _ => null
StatusIcons.Returner => BitmapFontIcon.Returner, };
_ => null
};
}
} }
} }

View File

@@ -1,24 +1,18 @@
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.Tools.Strings namespace Pilz.Dalamud.Tools.Strings;
public class StringChange
{ {
public class StringChange /// <summary>
{ /// The payloads to use for inserting/replacing.
/// <summary> /// </summary>
/// The payloads to use for inserting/replacing. public List<Payload> Payloads { get; init; } = [];
/// </summary>
public List<Payload> Payloads { get; init; } = new();
/// <summary> /// <summary>
/// Defines if only one anchor payload should be used, if using anchor payloads. /// Defines if only one anchor payload should be used, if using anchor payloads.
/// With this true the single anchor payload will be used in StringUpdateFactory instead of the anchor payload list. /// With this true the single anchor payload will be used in StringUpdateFactory instead of the anchor payload list.
/// Not needed to be true for the most cases. /// Not needed to be true for the most cases.
/// </summary> /// </summary>
public bool ForceUsingSingleAnchorPayload { get; set; } = false; public bool ForceUsingSingleAnchorPayload { get; set; } = false;
}
} }

View File

@@ -1,40 +1,32 @@
using Dalamud.Game.Text.SeStringHandling; namespace Pilz.Dalamud.Tools.Strings;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.Tools.Strings public class StringChanges
{ {
public class StringChanges private readonly Dictionary<StringPosition, StringChange> changes = [];
public StringChanges()
{ {
private readonly Dictionary<StringPosition, StringChange> changes = new(); changes.Add(StringPosition.Before, new StringChange());
changes.Add(StringPosition.After, new StringChange());
changes.Add(StringPosition.Replace, new StringChange());
}
public StringChanges() /// <summary>
{ /// Gets a change of the position of your choice where you can add your payloads.
changes.Add(StringPosition.Before, new StringChange()); /// </summary>
changes.Add(StringPosition.After, new StringChange()); /// <param name="position">The position of your choice.</param>
changes.Add(StringPosition.Replace, new StringChange()); /// <returns></returns>
} public StringChange GetChange(StringPosition position)
{
return changes[position];
}
/// <summary> /// <summary>
/// Gets a change of the position of your choice where you can add your payloads. /// Checks if there is any string change listed.
/// </summary> /// </summary>
/// <param name="position">The position of your choice.</param> /// <returns></returns>
/// <returns></returns> public bool Any()
public StringChange GetChange(StringPosition position) {
{ return changes.Sum(n => n.Value.Payloads.Count) != 0;
return changes[position];
}
/// <summary>
/// Checks if there is any string change listed.
/// </summary>
/// <returns></returns>
public bool Any()
{
return changes.Sum(n => n.Value.Payloads.Count) != 0;
}
} }
} }

View File

@@ -1,31 +1,24 @@
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.Tools.Strings namespace Pilz.Dalamud.Tools.Strings;
public class StringChangesProps
{ {
public class StringChangesProps /// <summary>
{ /// The string where the changes should be applied.
/// <summary> /// </summary>
/// The string where the changes should be applied. public SeString Destination { get; set; }
/// </summary> /// <summary>
public SeString Destination { get; set; } /// The changes that should be applied to the destination.
/// <summary> /// </summary>
/// The changes that should be applied to the destination. public StringChanges StringChanges { get; set; } = new();
/// </summary> /// <summary>
public StringChanges StringChanges { get; set; } = new(); /// Payloads to use as anchor where the changes should be applied to.
/// <summary> /// </summary>
/// Payloads to use as anchor where the changes should be applied to. public List<Payload> AnchorPayloads { get; set; } = [];
/// </summary> /// <summary>
public List<Payload> AnchorPayloads { get; set; } = new(); /// A single payload to use as anchor where the changes should be applied to.
/// <summary> /// This property will only be used if StringChange.ForceSingleAnchorPayload is true.
/// A single payload to use as anchor where the changes should be applied to. /// </summary>
/// This property will only be used if StringChange.ForceSingleAnchorPayload is true. public Payload AnchorPayload { get; set; }
/// </summary>
public Payload AnchorPayload { get; set; }
}
} }

View File

@@ -1,15 +1,8 @@
using System; namespace Pilz.Dalamud.Tools.Strings;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.Tools.Strings public enum StringPosition
{ {
public enum StringPosition Before,
{ After,
Before, Replace
After,
Replace
}
} }

View File

@@ -1,132 +1,125 @@
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
using Lumina.Text;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pilz.Dalamud.Tools.Strings namespace Pilz.Dalamud.Tools.Strings;
public static class StringUpdateFactory
{ {
public static class StringUpdateFactory public static void ApplyStringChanges(StringChangesProps props)
{ {
public static void ApplyStringChanges(StringChangesProps props) if (props.StringChanges != null && props.StringChanges.Any())
{ {
if (props.StringChanges != null && props.StringChanges.Any()) var seString = props.Destination;
List<StringPosition> stringPositionsOrdered = GetOrderedStringPositions(props);
foreach (var stringPosition in stringPositionsOrdered)
{ {
var seString = props.Destination; var stringChange = props.StringChanges.GetChange(stringPosition);
List<StringPosition> stringPositionsOrdered = GetOrderedStringPositions(props); if (stringChange != null && stringChange.Payloads.Any())
foreach (var stringPosition in stringPositionsOrdered)
{ {
var stringChange = props.StringChanges.GetChange(stringPosition); AddSpacesBetweenTextPayloads(stringChange.Payloads, stringPosition);
if (stringChange != null && stringChange.Payloads.Any())
if (stringPosition == StringPosition.Before)
{ {
AddSpacesBetweenTextPayloads(stringChange.Payloads, stringPosition); Payload anchorFirst = stringChange.ForceUsingSingleAnchorPayload ? props.AnchorPayload : props.AnchorPayloads?.FirstOrDefault();
if (stringPosition == StringPosition.Before) if (anchorFirst != null)
{ {
Payload anchorFirst = stringChange.ForceUsingSingleAnchorPayload ? props.AnchorPayload : props.AnchorPayloads?.FirstOrDefault(); var anchorPayloadIndex = seString.Payloads.IndexOf(anchorFirst);
seString.Payloads.InsertRange(anchorPayloadIndex, stringChange.Payloads);
if (anchorFirst != null)
{
var anchorPayloadIndex = seString.Payloads.IndexOf(anchorFirst);
seString.Payloads.InsertRange(anchorPayloadIndex, stringChange.Payloads);
}
else
seString.Payloads.InsertRange(0, stringChange.Payloads);
} }
else if (stringPosition == StringPosition.After) else
{ seString.Payloads.InsertRange(0, stringChange.Payloads);
Payload anchorLast = stringChange.ForceUsingSingleAnchorPayload ? props.AnchorPayload : props.AnchorPayloads?.LastOrDefault(); }
else if (stringPosition == StringPosition.After)
{
Payload anchorLast = stringChange.ForceUsingSingleAnchorPayload ? props.AnchorPayload : props.AnchorPayloads?.LastOrDefault();
if (anchorLast != null) if (anchorLast != null)
{ {
var anchorPayloadIndex = seString.Payloads.IndexOf(anchorLast); var anchorPayloadIndex = seString.Payloads.IndexOf(anchorLast);
seString.Payloads.InsertRange(anchorPayloadIndex + 1, stringChange.Payloads); seString.Payloads.InsertRange(anchorPayloadIndex + 1, stringChange.Payloads);
}
else
seString.Payloads.AddRange(stringChange.Payloads);
} }
else if (stringPosition == StringPosition.Replace) else
{ seString.Payloads.AddRange(stringChange.Payloads);
Payload anchorReplace = props.AnchorPayload; }
else if (stringPosition == StringPosition.Replace)
{
Payload anchorReplace = props.AnchorPayload;
if (anchorReplace != null) if (anchorReplace != null)
{ {
var anchorPayloadIndex = seString.Payloads.IndexOf(anchorReplace); var anchorPayloadIndex = seString.Payloads.IndexOf(anchorReplace);
seString.Payloads.InsertRange(anchorPayloadIndex, stringChange.Payloads); seString.Payloads.InsertRange(anchorPayloadIndex, stringChange.Payloads);
seString.Remove(anchorReplace); seString.Remove(anchorReplace);
} }
else else
{ {
seString.Payloads.Clear(); seString.Payloads.Clear();
seString.Payloads.AddRange(stringChange.Payloads); seString.Payloads.AddRange(stringChange.Payloads);
}
} }
} }
} }
} }
} }
}
private static void AddSpacesBetweenTextPayloads(List<Payload> payloads, StringPosition tagPosition)
{ private static void AddSpacesBetweenTextPayloads(List<Payload> payloads, StringPosition tagPosition)
if (payloads != null && payloads.Any()) {
{ if (payloads != null && payloads.Any())
var indicesToInsertSpacesAt = new List<int>(); {
var lastTextPayloadIndex = -1; var indicesToInsertSpacesAt = new List<int>();
var lastTextPayloadIndex = -1;
static TextPayload getNewTextPayload() => new(" ");
static TextPayload getNewTextPayload() => new(" ");
foreach (var payload in payloads.Reverse<Payload>())
{ foreach (var payload in payloads.Reverse<Payload>())
if (payload is IconPayload iconPayload) {
lastTextPayloadIndex = -1; if (payload is IconPayload iconPayload)
else if (payload is TextPayload textPayload) lastTextPayloadIndex = -1;
{ else if (payload is TextPayload textPayload)
if (lastTextPayloadIndex != -1) {
indicesToInsertSpacesAt.Add(payloads.IndexOf(textPayload) + 1); if (lastTextPayloadIndex != -1)
lastTextPayloadIndex = payloads.IndexOf(textPayload); indicesToInsertSpacesAt.Add(payloads.IndexOf(textPayload) + 1);
} lastTextPayloadIndex = payloads.IndexOf(textPayload);
} }
}
foreach (var indexToInsertSpaceAt in indicesToInsertSpacesAt)
payloads.Insert(indexToInsertSpaceAt, getNewTextPayload()); foreach (var indexToInsertSpaceAt in indicesToInsertSpacesAt)
payloads.Insert(indexToInsertSpaceAt, getNewTextPayload());
// Decide whether to add a space to the end
if (tagPosition == StringPosition.Before) // Decide whether to add a space to the end
{ if (tagPosition == StringPosition.Before)
var significantPayloads = payloads.Where(payload => payload is TextPayload || payload is IconPayload); {
if (significantPayloads.LastOrDefault() is TextPayload) var significantPayloads = payloads.Where(payload => payload is TextPayload || payload is IconPayload);
payloads.Add(getNewTextPayload()); if (significantPayloads.LastOrDefault() is TextPayload)
} payloads.Add(getNewTextPayload());
// Decide whether to add a space to the beginning }
else if (tagPosition == StringPosition.After) // Decide whether to add a space to the beginning
{ else if (tagPosition == StringPosition.After)
var significantPayloads = payloads.Where(payload => payload is TextPayload || payload is IconPayload); {
if (significantPayloads.FirstOrDefault() is TextPayload) var significantPayloads = payloads.Where(payload => payload is TextPayload || payload is IconPayload);
payloads.Insert(0, getNewTextPayload()); if (significantPayloads.FirstOrDefault() is TextPayload)
} payloads.Insert(0, getNewTextPayload());
} }
} }
}
private static List<StringPosition> GetOrderedStringPositions(StringChangesProps props)
{ private static List<StringPosition> GetOrderedStringPositions(StringChangesProps props)
var tagPositionsOrdered = new List<StringPosition>(); {
var tagPositionsOrdered = new List<StringPosition>();
// If there's no anchor payload, do replaces first so that befores and afters are based on the replaced data
if (props.AnchorPayloads == null || !props.AnchorPayloads.Any()) // If there's no anchor payload, do replaces first so that befores and afters are based on the replaced data
tagPositionsOrdered.Add(StringPosition.Replace); if (props.AnchorPayloads == null || !props.AnchorPayloads.Any())
tagPositionsOrdered.Add(StringPosition.Replace);
tagPositionsOrdered.Add(StringPosition.Before);
tagPositionsOrdered.Add(StringPosition.After); tagPositionsOrdered.Add(StringPosition.Before);
tagPositionsOrdered.Add(StringPosition.After);
// If there is an anchor payload, do replaces last so that we still know which payload needs to be removed
if (props.AnchorPayloads != null && props.AnchorPayloads.Any()) // If there is an anchor payload, do replaces last so that we still know which payload needs to be removed
tagPositionsOrdered.Add(StringPosition.Replace); if (props.AnchorPayloads != null && props.AnchorPayloads.Any())
tagPositionsOrdered.Add(StringPosition.Replace);
return tagPositionsOrdered;
} return tagPositionsOrdered;
} }
} }

View File

@@ -1,39 +0,0 @@
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Pilz.Dalamud.Nameplates.Model;
namespace Pilz.Dalamud
{
public class XivApi
{
private static IntPtr _RaptureAtkModulePtr = IntPtr.Zero;
public static IntPtr RaptureAtkModulePtr
{
get
{
if (_RaptureAtkModulePtr == IntPtr.Zero)
{
unsafe
{
var framework = Framework.Instance();
var uiModule = framework->GetUiModule();
_RaptureAtkModulePtr = new IntPtr(uiModule->GetRaptureAtkModule());
}
}
return _RaptureAtkModulePtr;
}
}
public static SafeAddonNameplate GetSafeAddonNamePlate()
{
return new(PluginServices.PluginInterface);
}
}
}

View File

@@ -1,6 +1,6 @@
{ {
"version": 1, "version": 1,
"dependencies": { "dependencies": {
"net7.0-windows7.0": {} "net8.0-windows7.0": {}
} }
} }

View File

@@ -0,0 +1,14 @@
@echo off
set sourceP="https://git.pilzinsel64.de/api/v4/projects/25/packages/nuget/index.json"
set apikeyP=%GitLab_git_pilzinsel64_de%
set sourceN="https://api.nuget.org/v3/index.json"
set apikeyN=%NuGet_ApiKey%
for %%x in (%*) do (
dotnet nuget push "%%x" --source "%sourceP%" --api-key "%apikeyP%" --skip-duplicate
dotnet nuget push "%%x" --source "%sourceN%" --api-key "%apikeyN%" --skip-duplicate
)
pause

View File

@@ -8,6 +8,9 @@ At the moment it's far away from being complete or even good. Right now it as so
Install the latest version of `Pilz.Dalamud` via NuGet Package Manager or NuGet Console:\ Install the latest version of `Pilz.Dalamud` via NuGet Package Manager or NuGet Console:\
https://www.nuget.org/packages/Pilz.Dalamud https://www.nuget.org/packages/Pilz.Dalamud
Or add the alternative package source for slightly quicker updates:\
`<add key="Pilz.Dalamud" value="https://git.pilzinsel64.de/api/v4/projects/8/packages/nuget/index.json" />`\
## Get started ## Get started
### Initialize Plugin Services ### Initialize Plugin Services
@@ -25,78 +28,16 @@ public Plugin(DalamudPluginInterface pluginInterface)
} }
``` ```
### Hook into Nameplates ### Hook into NamePlates
To edit the nameplate, you first need to hook and listen to the Game's updates. Also don't forget to unhook and dispose on unloading the plugins! Nameplates has been reworked by @nebel and will be part of core Dalamud soon.
Read more at: https://github.com/goatcorp/Dalamud/pull/1915
Use the new service already now via Pilz.Dalamud:
```cs ```cs
public class NameplateFeature : IDisposable var namePlateGui = Pilz.Dalamud.NamePlate.INamePlateGui.Instance;
{
public NameplateManager NameplateManager { get; init; }
/// <summary>
/// Occurs when a player nameplate is updated by the game.
/// </summary>
public event PlayerNameplateUpdatedDelegate? PlayerNameplateUpdated;
public NameplateFeature()
{
NameplateManager = new();
NameplateManager.Hooks.AddonNamePlate_SetPlayerNameManaged += Hooks_AddonNamePlate_SetPlayerNameManaged;
}
public void Dispose()
{
NameplateManager.Hooks.AddonNamePlate_SetPlayerNameManaged -= Hooks_AddonNamePlate_SetPlayerNameManaged;
NameplateManager.Dispose();
}
private void Hooks_AddonNamePlate_SetPlayerNameManaged(Pilz.Dalamud.Nameplates.EventArgs.AddonNamePlate_SetPlayerNameManagedEventArgs eventArgs)
{
}
}
```
This is an example of editing the title to "Good Player", make the name italic and also force the title to always be above the name:
```cs
private void Hooks_AddonNamePlate_SetPlayerNameManaged(Pilz.Dalamud.Nameplates.EventArgs.AddonNamePlate_SetPlayerNameManagedEventArgs eventArgs)
{
try
{
// Get the referenced player object for the nameplate object
PlayerCharacter? playerCharacter = NameplateManager.GetNameplateGameObject<PlayerCharacter>(eventArgs.SafeNameplateObject);
if (playerCharacter != null && playerCharacter.StatusFlags.HasFlag(StatusFlags.Friend))
{
const string TEXT_GOOD_PLAYER = "Good Player";
// Create a new change
var nameplateChanges = new NameplateChanges(eventArgs);
// Replace the title
var titleChange = nameplateChanges.GetChange(NameplateElements.Title, StringPosition.Replace);
titleChange.Payloads.Add(new TextPayload(TEXT_GOOD_PLAYER));
// Make the name italic
var nameChangeBefore = nameplateChanges.GetChange(NameplateElements.Name, StringPosition.Before);
nameChangeBefore.Payloads.Add(new EmphasisItalicPayload(true));
var nameChangeAfter = nameplateChanges.GetChange(NameplateElements.Name, StringPosition.After);
nameChangeAfter.Payloads.Add(new EmphasisItalicPayload(false));
// Forge the title to be always above the name (this we can edit directly)
eventArgs.IsTitleAboveName = true;
// Apply the string changes!
NameplateUpdateFactory.ApplyNameplateChanges(new NameplateChangesProps(nameplateChanges));
}
}
catch (Exception ex)
{
PluginLog.Error(ex, $"SetPlayerNameplateDetour");
}
}
``` ```
## Contribute ## Contribute