├── .gitignore ├── PartyIcons ├── Resources │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ └── icon.png ├── Configuration │ ├── ChatMode.cs │ ├── IconSetId.cs │ ├── NameplateSizeMode.cs │ ├── NameplateMode.cs │ ├── ChatConfig.cs │ ├── SettingsV1.cs │ └── Settings.cs ├── DalamudPackager.targets ├── PartyIcons.json ├── packages.lock.json ├── Entities │ ├── RoleId.cs │ ├── GenericRole.cs │ └── Job.cs ├── Service.cs ├── UI │ ├── Controls │ │ └── FlashingText.cs │ ├── ChatNameSettings.cs │ ├── GeneralSettings.cs │ ├── SettingsWindow.cs │ ├── StaticAssignmentsSettings.cs │ └── NameplateSettings.cs ├── Utils │ ├── SeStringUtils.cs │ ├── WindowSizeHelper.cs │ └── PartyListHUDView.cs ├── CommandHandler.cs ├── Runtime │ ├── NPCNameplateFixer.cs │ ├── PartyListHUDUpdater.cs │ ├── ViewModeSetter.cs │ ├── ChatNameUpdater.cs │ ├── NameplateUpdater.cs │ └── RoleTracker.cs ├── Plugin.cs ├── PartyIcons.csproj ├── Api │ ├── PluginAddressResolver.cs │ └── XivApi.cs ├── View │ ├── IconSet.cs │ ├── PlayerContextMenu.cs │ └── NameplateView.cs └── Stylesheet │ └── PlayerStylesheet.cs ├── .gitmodules ├── README.md └── PartyIcons.sln /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | obj/ 3 | **/bin/ 4 | *.user 5 | .idea/ 6 | -------------------------------------------------------------------------------- /PartyIcons/Resources/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shdwp/xivPartyIcons/HEAD/PartyIcons/Resources/1.png -------------------------------------------------------------------------------- /PartyIcons/Resources/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shdwp/xivPartyIcons/HEAD/PartyIcons/Resources/2.png -------------------------------------------------------------------------------- /PartyIcons/Resources/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shdwp/xivPartyIcons/HEAD/PartyIcons/Resources/3.png -------------------------------------------------------------------------------- /PartyIcons/Resources/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shdwp/xivPartyIcons/HEAD/PartyIcons/Resources/4.png -------------------------------------------------------------------------------- /PartyIcons/Resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shdwp/xivPartyIcons/HEAD/PartyIcons/Resources/icon.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Dalamud.ContextMenu"] 2 | path = Dalamud.ContextMenu 3 | url = https://github.com/squidmade/Dalamud.ContextMenu 4 | branch = net7 5 | -------------------------------------------------------------------------------- /PartyIcons/Configuration/ChatMode.cs: -------------------------------------------------------------------------------- 1 | namespace PartyIcons.Configuration; 2 | 3 | public enum ChatMode 4 | { 5 | GameDefault, 6 | Role, 7 | Job 8 | } 9 | -------------------------------------------------------------------------------- /PartyIcons/Configuration/IconSetId.cs: -------------------------------------------------------------------------------- 1 | namespace PartyIcons.Configuration; 2 | 3 | public enum IconSetId 4 | { 5 | GlowingColored, 6 | GlowingGold, 7 | Framed 8 | } 9 | -------------------------------------------------------------------------------- /PartyIcons/Configuration/NameplateSizeMode.cs: -------------------------------------------------------------------------------- 1 | namespace PartyIcons.Configuration; 2 | 3 | public enum NameplateSizeMode 4 | { 5 | Smaller, 6 | Medium, 7 | Bigger 8 | } 9 | -------------------------------------------------------------------------------- /PartyIcons/Configuration/NameplateMode.cs: -------------------------------------------------------------------------------- 1 | namespace PartyIcons.Configuration; 2 | 3 | public enum NameplateMode 4 | { 5 | Default, 6 | Hide, 7 | SmallJobIcon, 8 | SmallJobIconAndRole, 9 | BigJobIcon, 10 | BigJobIconAndPartySlot, 11 | RoleLetters 12 | } 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PartyIcons 2 | Plugin to display icons instead of names in parties. 3 | 4 | * Show small job icons and names 5 | * Show big job icons 6 | * Automatically detect and show raid roles 7 | * Show sprout, flower, etc icons in addition to the job icons 8 | 9 | Special thanks to PhaineOfCatz for making the PartyIcons icon. -------------------------------------------------------------------------------- /PartyIcons/Configuration/ChatConfig.cs: -------------------------------------------------------------------------------- 1 | namespace PartyIcons.Configuration; 2 | 3 | public class ChatConfig 4 | { 5 | public ChatConfig(ChatMode mode, bool useRoleColor = true) 6 | { 7 | Mode = mode; 8 | UseRoleColor = useRoleColor; 9 | } 10 | 11 | public ChatMode Mode { get; set; } 12 | 13 | public bool UseRoleColor { get; set; } 14 | } -------------------------------------------------------------------------------- /PartyIcons/DalamudPackager.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /PartyIcons/PartyIcons.json: -------------------------------------------------------------------------------- 1 | { 2 | "Author": "shdwp, Avaflow, abashby", 3 | "Name": "Party Icons", 4 | "Punchline": "Replace names with job icons, raid role positions and more!", 5 | "Description": "Adjusts player nameplates base on current content. For example you can display raid positions (that are assigned automatically by listening to the chat), job icons, or names with job icons.\n\nReverse-engineering effort achieved by authors of JobIcons: haplo, daemitus, Loskh, wozaiha, without them this plugin would not be possible!", 6 | "InternalName": "PartyIcons", 7 | "RepoUrl": "https://github.com/shdwp/xivPartyIcons", 8 | "Tags": [ 9 | "UI" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /PartyIcons/packages.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": { 4 | "net7.0-windows7.0": { 5 | "DalamudPackager": { 6 | "type": "Direct", 7 | "requested": "[2.1.10, )", 8 | "resolved": "2.1.10", 9 | "contentHash": "S6NrvvOnLgT4GDdgwuKVJjbFo+8ZEj+JsEYk9ojjOR/MMfv1dIFpT8aRJQfI24rtDcw1uF+GnSSMN4WW1yt7fw==" 10 | }, 11 | "XivCommon": { 12 | "type": "Direct", 13 | "requested": "[7.0.0-alpha.1, )", 14 | "resolved": "7.0.0-alpha.1", 15 | "contentHash": "4Yy4Wg3bnI/g9BSEYAqOZALmVMJZS0wcP847VlUIThBqIS/47O6tw2BI84he4KuPSTfIsYOzrcz3vAia8+Pn3g==" 16 | }, 17 | "dalamud.contextmenu": { 18 | "type": "Project" 19 | } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /PartyIcons/Entities/RoleId.cs: -------------------------------------------------------------------------------- 1 | namespace PartyIcons.Entities; 2 | 3 | public enum RoleId 4 | { 5 | Undefined, 6 | MT = 1, 7 | OT, 8 | M1, 9 | M2, 10 | R1, 11 | R2, 12 | H1, 13 | H2 14 | } 15 | 16 | public static class RoleIdUtils 17 | { 18 | public static RoleId Counterpart(RoleId roleId) 19 | { 20 | return roleId switch 21 | { 22 | RoleId.MT => RoleId.OT, 23 | RoleId.OT => RoleId.MT, 24 | RoleId.H1 => RoleId.H2, 25 | RoleId.H2 => RoleId.H1, 26 | RoleId.M1 => RoleId.M2, 27 | RoleId.M2 => RoleId.M1, 28 | RoleId.R1 => RoleId.R2, 29 | RoleId.R2 => RoleId.R1, 30 | _ => RoleId.Undefined 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /PartyIcons/Entities/GenericRole.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PartyIcons.Entities; 4 | 5 | public enum GenericRole : uint 6 | { 7 | Tank = 0, 8 | Melee = 1, 9 | Ranged = 2, 10 | Healer = 3, 11 | Crafter = 4, 12 | Gatherer = 5 13 | } 14 | 15 | public static class JobRoleExtensions 16 | { 17 | public static Job[] GetJobs(this GenericRole role) 18 | { 19 | return role switch 20 | { 21 | GenericRole.Tank => new[] {Job.GLA, Job.MRD, Job.PLD, Job.WAR, Job.DRK, Job.GNB}, 22 | GenericRole.Healer => new[] {Job.CNJ, Job.AST, Job.WHM, Job.SCH}, 23 | GenericRole.Melee => new[] {Job.PGL, Job.LNC, Job.MNK, Job.DRG, Job.ROG, Job.NIN, Job.SAM}, 24 | GenericRole.Ranged => new[] 25 | {Job.ARC, Job.BRD, Job.MCH, Job.DNC, Job.THM, Job.BLM, Job.ACN, Job.SMN, Job.RDM, Job.BLU}, 26 | GenericRole.Crafter => new[] {Job.CRP, Job.BSM, Job.ARM, Job.GSM, Job.LTW, Job.WVR, Job.ALC, Job.CUL}, 27 | GenericRole.Gatherer => new[] {Job.MIN, Job.BTN, Job.FSH}, 28 | _ => throw new ArgumentException($"Unknown jobRoleID {(int) role}") 29 | }; 30 | } 31 | 32 | public static GenericRole RoleFromByte(byte roleId) => (GenericRole) (roleId - 1); 33 | } 34 | -------------------------------------------------------------------------------- /PartyIcons.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29709.97 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PartyIcons", "PartyIcons\PartyIcons.csproj", "{13C812E9-0D42-4B95-8646-40EEBF30636F}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.ContextMenu", "Dalamud.ContextMenu\src\Dalamud.ContextMenu\Dalamud.ContextMenu.csproj", "{1010CD03-5986-4D78-A7A8-659FC8B80255}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|x64 = Debug|x64 13 | Release|x64 = Release|x64 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64 17 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64 18 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64 19 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64 20 | {1010CD03-5986-4D78-A7A8-659FC8B80255}.Debug|x64.ActiveCfg = Debug|x64 21 | {1010CD03-5986-4D78-A7A8-659FC8B80255}.Debug|x64.Build.0 = Debug|x64 22 | {1010CD03-5986-4D78-A7A8-659FC8B80255}.Release|x64.ActiveCfg = Release|x64 23 | {1010CD03-5986-4D78-A7A8-659FC8B80255}.Release|x64.Build.0 = Release|x64 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {B17E85B1-5F60-4440-9F9A-3DDE877E8CDF} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /PartyIcons/Service.cs: -------------------------------------------------------------------------------- 1 | // using Dalamud.ContextMenu; 2 | using Dalamud.Data; 3 | using Dalamud.Game; 4 | using Dalamud.Game.ClientState; 5 | using Dalamud.Game.ClientState.Conditions; 6 | using Dalamud.Game.ClientState.Objects; 7 | using Dalamud.Game.ClientState.Party; 8 | using Dalamud.Game.Command; 9 | using Dalamud.Game.Gui; 10 | using Dalamud.Game.Gui.Toast; 11 | using Dalamud.Game.Network; 12 | using Dalamud.IoC; 13 | using Dalamud.Plugin; 14 | using PartyIcons.Stylesheet; 15 | 16 | namespace PartyIcons; 17 | 18 | internal class Service 19 | { 20 | [PluginService] public static DalamudPluginInterface PluginInterface { get; private set; } = null!; 21 | [PluginService] public static ChatGui ChatGui { get; private set; } = null!; 22 | [PluginService] public static ClientState ClientState { get; private set; } = null!; 23 | [PluginService] public static CommandManager CommandManager { get; private set; } = null!; 24 | [PluginService] public static Condition Condition { get; private set; } = null!; 25 | [PluginService] public static DataManager DataManager { get; private set; } = null!; 26 | [PluginService] public static Framework Framework { get; private set; } = null!; 27 | [PluginService] public static ObjectTable ObjectTable { get; private set; } = null!; 28 | [PluginService] public static GameGui GameGui { get; private set; } = null!; 29 | [PluginService] public static PartyList PartyList { get; private set; } = null!; 30 | [PluginService] public static SigScanner SigScanner { get; private set; } = null!; 31 | [PluginService] public static GameNetwork GameNetwork { get; private set; } = null!; 32 | [PluginService] public static ToastGui ToastGui { get; private set; } = null!; 33 | } 34 | -------------------------------------------------------------------------------- /PartyIcons/UI/Controls/FlashingText.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Numerics; 4 | using ImGuiNET; 5 | using PartyIcons.Configuration; 6 | 7 | namespace PartyIcons.UI.Controls; 8 | 9 | public class FlashingText 10 | { 11 | public FlashingText() 12 | { 13 | _flashColor0 = new Vector4(0.4f, 0.1f, 0.1f, 1.0f); 14 | _flashColor1 = new Vector4(1.0f, 0.0f, 0.0f, 1.0f); 15 | } 16 | 17 | public bool IsFlashing 18 | { 19 | get => _isFlashing; 20 | set 21 | { 22 | if (_isFlashing != value) 23 | { 24 | _isFlashing = value; 25 | 26 | if (value) 27 | { 28 | _stopwatch.Start(); 29 | } 30 | else 31 | { 32 | _stopwatch.Stop(); 33 | } 34 | } 35 | } 36 | } 37 | 38 | public void Draw(Action draw) 39 | { 40 | _ = Draw(() => 41 | { 42 | draw.Invoke(); 43 | return true; 44 | }); 45 | } 46 | 47 | public bool Draw(Func draw) 48 | { 49 | Vector4 flashColor = _flashColor0; 50 | 51 | if (IsFlashing) 52 | { 53 | if (_stopwatch.ElapsedMilliseconds < FlashIntervalMs) 54 | { 55 | flashColor = _flashColor1; 56 | } 57 | 58 | if (_stopwatch.ElapsedMilliseconds > FlashIntervalMs * 2) 59 | { 60 | _stopwatch.Restart(); 61 | } 62 | } 63 | 64 | if (IsFlashing) 65 | { 66 | ImGui.PushStyleColor(0, flashColor); 67 | } 68 | 69 | bool result = draw.Invoke();//ImGui.Text(text); 70 | 71 | if (IsFlashing) 72 | { 73 | ImGui.PopStyleColor(); 74 | } 75 | 76 | return result; 77 | } 78 | 79 | private readonly Vector4 _flashColor0; 80 | private readonly Vector4 _flashColor1; 81 | 82 | private static readonly Stopwatch _stopwatch = new(); 83 | private const int FlashIntervalMs = 500; 84 | private bool _isFlashing; 85 | } -------------------------------------------------------------------------------- /PartyIcons/Utils/SeStringUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.InteropServices; 4 | using Dalamud.Game.Text.SeStringHandling; 5 | using Dalamud.Game.Text.SeStringHandling.Payloads; 6 | 7 | namespace PartyIcons.Utils; 8 | 9 | public static class SeStringUtils 10 | { 11 | public static IntPtr emptyPtr; 12 | 13 | public static void Initialize() 14 | { 15 | emptyPtr = SeStringToPtr(Text("")); 16 | } 17 | 18 | public static void Dispose() { } 19 | 20 | public static SeString SeStringFromPtr(IntPtr seStringPtr) 21 | { 22 | byte b; 23 | var offset = 0; 24 | 25 | unsafe 26 | { 27 | while ((b = *(byte*) (seStringPtr + offset)) != 0) 28 | { 29 | offset++; 30 | } 31 | } 32 | 33 | var bytes = new byte[offset]; 34 | Marshal.Copy(seStringPtr, bytes, 0, offset); 35 | 36 | return SeString.Parse(bytes); 37 | } 38 | 39 | public static IntPtr SeStringToPtr(SeString seString) 40 | { 41 | var bytes = seString.Encode(); 42 | var pointer = Marshal.AllocHGlobal(bytes.Length + 1); 43 | Marshal.Copy(bytes, 0, pointer, bytes.Length); 44 | Marshal.WriteByte(pointer, bytes.Length, 0); 45 | 46 | return pointer; 47 | } 48 | 49 | public static void FreePtr(IntPtr seStringPtr) 50 | { 51 | if (seStringPtr != emptyPtr) 52 | { 53 | Marshal.FreeHGlobal(seStringPtr); 54 | } 55 | } 56 | 57 | public static SeString Text(string rawText) 58 | { 59 | var seString = new SeString(new List()); 60 | seString.Append(new TextPayload(rawText)); 61 | 62 | return seString; 63 | } 64 | 65 | public static SeString Text(string text, ushort color) 66 | { 67 | var seString = new SeString(new List()); 68 | seString.Append(new UIForegroundPayload(color)); 69 | seString.Append(new TextPayload(text)); 70 | seString.Append(UIForegroundPayload.UIForegroundOff); 71 | 72 | return seString; 73 | } 74 | 75 | public static SeString Icon(BitmapFontIcon icon, string prefix = null) 76 | { 77 | var seString = new SeString(new List()); 78 | 79 | if (prefix != null) 80 | { 81 | seString.Append(new TextPayload(prefix)); 82 | } 83 | 84 | seString.Append(new IconPayload(icon)); 85 | 86 | return seString; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /PartyIcons/CommandHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Dalamud.Game.Command; 3 | using Dalamud.Logging; 4 | 5 | namespace PartyIcons; 6 | 7 | public class CommandHandler : IDisposable 8 | { 9 | private const string commandName = "/ppi"; 10 | 11 | public CommandHandler() 12 | { 13 | Service.CommandManager.AddHandler(commandName, new CommandInfo(OnCommand) 14 | { 15 | HelpMessage = 16 | "opens configuration window; \"reset\" or \"r\" resets all assignments; \"debug\" prints debugging info" 17 | }); 18 | } 19 | 20 | public void Dispose() 21 | { 22 | Service.CommandManager.RemoveHandler(commandName); 23 | } 24 | 25 | private void OnCommand(string command, string arguments) 26 | { 27 | arguments = arguments.Trim().ToLower(); 28 | 29 | if (arguments == "" || arguments == "config") 30 | { 31 | Plugin.SettingsWindow.ToggleSettingsWindow(); 32 | } 33 | else if (arguments == "reset" || arguments == "r") 34 | { 35 | Plugin.RoleTracker.ResetOccupations(); 36 | Plugin.RoleTracker.ResetAssignments(); 37 | Plugin.RoleTracker.CalculateUnassignedPartyRoles(); 38 | Service.ChatGui.Print("Occupations are reset, roles are auto assigned."); 39 | } 40 | else if (arguments == "dbg state") 41 | { 42 | Service.ChatGui.Print($"Current mode is {Plugin.NameplateView.PartyMode}, party count {Service.PartyList.Length}"); 43 | Service.ChatGui.Print(Plugin.RoleTracker.DebugDescription()); 44 | } 45 | else if (arguments == "dbg party") 46 | { 47 | Service.ChatGui.Print(Plugin.PartyHudView.GetDebugInfo()); 48 | } 49 | else if (arguments.Contains("set")) 50 | { 51 | var argv = arguments.Split(' '); 52 | 53 | if (argv.Length == 2) 54 | { 55 | try 56 | { 57 | Plugin.NameplateUpdater.DebugIcon = int.Parse(argv[1]); 58 | PluginLog.Verbose($"Set debug icon to {Plugin.NameplateUpdater.DebugIcon}"); 59 | } 60 | catch (Exception) 61 | { 62 | PluginLog.Verbose("Invalid icon id given for debug icon."); 63 | Plugin.NameplateUpdater.DebugIcon = -1; 64 | } 65 | } 66 | else 67 | { 68 | Plugin.NameplateUpdater.DebugIcon = -1; 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /PartyIcons/Configuration/SettingsV1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using PartyIcons.Entities; 4 | 5 | namespace PartyIcons.Configuration; 6 | 7 | public class SettingsV1 8 | { 9 | public enum ChatModeV1 10 | { 11 | GameDefault, 12 | OnlyColor, 13 | Role, 14 | Job 15 | } 16 | 17 | public static ChatConfig ToChatConfig(ChatModeV1 chatModeV1) 18 | { 19 | switch (chatModeV1) 20 | { 21 | case ChatModeV1.GameDefault: 22 | return new ChatConfig(ChatMode.GameDefault, useRoleColor: false); 23 | 24 | case ChatModeV1.OnlyColor: 25 | return new ChatConfig(ChatMode.GameDefault, useRoleColor: true); 26 | 27 | case ChatModeV1.Role: 28 | return new ChatConfig(ChatMode.Role, useRoleColor: true); 29 | 30 | case ChatModeV1.Job: 31 | return new ChatConfig(ChatMode.Job, useRoleColor: true); 32 | 33 | default: 34 | throw new ArgumentOutOfRangeException(nameof(chatModeV1), chatModeV1, null); 35 | } 36 | } 37 | 38 | public int Version { get; set; } = 1; 39 | 40 | public bool ChatContentMessage = true; 41 | public bool HideLocalPlayerNameplate = false; 42 | public bool TestingMode = true; 43 | public bool EasternNamingConvention = false; 44 | public bool DisplayRoleInPartyList = false; 45 | public bool UseContextMenu = false; 46 | public bool AssignFromChat = true; 47 | public bool UsePriorityIcons = true; 48 | 49 | public IconSetId IconSetId { get; set; } = IconSetId.GlowingColored; 50 | public NameplateSizeMode SizeMode { get; set; } = NameplateSizeMode.Medium; 51 | 52 | public NameplateMode NameplateOverworld { get; set; } = NameplateMode.SmallJobIcon; 53 | public NameplateMode NameplateAllianceRaid { get; set; } = NameplateMode.BigJobIconAndPartySlot; 54 | public NameplateMode NameplateDungeon { get; set; } = NameplateMode.BigJobIconAndPartySlot; 55 | public NameplateMode NameplateBozjaParty { get; set; } = NameplateMode.BigJobIconAndPartySlot; 56 | public NameplateMode NameplateBozjaOthers { get; set; } = NameplateMode.Default; 57 | public NameplateMode NameplateRaid { get; set; } = NameplateMode.RoleLetters; 58 | public NameplateMode NameplateOthers { get; set; } = NameplateMode.SmallJobIcon; 59 | 60 | public ChatModeV1 ChatOverworld { get; set; } = ChatModeV1.Role; 61 | public ChatModeV1 ChatAllianceRaid { get; set; } = ChatModeV1.Role; 62 | public ChatModeV1 ChatDungeon { get; set; } = ChatModeV1.Job; 63 | public ChatModeV1 ChatRaid { get; set; } = ChatModeV1.Role; 64 | public ChatModeV1 ChatOthers { get; set; } = ChatModeV1.Job; 65 | 66 | public Dictionary StaticAssignments { get; set; } = new(); 67 | } -------------------------------------------------------------------------------- /PartyIcons/Entities/Job.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PartyIcons.Entities; 4 | 5 | public enum Job : uint 6 | { 7 | ADV = 0, 8 | GLA = 1, 9 | PGL = 2, 10 | MRD = 3, 11 | LNC = 4, 12 | ARC = 5, 13 | CNJ = 6, 14 | THM = 7, 15 | CRP = 8, 16 | BSM = 9, 17 | ARM = 10, 18 | GSM = 11, 19 | LTW = 12, 20 | WVR = 13, 21 | ALC = 14, 22 | CUL = 15, 23 | MIN = 16, 24 | BTN = 17, 25 | FSH = 18, 26 | PLD = 19, 27 | MNK = 20, 28 | WAR = 21, 29 | DRG = 22, 30 | BRD = 23, 31 | WHM = 24, 32 | BLM = 25, 33 | ACN = 26, 34 | SMN = 27, 35 | SCH = 28, 36 | ROG = 29, 37 | NIN = 30, 38 | MCH = 31, 39 | DRK = 32, 40 | AST = 33, 41 | SAM = 34, 42 | RDM = 35, 43 | BLU = 36, 44 | GNB = 37, 45 | DNC = 38, 46 | RPR = 39, 47 | SGE = 40 48 | } 49 | 50 | public static class JobExtensions 51 | { 52 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0066:Convert switch statement to expression", 53 | Justification = "No, it looks dumb")] 54 | public static GenericRole GetRole(this Job job) 55 | { 56 | switch (job) 57 | { 58 | case Job.GLA: 59 | case Job.MRD: 60 | case Job.PLD: 61 | case Job.WAR: 62 | case Job.DRK: 63 | case Job.GNB: 64 | return GenericRole.Tank; 65 | 66 | case Job.CNJ: 67 | case Job.AST: 68 | case Job.WHM: 69 | case Job.SCH: 70 | case Job.SGE: 71 | return GenericRole.Healer; 72 | 73 | case Job.PGL: 74 | case Job.LNC: 75 | case Job.MNK: 76 | case Job.DRG: 77 | case Job.ROG: 78 | case Job.NIN: 79 | case Job.SAM: 80 | case Job.RPR: 81 | return GenericRole.Melee; 82 | 83 | case Job.ARC: 84 | case Job.BRD: 85 | case Job.MCH: 86 | case Job.DNC: 87 | case Job.THM: 88 | case Job.BLM: 89 | case Job.ACN: 90 | case Job.SMN: 91 | case Job.RDM: 92 | case Job.BLU: 93 | return GenericRole.Ranged; 94 | 95 | case Job.CRP: 96 | case Job.BSM: 97 | case Job.ARM: 98 | case Job.GSM: 99 | case Job.LTW: 100 | case Job.WVR: 101 | case Job.ALC: 102 | case Job.CUL: 103 | return GenericRole.Crafter; 104 | 105 | case Job.MIN: 106 | case Job.BTN: 107 | case Job.FSH: 108 | return GenericRole.Gatherer; 109 | 110 | default: throw new ArgumentException($"Unknown jobID {(int) job}"); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /PartyIcons/Runtime/NPCNameplateFixer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Dalamud.Game; 5 | using Dalamud.Logging; 6 | using PartyIcons.Api; 7 | using PartyIcons.View; 8 | 9 | namespace PartyIcons.Runtime; 10 | 11 | /// 12 | /// Reverts NPC nameplates that have had their icon or name text scaled and 13 | /// also reverts all nameplates when the plugin is unloading. 14 | /// 15 | public sealed class NPCNameplateFixer : IDisposable 16 | { 17 | private const uint NoTarget = 0xE0000000; 18 | private readonly NameplateView _view; 19 | 20 | public NPCNameplateFixer(NameplateView view) 21 | { 22 | _view = view; 23 | } 24 | 25 | public void Enable() 26 | { 27 | Service.Framework.Update += OnUpdate; 28 | } 29 | 30 | public void Dispose() 31 | { 32 | Service.Framework.Update -= OnUpdate; 33 | RevertAll(); 34 | } 35 | 36 | private void OnUpdate(Framework framework) 37 | { 38 | RevertNPC(); 39 | } 40 | 41 | private void RevertNPC() 42 | { 43 | var addon = XivApi.GetSafeAddonNamePlate(); 44 | 45 | for (var i = 0; i < 50; i++) 46 | { 47 | var npObject = addon.GetNamePlateObject(i); 48 | 49 | if (npObject == null || !npObject.IsVisible) 50 | { 51 | continue; 52 | } 53 | 54 | var npInfo = npObject.NamePlateInfo; 55 | 56 | if (npInfo == null) 57 | { 58 | continue; 59 | } 60 | 61 | var actorID = npInfo.Data.ObjectID.ObjectID; 62 | 63 | if (actorID == NoTarget) 64 | { 65 | continue; 66 | } 67 | 68 | var isPC = npInfo.IsPlayerCharacter(); 69 | 70 | if (!isPC && _view.SetupDefault(npObject)) 71 | { 72 | PluginLog.Verbose($"Reverted NPC {actorID} (#{i})"); 73 | } 74 | } 75 | } 76 | 77 | private void RevertAll() 78 | { 79 | var addon = XivApi.GetSafeAddonNamePlate(); 80 | 81 | for (var i = 0; i < 50; i++) 82 | { 83 | var npObject = addon.GetNamePlateObject(i); 84 | 85 | if (npObject == null) 86 | { 87 | continue; 88 | } 89 | 90 | var npInfo = npObject.NamePlateInfo; 91 | 92 | if (npInfo == null) 93 | { 94 | continue; 95 | } 96 | 97 | var actorID = npInfo.Data.ObjectID.ObjectID; 98 | 99 | if (actorID == NoTarget) 100 | { 101 | continue; 102 | } 103 | 104 | if (_view.SetupDefault(npObject)) 105 | { 106 | PluginLog.Verbose($"Reverted {actorID} (#{i})"); 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /PartyIcons/Utils/WindowSizeHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Numerics; 2 | using Dalamud.Interface; 3 | using Dalamud.Logging; 4 | using ImGuiNET; 5 | 6 | namespace PartyIcons.Utils; 7 | 8 | public class WindowSizeHelper 9 | { 10 | /// 11 | /// Sets the next window's size and position. 12 | /// 13 | /// 14 | /// This additionally checks for whether an unreasonably large or offscreen window was detected. 15 | /// If so, the window size and position are forced to be the default instead of what was stored 16 | /// in the ImGui ini file. 17 | /// 18 | public void SetWindowSize() 19 | { 20 | var condition = ImGuiCond.FirstUseEver; 21 | 22 | if (_shouldForceSize) 23 | { 24 | condition = ImGuiCond.Always; 25 | 26 | _shouldForceSize = false; 27 | } 28 | 29 | SetDefaultWindowSizeAndPos(condition); 30 | } 31 | 32 | /// 33 | /// Check for an unreasonably large or offscreen window. 34 | /// 35 | public void CheckWindowSize() 36 | { 37 | if (_sizeAlreadyChecked) 38 | { 39 | return; 40 | } 41 | 42 | _sizeAlreadyChecked = true; 43 | 44 | Vector2 currentSize = ImGui.GetWindowSize(); 45 | Vector2 currentPos = ImGui.GetWindowPos(); 46 | 47 | var boundsToCheck = currentPos + currentSize; 48 | 49 | if (boundsToCheck.X > ImGuiHelpers.MainViewport.Size.X || 50 | boundsToCheck.Y > ImGuiHelpers.MainViewport.Size.Y) 51 | { 52 | PluginLog.LogDebug("Unreasonable window size detected as a result of a previously fixed bug. Forcing a reasonable window size."); 53 | 54 | _shouldForceSize = true; 55 | } 56 | } 57 | 58 | /// 59 | /// Force the next window to use the default size and position. 60 | /// 61 | public void ForceSize() 62 | { 63 | if (_shouldForceSize) 64 | { 65 | SetDefaultWindowSizeAndPos(ImGuiCond.Always); 66 | 67 | _shouldForceSize = false; 68 | } 69 | } 70 | 71 | private static void SetDefaultWindowSizeAndPos(ImGuiCond condition) 72 | { 73 | Vector2 initialSize = ImGuiHelpers.MainViewport.Size / 2f; 74 | Vector2 minimumSize = initialSize / 2f; 75 | Vector2 initialPosition = ImGuiHelpers.MainViewport.Size / 5f; 76 | 77 | ImGui.SetNextWindowSize(initialSize, condition); 78 | ImGui.SetNextWindowSizeConstraints(minimumSize, new Vector2(float.MaxValue)); 79 | 80 | ImGuiHelpers.SetNextWindowPosRelativeMainViewport(initialPosition, condition); 81 | } 82 | 83 | /// 84 | /// Whether constraints were already checked. 85 | /// 86 | private bool _sizeAlreadyChecked; 87 | 88 | /// 89 | /// Whether constraints should be checked next time the window is rendered. 90 | /// 91 | private bool _shouldForceSize; 92 | } -------------------------------------------------------------------------------- /PartyIcons/Plugin.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Plugin; 2 | using PartyIcons.Api; 3 | using PartyIcons.Configuration; 4 | using PartyIcons.Runtime; 5 | using PartyIcons.Stylesheet; 6 | using PartyIcons.UI; 7 | using PartyIcons.Utils; 8 | using PartyIcons.View; 9 | 10 | namespace PartyIcons; 11 | 12 | public sealed class Plugin : IDalamudPlugin 13 | { 14 | public string Name => "Party Icons"; 15 | 16 | public PluginAddressResolver Address { get; } 17 | 18 | public static PartyListHUDView PartyHudView { get; private set; } = null!; 19 | public static PartyListHUDUpdater PartyListHudUpdater { get; private set; } = null!; 20 | public static SettingsWindow SettingsWindow { get; private set; } = null!; 21 | public static NameplateUpdater NameplateUpdater { get; private set; } = null!; 22 | public static NPCNameplateFixer NpcNameplateFixer { get; private set; } = null!; 23 | public static NameplateView NameplateView { get; private set; } = null!; 24 | public static RoleTracker RoleTracker { get; private set; } = null!; 25 | public static ViewModeSetter ModeSetter { get; private set; } = null!; 26 | public static ChatNameUpdater ChatNameUpdater { get; private set; } = null!; 27 | public static PlayerContextMenu ContextMenu { get; private set; } = null!; 28 | public static CommandHandler CommandHandler { get; private set; } = null!; 29 | public static Settings Settings { get; private set; } = null!; 30 | public static PlayerStylesheet PlayerStylesheet { get; private set; } = null!; 31 | 32 | public Plugin(DalamudPluginInterface pluginInterface) 33 | { 34 | pluginInterface.Create(); 35 | 36 | Settings = Settings.Load(); 37 | 38 | Address = new PluginAddressResolver(); 39 | Address.Setup(Service.SigScanner); 40 | 41 | PlayerStylesheet = new PlayerStylesheet(Settings); 42 | 43 | SettingsWindow = new SettingsWindow(); 44 | 45 | XivApi.Initialize(this, Address); 46 | 47 | SeStringUtils.Initialize(); 48 | 49 | PartyHudView = new PartyListHUDView(Service.GameGui, PlayerStylesheet); 50 | RoleTracker = new RoleTracker(Settings); 51 | NameplateView = new NameplateView(RoleTracker, Settings, PlayerStylesheet, PartyHudView); 52 | ChatNameUpdater = new ChatNameUpdater(RoleTracker, PlayerStylesheet); 53 | PartyListHudUpdater = new PartyListHUDUpdater(PartyHudView, RoleTracker, Settings); 54 | ModeSetter = new ViewModeSetter(NameplateView, Settings, ChatNameUpdater, PartyListHudUpdater); 55 | NameplateUpdater = new NameplateUpdater(Settings, Address, NameplateView, ModeSetter); 56 | NpcNameplateFixer = new NPCNameplateFixer(NameplateView); 57 | ContextMenu = new PlayerContextMenu(RoleTracker, Settings, PlayerStylesheet); 58 | CommandHandler = new CommandHandler(); 59 | 60 | SettingsWindow.Initialize(); 61 | 62 | PartyListHudUpdater.Enable(); 63 | ModeSetter.Enable(); 64 | RoleTracker.Enable(); 65 | NameplateUpdater.Enable(); 66 | NpcNameplateFixer.Enable(); 67 | ChatNameUpdater.Enable(); 68 | ContextMenu.Enable(); 69 | } 70 | 71 | public void Dispose() 72 | { 73 | PartyHudView.Dispose(); 74 | PartyListHudUpdater.Dispose(); 75 | ChatNameUpdater.Dispose(); 76 | ContextMenu.Dispose(); 77 | NameplateUpdater.Dispose(); 78 | NpcNameplateFixer.Dispose(); 79 | RoleTracker.Dispose(); 80 | ModeSetter.Dispose(); 81 | SettingsWindow.Dispose(); 82 | CommandHandler.Dispose(); 83 | 84 | SeStringUtils.Dispose(); 85 | XivApi.DisposeInstance(); 86 | } 87 | } -------------------------------------------------------------------------------- /PartyIcons/PartyIcons.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | shdwp, abashby 6 | shdwp 7 | PartyIcons plugin 8 | shdwp 2021 9 | https://github.com/abashby/xivPartyIcons 10 | 1.1.4.0 11 | 12 | 13 | 14 | net7.0-windows 15 | x64 16 | enable 17 | latest 18 | true 19 | false 20 | false 21 | true 22 | true 23 | 24 | 25 | 26 | 27 | PreserveNewest 28 | 29 | 30 | PreserveNewest 31 | 32 | 33 | PreserveNewest 34 | 35 | 36 | PreserveNewest 37 | 38 | 39 | 40 | 41 | $(appdata)\XIVLauncher\addon\Hooks\dev\ 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | $(DalamudLibPath)FFXIVClientStructs.dll 55 | false 56 | 57 | 58 | $(DalamudLibPath)Newtonsoft.Json.dll 59 | false 60 | 61 | 62 | $(DalamudLibPath)Dalamud.dll 63 | false 64 | 65 | 66 | $(DalamudLibPath)ImGui.NET.dll 67 | false 68 | 69 | 70 | $(DalamudLibPath)ImGuiScene.dll 71 | false 72 | 73 | 74 | $(DalamudLibPath)Lumina.dll 75 | false 76 | 77 | 78 | $(DalamudLibPath)Lumina.Excel.dll 79 | false 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /PartyIcons/UI/ChatNameSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Numerics; 4 | using Dalamud.Interface; 5 | using ImGuiNET; 6 | using PartyIcons.Configuration; 7 | 8 | namespace PartyIcons.UI; 9 | 10 | public sealed class ChatNameSettings 11 | { 12 | public void DrawChatNameSettings() 13 | { 14 | const float separatorPadding = 2f; 15 | ImGui.Dummy(new Vector2(0, separatorPadding)); 16 | 17 | ImGui.PushStyleColor(0, ImGuiHelpers.DefaultColorPalette()[0]); 18 | ImGui.Text("Overworld"); 19 | ImGui.PopStyleColor(); 20 | ImGui.Separator(); 21 | ImGui.Dummy(new Vector2(0, separatorPadding)); 22 | ImGui.Indent(15 * ImGuiHelpers.GlobalScale); 23 | { 24 | ChatModeSection("##chat_overworld", 25 | () => Plugin.Settings.ChatOverworld, 26 | (config) => Plugin.Settings.ChatOverworld = config, 27 | "Party:"); 28 | 29 | ChatModeSection("##chat_others", 30 | () => Plugin.Settings.ChatOthers, 31 | (config) => Plugin.Settings.ChatOthers = config, 32 | "Others:"); 33 | } 34 | ImGui.Indent(-15 * ImGuiHelpers.GlobalScale); 35 | ImGui.Dummy(new Vector2(0, 2f)); 36 | 37 | ImGui.PushStyleColor(0, ImGuiHelpers.DefaultColorPalette()[0]); 38 | ImGui.Text("Instances"); 39 | ImGui.PopStyleColor(); 40 | ImGui.Separator(); 41 | ImGui.Dummy(new Vector2(0, separatorPadding)); 42 | ImGui.Indent(15 * ImGuiHelpers.GlobalScale); 43 | { 44 | ChatModeSection("##chat_dungeon", 45 | () => Plugin.Settings.ChatDungeon, 46 | (config) => Plugin.Settings.ChatDungeon = config, 47 | "Dungeon:"); 48 | 49 | ChatModeSection("##chat_raid", 50 | () => Plugin.Settings.ChatRaid, 51 | (config) => Plugin.Settings.ChatRaid = config, 52 | "Raid:"); 53 | 54 | ChatModeSection("##chat_alliance", 55 | () => Plugin.Settings.ChatAllianceRaid, 56 | (config) => Plugin.Settings.ChatAllianceRaid = config, 57 | "Alliance:"); 58 | } 59 | ImGui.Indent(-15 * ImGuiHelpers.GlobalScale); 60 | ImGui.Dummy(new Vector2(0, 2f)); 61 | } 62 | 63 | private static void ChatModeSection(string label, Func getter, Action setter, string title = "Chat name: ") 64 | { 65 | ChatConfig NewConf = new ChatConfig(ChatMode.GameDefault, true); 66 | 67 | ImGui.Text(title); 68 | ImGui.SameLine(100f); 69 | SettingsWindow.SetComboWidth(Enum.GetValues().Select(ChatModeToString)); 70 | 71 | // hack to fix incorrect configurations 72 | try 73 | { 74 | NewConf = getter(); 75 | } 76 | catch (ArgumentException ex) 77 | { 78 | setter(NewConf); 79 | Plugin.Settings.Save(); 80 | } 81 | 82 | if (ImGui.BeginCombo(label, ChatModeToString(NewConf.Mode))) 83 | { 84 | foreach (var mode in Enum.GetValues()) 85 | { 86 | if (ImGui.Selectable(ChatModeToString(mode), mode == NewConf.Mode)) 87 | { 88 | NewConf.Mode = mode;; 89 | setter(NewConf); 90 | Plugin.Settings.Save(); 91 | } 92 | } 93 | 94 | ImGui.EndCombo(); 95 | } 96 | 97 | ImGui.SameLine(); 98 | var colored = NewConf.UseRoleColor; 99 | 100 | if (ImGui.Checkbox($"Role Color{label}", ref colored)) 101 | { 102 | NewConf.UseRoleColor = colored; 103 | setter(NewConf); 104 | Plugin.Settings.Save(); 105 | } 106 | } 107 | 108 | private static string ChatModeToString(ChatMode mode) 109 | { 110 | return mode switch 111 | { 112 | ChatMode.GameDefault => "Game Default", 113 | ChatMode.Role => "Role", 114 | ChatMode.Job => "Job abbreviation", 115 | _ => throw new ArgumentException() 116 | }; 117 | } 118 | } -------------------------------------------------------------------------------- /PartyIcons/Api/PluginAddressResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using Dalamud.Game; 4 | using Dalamud.Game.Text; 5 | using FFXIVClientStructs.FFXIV.Client.UI.Misc; 6 | 7 | namespace PartyIcons.Api; 8 | 9 | [UnmanagedFunctionPointer(CallingConvention.ThisCall, CharSet = CharSet.Ansi)] 10 | public delegate IntPtr SetNamePlateDelegate(IntPtr addon, bool isPrefixTitle, bool displayTitle, IntPtr title, 11 | IntPtr name, IntPtr fcName, IntPtr prefix, int iconID); 12 | 13 | [UnmanagedFunctionPointer(CallingConvention.ThisCall, CharSet = CharSet.Ansi)] 14 | public delegate IntPtr AtkResNode_SetScaleDelegate(IntPtr node, float x, float y); 15 | 16 | [UnmanagedFunctionPointer(CallingConvention.ThisCall, CharSet = CharSet.Ansi)] 17 | public delegate IntPtr AtkResNode_SetPositionShortDelegate(IntPtr node, short x, short y); 18 | 19 | [UnmanagedFunctionPointer(CallingConvention.ThisCall, CharSet = CharSet.Ansi)] 20 | public delegate IntPtr Framework_GetUIModuleDelegate(IntPtr framework); 21 | 22 | [UnmanagedFunctionPointer(CallingConvention.ThisCall, CharSet = CharSet.Ansi)] 23 | public delegate IntPtr UIModule_GetRaptureAtkModuleDelegate(IntPtr uiModule); 24 | 25 | [UnmanagedFunctionPointer(CallingConvention.ThisCall, CharSet = CharSet.Ansi)] 26 | public delegate byte GroupManager_IsObjectIDInPartyDelegate(IntPtr groupManager, uint actorId); 27 | 28 | [UnmanagedFunctionPointer(CallingConvention.ThisCall, CharSet = CharSet.Ansi)] 29 | public delegate byte GroupManager_IsObjectIDInAllianceDelegate(IntPtr groupManager, uint actorId); 30 | 31 | [UnmanagedFunctionPointer(CallingConvention.ThisCall, CharSet = CharSet.Ansi)] 32 | public delegate IntPtr BattleCharaStore_LookupBattleCharaByObjectIDDelegate(IntPtr battleCharaStore, uint actorId); 33 | 34 | [UnmanagedFunctionPointer(CallingConvention.ThisCall, CharSet = CharSet.Ansi)] 35 | public delegate IntPtr PrintMessage(IntPtr chatManager, XivChatType xivChatType, IntPtr senderName, IntPtr message, 36 | uint senderId, byte param); 37 | 38 | public sealed class PluginAddressResolver : BaseAddressResolver 39 | { 40 | private const string AddonNamePlate_SetNamePlateSignature = 41 | "E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 48 8B 5C 24 ?? 45 38 BE"; 42 | 43 | public IntPtr AddonNamePlate_SetNamePlatePtr; 44 | 45 | private const string AtkResNode_SetScaleSignature = 46 | "8B 81 ?? ?? ?? ?? A8 01 75 ?? F3 0F 10 41 ?? 0F 2E C1 7A ?? 75 ?? F3 0F 10 41 ?? 0F 2E C2 7A ?? 74 ?? 83 C8 01 89 81 ?? ?? ?? ?? F3 0F 10 05 ?? ?? ?? ??"; 47 | 48 | public IntPtr AtkResNode_SetScalePtr; 49 | 50 | private const string AtkResNode_SetPositionShortSignature = 51 | "48 85 C9 74 4A 41 0F BF C0 66 0F 6E C8 0F BF C2 0F 5B C9 66 0F 6E D0"; 52 | 53 | public IntPtr AtkResNode_SetPositionShortPtr; 54 | 55 | private const string Framework_GetUIModuleSignature = "E8 ?? ?? ?? ?? 80 7B 1D 01"; 56 | public IntPtr Framework_GetUIModulePtr; 57 | 58 | private const string GroupManagerSignature = "48 8D 0D ?? ?? ?? ?? 44 8B E7"; 59 | public IntPtr GroupManagerPtr; 60 | 61 | private const string GroupManager_IsObjectIDInPartySignature = "E8 ?? ?? ?? ?? EB B8 E8"; 62 | public IntPtr GroupManager_IsObjectIDInPartyPtr; 63 | 64 | private const string GroupManager_IsObjectIDInAllianceSignature = "33 C0 44 8B CA F6 81 ?? ?? ?? ?? ??"; 65 | public IntPtr GroupManager_IsObjectIDInAlliancePtr; 66 | 67 | private const string BattleCharaStoreSignature = 68 | "8B D0 48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 8B D8 48 85 C0 74 3A"; 69 | 70 | public IntPtr BattleCharaStorePtr; 71 | 72 | private const string BattleCharaStore_LookupBattleCharaByObjectIDSignature = 73 | "E8 ?? ?? ?? ?? 48 8B D8 48 85 C0 74 3A 48 8B C8 E8 ?? ?? ?? ?? 84 C0"; 74 | 75 | public IntPtr BattleCharaStore_LookupBattleCharaByObjectIDPtr; 76 | 77 | private const string PrintChatMessageSignature = "E8 ?? ?? ?? ?? 4C 8B BC 24 ?? ?? ?? ?? 4D 85 F6"; 78 | public IntPtr PrintChatMessagePtr; 79 | 80 | protected override void Setup64Bit(SigScanner scanner) 81 | { 82 | AddonNamePlate_SetNamePlatePtr = scanner.ScanText(AddonNamePlate_SetNamePlateSignature); 83 | AtkResNode_SetScalePtr = scanner.ScanText(AtkResNode_SetScaleSignature); 84 | AtkResNode_SetPositionShortPtr = scanner.ScanText(AtkResNode_SetPositionShortSignature); 85 | Framework_GetUIModulePtr = scanner.ScanText(Framework_GetUIModuleSignature); 86 | GroupManagerPtr = scanner.GetStaticAddressFromSig(GroupManagerSignature); 87 | GroupManager_IsObjectIDInPartyPtr = scanner.ScanText(GroupManager_IsObjectIDInPartySignature); 88 | GroupManager_IsObjectIDInAlliancePtr = scanner.ScanText(GroupManager_IsObjectIDInAllianceSignature); 89 | BattleCharaStorePtr = scanner.GetStaticAddressFromSig(BattleCharaStoreSignature); 90 | BattleCharaStore_LookupBattleCharaByObjectIDPtr = 91 | scanner.ScanText(BattleCharaStore_LookupBattleCharaByObjectIDSignature); 92 | PrintChatMessagePtr = scanner.ScanText(PrintChatMessageSignature); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /PartyIcons/View/IconSet.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace PartyIcons.View; 4 | 5 | public sealed class IconSet 6 | { 7 | private Dictionary _icons = new(); 8 | private Dictionary _iconScales = new(); 9 | 10 | public IconSet() 11 | { 12 | Add("Gold", new[] 13 | { 14 | 62001, 62002, 62003, 62004, 62005, 62006, 62007, 62008, 62009, 62010, 15 | 62011, 62012, 62013, 62014, 62015, 62016, 62017, 62018, 62019, 62020, 16 | 62021, 62022, 62023, 62024, 62025, 62026, 62027, 62028, 62029, 62030, 17 | 62031, 62032, 62033, 62034, 62035, 62036, 62037, 62038, 62039, 62040 18 | }); 19 | Add("Framed", new[] 20 | { 21 | 62101, 62102, 62103, 62104, 62105, 62106, 62107, 62108, 62109, 62110, 22 | 62111, 62112, 62113, 62114, 62115, 62116, 62117, 62118, 62119, 62120, 23 | 62121, 62122, 62123, 62124, 62125, 62126, 62127, 62128, 62129, 62130, 24 | 62131, 62132, 62133, 62134, 62135, 62136, 62137, 62138, 62139, 62140 25 | }); 26 | Add("Glowing", new[] 27 | { 28 | 62301, 62302, 62303, 62304, 62305, 62306, 62307, 62310, 62311, 62312, 29 | 62313, 62314, 62315, 62316, 62317, 62318, 62319, 62320, 62401, 62402, 30 | 62403, 62404, 62405, 62406, 62407, 62308, 62408, 62409, 62309, 62410, 31 | 62411, 62412, 62413, 62414, 62415, 62416, 62417, 62418, 62419, 62420 32 | }); 33 | Add("Grey", new[] 34 | { 35 | 91022, 91023, 91024, 91025, 91026, 91028, 91029, 91031, 91032, 91033, 36 | 91034, 91035, 91036, 91037, 91038, 91039, 91040, 91041, 91079, 91080, 37 | 91081, 91082, 91083, 91084, 91085, 91030, 91086, 91087, 91121, 91122, 38 | 91125, 91123, 91124, 91127, 91128, 91129, 91130, 91131, 91132, 91133 39 | }, 2); 40 | Add("Black", new[] 41 | { 42 | 91522, 91523, 91524, 91525, 91526, 91528, 91529, 91531, 91532, 91533, 43 | 91534, 91535, 91536, 91537, 91538, 91539, 91540, 91541, 91579, 91580, 44 | 91581, 91582, 91583, 91584, 91585, 91530, 91586, 91587, 91621, 91622, 45 | 91625, 91623, 91624, 91627, 91628, 91629, 91630, 91631, 91632, 91633 46 | }, 2); 47 | Add("Yellow", new[] 48 | { 49 | 92022, 92023, 92024, 92025, 92026, 92028, 92029, 92031, 92032, 92033, 50 | 92034, 92035, 92036, 92037, 92038, 92039, 92040, 92041, 92079, 92080, 51 | 92081, 92082, 92083, 92084, 92085, 92030, 92086, 92087, 92121, 92122, 52 | 92125, 92123, 92124, 92127, 92128, 92129, 92130, 92131, 92132, 92133 53 | }, 2); 54 | Add("Orange", new[] 55 | { 56 | 92522, 92523, 92524, 92525, 92526, 92528, 92529, 92531, 92532, 92533, 57 | 92534, 92535, 92536, 92537, 92538, 92539, 92540, 92541, 92579, 92580, 58 | 92581, 92582, 92583, 92584, 92585, 92530, 92586, 92587, 92621, 92622, 59 | 92625, 92623, 92624, 92627, 92628, 92629, 92630, 92631, 92632, 92633 60 | }, 2); 61 | Add("Red", new[] 62 | { 63 | 93022, 93023, 93024, 93025, 93026, 93028, 93029, 93031, 93032, 93033, 64 | 93034, 93035, 93036, 93037, 93038, 93039, 93040, 93041, 93079, 93080, 65 | 93081, 93082, 93083, 93084, 93085, 93030, 93086, 93087, 93121, 93122, 66 | 93125, 93123, 93124, 93127, 93128, 93129, 93130, 93131, 93132, 93133 67 | }, 2); 68 | Add("Purple", new[] 69 | { 70 | 93522, 93523, 93524, 93525, 93526, 93528, 93529, 93531, 93532, 93533, 71 | 93534, 93535, 93536, 93537, 93538, 93539, 93540, 93541, 93579, 93580, 72 | 93581, 93582, 93583, 93584, 93585, 93530, 93586, 93587, 93621, 93622, 73 | 93625, 93623, 93624, 93627, 93628, 93629, 93630, 93631, 93632, 93633 74 | }, 2); 75 | Add("Blue", new[] 76 | { 77 | 94022, 94023, 94024, 94025, 94026, 94028, 94029, 94031, 94032, 94033, 78 | 94034, 94035, 94036, 94037, 94038, 94039, 94040, 94041, 94079, 94080, 79 | 94081, 94082, 94083, 94084, 94085, 94030, 94086, 94087, 94121, 94122, 80 | 94125, 94123, 94124, 94127, 94128, 94129, 94130, 94131, 94132, 94133 81 | }, 2); 82 | Add("Green", new[] 83 | { 84 | 94522, 94523, 94524, 94525, 94526, 94528, 94529, 94531, 94532, 94533, 85 | 94534, 94535, 94536, 94537, 94538, 94539, 94540, 94541, 94579, 94580, 86 | 94581, 94582, 94583, 94584, 94585, 94530, 94586, 94587, 94621, 94622, 87 | 94625, 94623, 94624, 94627, 94628, 94629, 94630, 94631, 94632, 94633 88 | }, 2); 89 | 90 | Add("Role", new[] 91 | { 92 | 62581, 62584, 62581, 62584, 62586, 62582, 62502, 62502, 62503, 62504, 93 | 62505, 62506, 62507, 62508, 62509, 62510, 62511, 62512, 62581, 62584, 94 | 62581, 62584, 62586, 62582, 62587, 62587, 62587, 62582, 62584, 62584, 95 | 62586, 62581, 62582, 62584, 62587, 62587, 62581, 62586 96 | }); 97 | } 98 | 99 | public void Add(string id, int[] icons, float scale = 1f) 100 | { 101 | _icons[id] = icons; 102 | _iconScales[id] = scale; 103 | } 104 | 105 | public int GetJobIcon(string set, uint jobId) => _icons[set][jobId - 1]; 106 | } 107 | -------------------------------------------------------------------------------- /PartyIcons/UI/GeneralSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Net.Http; 4 | using System.Numerics; 5 | using System.Text.RegularExpressions; 6 | using Dalamud.Interface; 7 | using Dalamud.Interface.Colors; 8 | using Dalamud.Interface.Components; 9 | using Dalamud.Logging; 10 | using ImGuiNET; 11 | using PartyIcons.UI.Controls; 12 | 13 | namespace PartyIcons.UI; 14 | 15 | public sealed class GeneralSettings 16 | { 17 | private readonly Notice _notice = new(); 18 | 19 | public void Initialize() 20 | { 21 | _notice.Initialize(); 22 | } 23 | 24 | private FlashingText _testingModeText = new(); 25 | 26 | public void DrawGeneralSettings() 27 | { 28 | ImGui.Dummy(new Vector2(0, 2f)); 29 | 30 | var usePriorityIcons = Plugin.Settings.UsePriorityIcons; 31 | 32 | if (ImGui.Checkbox("##usePriorityIcons", ref usePriorityIcons)) 33 | { 34 | Plugin.Settings.UsePriorityIcons = usePriorityIcons; 35 | Plugin.Settings.Save(); 36 | } 37 | 38 | ImGui.SameLine(); 39 | ImGui.Text("Prioritize status icons"); 40 | ImGuiComponents.HelpMarker("Prioritizes certain status icons over job icons.\n\nInside of a duty, the only status icons that take priority are Disconnecting, Viewing Cutscene, Idle, and Group Pose.\n\nEven if this is unchecked, the Disconnecting icon will always take priority."); 41 | 42 | /* 43 | // Sample code for later when we want to incorporate icon previews into the UI. 44 | var iconTex = _dataManager.GetIcon(61508); 45 | //if (iconTex == null) return; 46 | 47 | if (iconTex != null) 48 | { 49 | var tex = Interface.UiBuilder.LoadImageRaw(iconTex.GetRgbaImageData(), iconTex.Header.Width, iconTex.Header.Height, 4); 50 | } 51 | 52 | // ... 53 | 54 | ImGui.Image(tex.ImGuiHandle, new Vector2(tex.Width, tex.Height)); 55 | */ 56 | 57 | var testingMode = Plugin.Settings.TestingMode; 58 | 59 | if (ImGui.Checkbox("##testingMode", ref testingMode)) 60 | { 61 | Plugin.Settings.TestingMode = testingMode; 62 | Plugin.Settings.Save(); 63 | } 64 | 65 | ImGui.SameLine(); 66 | _testingModeText.IsFlashing = Plugin.Settings.TestingMode; 67 | _testingModeText.Draw(() => ImGui.Text("Enable testing mode")); 68 | // ImGui.Text("Enable testing mode"); 69 | ImGuiComponents.HelpMarker("Applies settings to any player, contrary to only the ones that are in the party."); 70 | 71 | var chatContentMessage = Plugin.Settings.ChatContentMessage; 72 | 73 | if (ImGui.Checkbox("##chatmessage", ref chatContentMessage)) 74 | { 75 | Plugin.Settings.ChatContentMessage = chatContentMessage; 76 | Plugin.Settings.Save(); 77 | } 78 | 79 | ImGui.SameLine(); 80 | ImGui.Text("Display chat message when entering duty"); 81 | ImGuiComponents.HelpMarker("Can be used to determine the duty type before fully loading in."); 82 | 83 | _notice.DisplayNotice(); 84 | } 85 | } 86 | 87 | public sealed class Notice 88 | { 89 | public Notice() 90 | { 91 | _httpClient = new HttpClient(); 92 | } 93 | 94 | private HttpClient _httpClient; 95 | 96 | public void Initialize() 97 | { 98 | DownloadAndParseNotice(); 99 | } 100 | 101 | private string? _noticeString; 102 | private string? _noticeUrl; 103 | 104 | private void DownloadAndParseNotice() 105 | { 106 | try 107 | { 108 | var stringAsync = _httpClient.GetStringAsync("https://shdwp.github.io/ukraine/xiv_notice.txt"); 109 | stringAsync.Wait(); 110 | var strArray = stringAsync.Result.Split('|'); 111 | 112 | if ((uint) strArray.Length > 0U) 113 | { 114 | _noticeString = Regex.Replace(strArray[0], "\n", "\n\n"); 115 | } 116 | 117 | if (strArray.Length <= 1) 118 | { 119 | return; 120 | } 121 | 122 | _noticeUrl = strArray[1]; 123 | 124 | if (!(_noticeUrl.StartsWith("http://") || _noticeUrl.StartsWith("https://"))) 125 | { 126 | PluginLog.Warning($"Received invalid noticeUrl {_noticeUrl}, ignoring"); 127 | _noticeUrl = null; 128 | } 129 | } 130 | catch (Exception ex) { } 131 | } 132 | 133 | public void DisplayNotice() 134 | { 135 | if (_noticeString == null) 136 | { 137 | return; 138 | } 139 | 140 | ImGui.Dummy(new Vector2(0.0f, 15f)); 141 | ImGui.PushStyleColor((ImGuiCol) 0, ImGuiColors.DPSRed); 142 | ImGuiHelpers.SafeTextWrapped(_noticeString); 143 | 144 | if (_noticeUrl != null) 145 | { 146 | if (ImGui.Button(_noticeUrl)) 147 | { 148 | try 149 | { 150 | Process.Start(new ProcessStartInfo 151 | { 152 | FileName = _noticeUrl, 153 | UseShellExecute = true 154 | }); 155 | } 156 | catch (Exception ex) { } 157 | } 158 | } 159 | 160 | ImGui.PopStyleColor(); 161 | } 162 | } -------------------------------------------------------------------------------- /PartyIcons/View/PlayerContextMenu.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Dalamud.IoC; 3 | using Dalamud.Logging; 4 | using PartyIcons.Entities; 5 | using PartyIcons.Runtime; 6 | using PartyIcons.Stylesheet; 7 | using Dalamud.ContextMenu; 8 | using PartyIcons.Configuration; 9 | 10 | namespace PartyIcons.View 11 | { 12 | public sealed class PlayerContextMenu : IDisposable 13 | { 14 | private DalamudContextMenu _contextMenu = new(); 15 | 16 | // Whether to indicate context menu items are from Dalamud. 17 | // Setting this to true at least sets apart the menu items given that submenus are not currently supported in Dalamud.ContextMenu. 18 | private static bool _useDalamudIndicator = true; 19 | 20 | private readonly RoleTracker _roleTracker; 21 | private readonly Settings _configuration; 22 | private readonly PlayerStylesheet _stylesheet; 23 | 24 | public PlayerContextMenu(RoleTracker roleTracker, Settings configuration, PlayerStylesheet stylesheet) 25 | { 26 | _roleTracker = roleTracker; 27 | _configuration = configuration; 28 | _stylesheet = stylesheet; 29 | } 30 | 31 | public void Enable() 32 | { 33 | _contextMenu.OnOpenGameObjectContextMenu += OnOpenContextMenu; 34 | } 35 | 36 | public void Disable() 37 | { 38 | _contextMenu.OnOpenGameObjectContextMenu -= OnOpenContextMenu; 39 | } 40 | 41 | public void Dispose() 42 | { 43 | Disable(); 44 | } 45 | 46 | private void OnOpenContextMenu(GameObjectContextMenuOpenArgs args) 47 | { 48 | if (!_configuration.UseContextMenu || args.Text == null || !IsMenuValid(args)) 49 | { 50 | return; 51 | } 52 | 53 | var playerName = args.Text.TextValue; 54 | var playerWorld = args.ObjectWorld; 55 | 56 | PluginLog.Verbose($"Opening menu for {playerName}"); 57 | 58 | AddSuggestedRoleMenuItem(playerName, playerWorld, args); 59 | AddSwapRoleMenuItem(playerName, playerWorld, args); 60 | AddAssignPartyRoleMenuItems(playerName, playerWorld, args); 61 | } 62 | 63 | private void AddSuggestedRoleMenuItem(string playerName, ushort playerWorld, GameObjectContextMenuOpenArgs args) 64 | { 65 | if (_roleTracker.TryGetSuggestedRole(playerName, playerWorld, out var role)) 66 | { 67 | var roleName = _stylesheet.GetRoleName(role); 68 | 69 | var contextMenuItem = new GameObjectContextMenuItem( 70 | $"Assign {roleName} (suggested)", 71 | _ => OnAssignRole(playerName, playerWorld, role), 72 | _useDalamudIndicator); 73 | 74 | args.AddCustomItem(contextMenuItem); 75 | } 76 | } 77 | 78 | private void AddSwapRoleMenuItem(string playerName, ushort playerWorld, GameObjectContextMenuOpenArgs args) 79 | { 80 | if (_roleTracker.TryGetAssignedRole(playerName, playerWorld, out var currentRole)) 81 | { 82 | var swappedRole = RoleIdUtils.Counterpart(currentRole); 83 | var swappedRoleName = _stylesheet.GetRoleName(swappedRole); 84 | 85 | var contextMenuItem = new GameObjectContextMenuItem( 86 | $"Swap to {swappedRoleName}", 87 | _ => OnAssignRole(playerName, playerWorld, swappedRole), 88 | _useDalamudIndicator); 89 | 90 | args.AddCustomItem(contextMenuItem); 91 | } 92 | } 93 | 94 | private void AddAssignPartyRoleMenuItems(string playerName, ushort playerWorld, GameObjectContextMenuOpenArgs args) 95 | { 96 | foreach (var role in Enum.GetValues()) 97 | { 98 | if (role == RoleId.Undefined) 99 | { 100 | continue; 101 | } 102 | 103 | var contextMenuItem = new GameObjectContextMenuItem( 104 | $"Assign {_stylesheet.GetRoleName(role)}", 105 | _ => OnAssignRole(playerName, playerWorld, role), 106 | _useDalamudIndicator); 107 | 108 | args.AddCustomItem(contextMenuItem); 109 | } 110 | } 111 | 112 | private void OnAssignRole(string playerName, ushort playerWorld, RoleId role) 113 | { 114 | _roleTracker.OccupyRole(playerName, playerWorld, role); 115 | 116 | _roleTracker.CalculateUnassignedPartyRoles(); 117 | } 118 | 119 | private bool IsMenuValid(GameObjectContextMenuOpenArgs args) 120 | { 121 | PluginLog.LogDebug($"ParentAddonName {args.ParentAddonName}"); 122 | 123 | switch (args.ParentAddonName) 124 | { 125 | case null: // Nameplate/Model menu 126 | case "PartyMemberList": 127 | case "ChatLog": 128 | case "_PartyList": 129 | case "ContentMemberList": // Eureka/Bozja/... 130 | return args.Text != null && 131 | args.ObjectWorld != 0 && // Player 132 | args.ObjectWorld != 65535; 133 | 134 | default: 135 | return false; 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /PartyIcons/UI/SettingsWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Numerics; 5 | using Dalamud.Interface; 6 | using Dalamud.Interface.Colors; 7 | using Dalamud.Interface.Components; 8 | using ImGuiNET; 9 | using PartyIcons.Configuration; 10 | using PartyIcons.Entities; 11 | using PartyIcons.UI.Controls; 12 | using PartyIcons.Utils; 13 | 14 | namespace PartyIcons.UI; 15 | 16 | public sealed class SettingsWindow : IDisposable 17 | { 18 | public bool SettingsVisible 19 | { 20 | get => _settingsVisible; 21 | set 22 | { 23 | _settingsVisible = value; 24 | 25 | if (value) 26 | { 27 | _windowSizeHelper.ForceSize(); 28 | } 29 | } 30 | } 31 | 32 | public void Initialize() 33 | { 34 | Service.PluginInterface.UiBuilder.Draw += DrawSettingsWindow; 35 | Service.PluginInterface.UiBuilder.OpenConfigUi += OpenSettingsWindow; 36 | 37 | _generalSettings.Initialize(); 38 | _nameplateSettings.Initialize(); 39 | } 40 | 41 | public void Dispose() 42 | { 43 | Service.PluginInterface.UiBuilder.Draw -= DrawSettingsWindow; 44 | Service.PluginInterface.UiBuilder.OpenConfigUi -= OpenSettingsWindow; 45 | } 46 | 47 | public void OpenSettingsWindow() 48 | { 49 | SettingsVisible = true; 50 | } 51 | 52 | public void ToggleSettingsWindow() 53 | { 54 | SettingsVisible = !SettingsVisible; 55 | } 56 | 57 | public void DrawSettingsWindow() 58 | { 59 | if (!SettingsVisible) 60 | { 61 | return; 62 | } 63 | 64 | _windowSizeHelper.SetWindowSize(); 65 | 66 | if (ImGui.Begin("PartyIcons", ref _settingsVisible)) 67 | { 68 | _windowSizeHelper.CheckWindowSize(); 69 | 70 | if (ImGui.BeginTabBar("##tabbar")) 71 | { 72 | _generalTabText.IsFlashing = Plugin.Settings.TestingMode; 73 | 74 | if (_generalTabText.Draw(() => ImGui.BeginTabItem("General##general"))) 75 | { 76 | if (ImGui.BeginChild("##general_content")) 77 | { 78 | _generalSettings.DrawGeneralSettings(); 79 | 80 | ImGui.EndChild(); 81 | } 82 | 83 | ImGui.EndTabItem(); 84 | } 85 | 86 | if (ImGui.BeginTabItem("Nameplates")) 87 | { 88 | if (ImGui.BeginChild("##nameplates_content")) 89 | { 90 | _nameplateSettings.DrawNameplateSettings(); 91 | 92 | ImGui.EndChild(); 93 | } 94 | 95 | ImGui.EndTabItem(); 96 | } 97 | 98 | if (ImGui.BeginTabItem("Chat Names")) 99 | { 100 | if (ImGui.BeginChild("##chat_names_content")) 101 | { 102 | _chatNameSettings.DrawChatNameSettings(); 103 | 104 | ImGui.EndChild(); 105 | } 106 | 107 | ImGui.EndTabItem(); 108 | } 109 | 110 | if (ImGui.BeginTabItem("Roles##static_assignments")) 111 | { 112 | if (ImGui.BeginChild("##static_assignments_content")) 113 | { 114 | _staticAssignmentsSettings.DrawStaticAssignmentsSettings(); 115 | 116 | ImGui.EndChild(); 117 | } 118 | 119 | ImGui.EndTabItem(); 120 | } 121 | 122 | ImGui.EndTabBar(); 123 | } 124 | } 125 | 126 | ImGui.End(); 127 | } 128 | 129 | public static void SetComboWidth(IEnumerable values) 130 | { 131 | const float paddingMultiplier = 1.05f; 132 | float maxItemWidth = float.MinValue; 133 | 134 | foreach (var text in values) 135 | { 136 | var itemWidth = ImGui.CalcTextSize(text).X + ImGui.GetStyle().ScrollbarSize * 3f; 137 | maxItemWidth = Math.Max(maxItemWidth, itemWidth); 138 | } 139 | 140 | ImGui.SetNextItemWidth(maxItemWidth * paddingMultiplier); 141 | } 142 | 143 | public static void ImGuiHelpTooltip(string tooltip, bool experimental = false) 144 | { 145 | ImGui.SameLine(); 146 | 147 | if (experimental) 148 | { 149 | ImGui.TextColored(new Vector4(0.8f, 0.0f, 0.0f, 1f), "!"); 150 | } 151 | else 152 | { 153 | ImGui.TextColored(new Vector4(0.8f, 0.8f, 0.8f, 1f), "?"); 154 | } 155 | 156 | if (ImGui.IsItemHovered()) 157 | { 158 | ImGui.SetTooltip(tooltip); 159 | } 160 | } 161 | 162 | private bool _settingsVisible = false; 163 | private static WindowSizeHelper _windowSizeHelper = new(); 164 | private readonly GeneralSettings _generalSettings = new(); 165 | private readonly NameplateSettings _nameplateSettings = new(); 166 | private readonly ChatNameSettings _chatNameSettings = new(); 167 | private readonly StaticAssignmentsSettings _staticAssignmentsSettings = new StaticAssignmentsSettings(); 168 | 169 | private FlashingText _generalTabText = new(); 170 | } 171 | -------------------------------------------------------------------------------- /PartyIcons/Runtime/PartyListHUDUpdater.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Dalamud.Game; 4 | using Dalamud.Hooking; 5 | using Dalamud.Logging; 6 | using PartyIcons.Configuration; 7 | using PartyIcons.Entities; 8 | using PartyIcons.Utils; 9 | 10 | namespace PartyIcons.Runtime; 11 | 12 | public sealed class PartyListHUDUpdater : IDisposable 13 | { 14 | public bool UpdateHUD = false; 15 | 16 | private readonly Settings _configuration; 17 | private readonly PartyListHUDView _view; 18 | private readonly RoleTracker _roleTracker; 19 | 20 | private bool _displayingRoles; 21 | 22 | private bool _previousInParty; 23 | private bool _previousTesting; 24 | private DateTime _lastUpdate = DateTime.Today; 25 | 26 | private const string PrepareZoningSig = "48 89 5C 24 ?? 57 48 83 EC 40 F6 42 0D 08"; 27 | private delegate nint PrepareZoningDelegate (nint a1, nint a2, byte a3); 28 | private Hook prepareZoningHook; 29 | 30 | public PartyListHUDUpdater(PartyListHUDView view, RoleTracker roleTracker, Settings configuration) 31 | { 32 | _view = view; 33 | _roleTracker = roleTracker; 34 | _configuration = configuration; 35 | 36 | prepareZoningHook = 37 | Hook.FromAddress(Service.SigScanner.ScanText(PrepareZoningSig), PrepareZoning); 38 | prepareZoningHook?.Enable(); 39 | } 40 | 41 | public void Enable() 42 | { 43 | _roleTracker.OnAssignedRolesUpdated += OnAssignedRolesUpdated; 44 | Service.Framework.Update += OnUpdate; 45 | _configuration.OnSave += OnConfigurationSave; 46 | Service.ClientState.EnterPvP += OnEnterPvP; 47 | } 48 | 49 | public void Dispose() 50 | { 51 | Service.ClientState.EnterPvP -= OnEnterPvP; 52 | _configuration.OnSave -= OnConfigurationSave; 53 | Service.Framework.Update -= OnUpdate; 54 | _roleTracker.OnAssignedRolesUpdated -= OnAssignedRolesUpdated; 55 | prepareZoningHook?.Dispose(); 56 | } 57 | 58 | private nint PrepareZoning(nint a1, nint a2, byte a3) 59 | { 60 | PluginLog.Verbose("PartyListHUDUpdater Forcing update due to zoning"); 61 | // PluginLog.Verbose(_view.GetDebugInfo()); 62 | UpdatePartyListHUD(); 63 | return prepareZoningHook.OriginalDisposeSafe(a1,a2,a3); 64 | } 65 | 66 | private void OnEnterPvP() 67 | { 68 | if (_displayingRoles) 69 | { 70 | PluginLog.Verbose("PartyListHUDUpdater: reverting party list due to entering a PvP zone"); 71 | _displayingRoles = false; 72 | _view.RevertSlotNumbers(); 73 | } 74 | } 75 | 76 | private void OnConfigurationSave() 77 | { 78 | if (_displayingRoles) 79 | { 80 | PluginLog.Verbose("PartyListHUDUpdater: reverting party list before the update due to config change"); 81 | _view.RevertSlotNumbers(); 82 | } 83 | 84 | PluginLog.Verbose("PartyListHUDUpdater forcing update due to changes in the config"); 85 | // PluginLog.Verbose(_view.GetDebugInfo()); 86 | UpdatePartyListHUD(); 87 | } 88 | 89 | private void OnAssignedRolesUpdated() 90 | { 91 | PluginLog.Verbose("PartyListHUDUpdater forcing update due to assignments update"); 92 | // PluginLog.Verbose(_view.GetDebugInfo()); 93 | UpdatePartyListHUD(); 94 | } 95 | 96 | private void OnUpdate(Framework framework) 97 | { 98 | var inParty = Service.PartyList.Any(); 99 | 100 | if ((!inParty && _previousInParty) || (!_configuration.TestingMode && _previousTesting)) 101 | { 102 | PluginLog.Verbose("No longer in party/testing mode, reverting party list HUD changes"); 103 | _displayingRoles = false; 104 | _view.RevertSlotNumbers(); 105 | } 106 | 107 | _previousInParty = inParty; 108 | _previousTesting = _configuration.TestingMode; 109 | 110 | if (DateTime.Now - _lastUpdate > TimeSpan.FromSeconds(15)) 111 | { 112 | UpdatePartyListHUD(); 113 | _lastUpdate = DateTime.Now; 114 | } 115 | } 116 | 117 | private void UpdatePartyListHUD() 118 | { 119 | if (!_configuration.DisplayRoleInPartyList) 120 | { 121 | return; 122 | } 123 | 124 | if (_configuration.TestingMode && 125 | Service.ClientState.LocalPlayer is { } localPlayer) 126 | { 127 | _view.SetPartyMemberRole(localPlayer.Name.ToString(), localPlayer.ObjectId, RoleId.M1); 128 | } 129 | 130 | if (!UpdateHUD) 131 | { 132 | return; 133 | } 134 | 135 | if (Service.ClientState.IsPvP) 136 | { 137 | return; 138 | } 139 | 140 | PluginLog.Verbose($"Updating party list HUD. members = {Service.PartyList.Length}"); 141 | _displayingRoles = true; 142 | 143 | foreach (var member in Service.PartyList) 144 | { 145 | PluginLog.Verbose($"member {member.Name.ToString()}"); 146 | 147 | if (_roleTracker.TryGetAssignedRole(member.Name.ToString(), member.World.Id, out var roleId)) 148 | { 149 | PluginLog.Verbose($"Updating party list hud: member {member.Name} to {roleId}"); 150 | _view.SetPartyMemberRole(member.Name.ToString(), member.ObjectId, roleId); 151 | } 152 | else 153 | { 154 | PluginLog.Verbose($"Could not get assigned role for member {member.Name.ToString()}, {member.World.Id}"); 155 | } 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /PartyIcons/Configuration/Settings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using Newtonsoft.Json; 5 | using Newtonsoft.Json.Linq; 6 | using Dalamud.Configuration; 7 | using Dalamud.Logging; 8 | using PartyIcons.Entities; 9 | 10 | namespace PartyIcons.Configuration; 11 | 12 | [Serializable] 13 | public class Settings : IPluginConfiguration 14 | { 15 | public static int CurrentVersion = 2; 16 | 17 | public int Version { get; set; } = CurrentVersion; 18 | 19 | public bool ChatContentMessage = true; 20 | public bool HideLocalPlayerNameplate = false; 21 | public bool TestingMode = true; 22 | public bool EasternNamingConvention = false; 23 | public bool DisplayRoleInPartyList = false; 24 | public bool UseContextMenu = false; 25 | public bool AssignFromChat = true; 26 | public bool UsePriorityIcons = true; 27 | 28 | public IconSetId IconSetId { get; set; } = IconSetId.GlowingColored; 29 | public NameplateSizeMode SizeMode { get; set; } = NameplateSizeMode.Medium; 30 | 31 | public NameplateMode NameplateOverworld { get; set; } = NameplateMode.SmallJobIcon; 32 | public NameplateMode NameplateAllianceRaid { get; set; } = NameplateMode.BigJobIconAndPartySlot; 33 | public NameplateMode NameplateDungeon { get; set; } = NameplateMode.BigJobIconAndPartySlot; 34 | public NameplateMode NameplateBozjaParty { get; set; } = NameplateMode.BigJobIconAndPartySlot; 35 | public NameplateMode NameplateBozjaOthers { get; set; } = NameplateMode.Default; 36 | public NameplateMode NameplateRaid { get; set; } = NameplateMode.RoleLetters; 37 | public NameplateMode NameplateOthers { get; set; } = NameplateMode.SmallJobIcon; 38 | 39 | public ChatConfig ChatOverworld { get; set; } = new (ChatMode.Role); 40 | public ChatConfig ChatAllianceRaid { get; set; } = new (ChatMode.Role); 41 | public ChatConfig ChatDungeon { get; set; } = new (ChatMode.Job); 42 | public ChatConfig ChatRaid { get; set; } = new (ChatMode.Role); 43 | public ChatConfig ChatOthers { get; set; } = new (ChatMode.Job); 44 | 45 | public Dictionary StaticAssignments { get; set; } = new(); 46 | 47 | public event Action OnSave; 48 | 49 | public Settings() {} 50 | 51 | public Settings(SettingsV1 configV1) 52 | { 53 | ChatContentMessage = configV1.ChatContentMessage; 54 | HideLocalPlayerNameplate = configV1.HideLocalPlayerNameplate; 55 | TestingMode = configV1.TestingMode; 56 | EasternNamingConvention = configV1.EasternNamingConvention; 57 | DisplayRoleInPartyList = configV1.DisplayRoleInPartyList; 58 | UseContextMenu = configV1.UseContextMenu; 59 | AssignFromChat = configV1.AssignFromChat; 60 | UsePriorityIcons = configV1.UsePriorityIcons; 61 | 62 | IconSetId = configV1.IconSetId; 63 | SizeMode = configV1.SizeMode; 64 | NameplateOverworld = configV1.NameplateOverworld; 65 | NameplateAllianceRaid = configV1.NameplateAllianceRaid; 66 | NameplateDungeon = configV1.NameplateDungeon; 67 | NameplateBozjaParty = configV1.NameplateBozjaParty; 68 | NameplateBozjaOthers = configV1.NameplateBozjaOthers; 69 | NameplateRaid = configV1.NameplateRaid; 70 | NameplateOthers = configV1.NameplateOthers; 71 | 72 | ChatOverworld = SettingsV1.ToChatConfig(configV1.ChatOverworld); 73 | ChatAllianceRaid = SettingsV1.ToChatConfig(configV1.ChatAllianceRaid); 74 | ChatDungeon = SettingsV1.ToChatConfig(configV1.ChatDungeon); 75 | ChatRaid = SettingsV1.ToChatConfig(configV1.ChatRaid); 76 | ChatOthers = SettingsV1.ToChatConfig(configV1.ChatOthers); 77 | 78 | StaticAssignments = configV1.StaticAssignments; 79 | } 80 | 81 | public static Settings Load() 82 | { 83 | Settings? config = null; 84 | 85 | try 86 | { 87 | var configFileInfo = Service.PluginInterface.ConfigFile; 88 | 89 | if (configFileInfo.Exists) 90 | { 91 | var reader = new StreamReader(configFileInfo.FullName); 92 | var fileText = reader.ReadToEnd(); 93 | reader.Dispose(); 94 | 95 | var versionNumber = GetConfigFileVersion(fileText); 96 | 97 | if (versionNumber == Settings.CurrentVersion) 98 | { 99 | config = JsonConvert.DeserializeObject(fileText); 100 | PluginLog.Information($"Loaded configuration v{versionNumber} (current)"); 101 | } 102 | else if (versionNumber == 1) 103 | { 104 | var configV1 = JsonConvert.DeserializeObject(fileText); 105 | config = new Settings(configV1); 106 | config.Save(); 107 | PluginLog.Information($"Converted configuration v{versionNumber} to v{Settings.CurrentVersion}"); 108 | } 109 | else 110 | { 111 | PluginLog.Error($"No reader available for configuration v{versionNumber}"); 112 | } 113 | } 114 | } 115 | catch (Exception e) 116 | { 117 | PluginLog.Error("Could not read configuration."); 118 | PluginLog.Error(e.ToString()); 119 | } 120 | 121 | if (config != null) 122 | { 123 | return config; 124 | } 125 | 126 | PluginLog.Information("Creating a new configuration."); 127 | return new Settings(); 128 | } 129 | 130 | public void Save() 131 | { 132 | Service.PluginInterface.SavePluginConfig(this); 133 | OnSave?.Invoke(); 134 | } 135 | 136 | private static int GetConfigFileVersion(string fileText) 137 | { 138 | var json = JObject.Parse(fileText); 139 | 140 | return json.GetValue("Version")?.Value() ?? 0; 141 | } 142 | } -------------------------------------------------------------------------------- /PartyIcons/UI/StaticAssignmentsSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Numerics; 5 | using Dalamud.Interface; 6 | using Dalamud.Interface.Colors; 7 | using Dalamud.Interface.Components; 8 | using ImGuiNET; 9 | using PartyIcons.Entities; 10 | 11 | namespace PartyIcons.UI; 12 | 13 | public sealed class StaticAssignmentsSettings 14 | { 15 | public void DrawStaticAssignmentsSettings() 16 | { 17 | ImGui.Dummy(new Vector2(0, 2f)); 18 | 19 | var easternNamingConvention = Plugin.Settings.EasternNamingConvention; 20 | 21 | if (ImGui.Checkbox("##easteannaming", ref easternNamingConvention)) 22 | { 23 | Plugin.Settings.EasternNamingConvention = easternNamingConvention; 24 | Plugin.Settings.Save(); 25 | } 26 | 27 | ImGui.SameLine(); 28 | ImGui.Text("Eastern role naming convention"); 29 | ImGuiComponents.HelpMarker("Use Japanese data center role naming convention (MT ST D1-D4 H1-2)."); 30 | 31 | var displayRoleInPartyList = Plugin.Settings.DisplayRoleInPartyList; 32 | 33 | if (ImGui.Checkbox("##displayrolesinpartylist", ref displayRoleInPartyList)) 34 | { 35 | Plugin.Settings.DisplayRoleInPartyList = displayRoleInPartyList; 36 | Plugin.Settings.Save(); 37 | } 38 | 39 | ImGui.SameLine(); 40 | ImGui.Text("Replace party numbers with role in Party List"); 41 | SettingsWindow.ImGuiHelpTooltip( 42 | "EXPERIMENTAL. Only works when nameplates set to 'Role letters' and Party List player character names are shown in full (not abbreviated).", 43 | true); 44 | 45 | var useContextMenu = Plugin.Settings.UseContextMenu; 46 | 47 | if (ImGui.Checkbox("##useContextMenu", ref useContextMenu)) 48 | { 49 | Plugin.Settings.UseContextMenu = useContextMenu; 50 | Plugin.Settings.Save(); 51 | } 52 | 53 | ImGui.SameLine(); 54 | ImGui.Text("Add context menu commands to assign roles"); 55 | ImGuiComponents.HelpMarker("Adds context menu commands to assign roles to players. When applicable, commands to swap role and use a suggested role are also added."); 56 | 57 | var assignFromChat = Plugin.Settings.AssignFromChat; 58 | 59 | if (ImGui.Checkbox("##assignFromChat", ref assignFromChat)) 60 | { 61 | Plugin.Settings.AssignFromChat = assignFromChat; 62 | Plugin.Settings.Save(); 63 | } 64 | 65 | ImGui.SameLine(); 66 | ImGui.Text("Allow party members to self-assign roles via party chat"); 67 | ImGuiComponents.HelpMarker("Allows party members to assign themselves a role, e.g. saying 'h1' in party chat will give that player the healer 1 role."); 68 | 69 | ImGui.Dummy(new Vector2(0, 2f)); 70 | ImGui.PushStyleColor(0, ImGuiHelpers.DefaultColorPalette()[0]); 71 | ImGui.Text("Static Roles"); 72 | ImGui.PopStyleColor(); 73 | ImGui.Separator(); 74 | ImGui.Dummy(new Vector2(0, 2f)); 75 | { 76 | ImGui.PushStyleColor(0, ImGuiColors.ParsedGrey); 77 | { 78 | ImGui.TextWrapped( 79 | "Name should include world name, separated by @. Keep in mind that if players job is not appropriate for the assigned role, the assignment will be ignored!"); 80 | ImGui.Dummy(new Vector2(0f, 25f)); 81 | } 82 | ImGui.PopStyleColor(); 83 | } 84 | 85 | ImGui.SetCursorPosY(ImGui.GetCursorPos().Y - 22f); 86 | foreach (var kv in new Dictionary(Plugin.Settings.StaticAssignments)) 87 | { 88 | if (ImGui.Button("x##remove_occupation_" + kv.Key)) 89 | { 90 | Plugin.Settings.StaticAssignments.Remove(kv.Key); 91 | Plugin.Settings.Save(); 92 | 93 | continue; 94 | } 95 | 96 | ImGui.SameLine(); 97 | SettingsWindow.SetComboWidth(Enum.GetValues().Select(x => Plugin.PlayerStylesheet.GetRoleName(x))); 98 | 99 | if (ImGui.BeginCombo("##role_combo_" + kv.Key, 100 | Plugin.PlayerStylesheet.GetRoleName(Plugin.Settings.StaticAssignments[kv.Key]))) 101 | { 102 | foreach (var roleId in Enum.GetValues()) 103 | { 104 | if (ImGui.Selectable(Plugin.PlayerStylesheet.GetRoleName(roleId) + "##role_combo_option_" + kv.Key + "_" + 105 | roleId)) 106 | { 107 | Plugin.Settings.StaticAssignments[kv.Key] = roleId; 108 | Plugin.Settings.Save(); 109 | } 110 | } 111 | 112 | ImGui.EndCombo(); 113 | } 114 | 115 | ImGui.SameLine(); 116 | ImGui.Text(kv.Key); 117 | } 118 | 119 | if (ImGui.Button("+##add_occupation")) 120 | { 121 | Plugin.Settings.StaticAssignments[_occupationNewName] = _occupationNewRole; 122 | Plugin.Settings.Save(); 123 | } 124 | 125 | ImGui.SameLine(); 126 | SettingsWindow.SetComboWidth(Enum.GetValues().Select(x => Plugin.PlayerStylesheet.GetRoleName(x))); 127 | 128 | if (ImGui.BeginCombo("##new_role_combo", Plugin.PlayerStylesheet.GetRoleName(_occupationNewRole))) 129 | { 130 | foreach (var roleId in Enum.GetValues()) 131 | { 132 | if (ImGui.Selectable(Plugin.PlayerStylesheet.GetRoleName(roleId) + "##new_role_combo_option_" + "_" + roleId)) 133 | { 134 | _occupationNewRole = roleId; 135 | } 136 | } 137 | 138 | ImGui.EndCombo(); 139 | } 140 | 141 | ImGui.SameLine(); 142 | ImGui.InputText("##new_role_name", ref _occupationNewName, 64); 143 | 144 | ImGui.SetCursorPosY(ImGui.GetCursorPos().Y + 22f); 145 | 146 | } 147 | 148 | private string _occupationNewName = "Character Name@World"; 149 | private RoleId _occupationNewRole = RoleId.Undefined; 150 | } -------------------------------------------------------------------------------- /PartyIcons/Runtime/ViewModeSetter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Dalamud.Data; 4 | using Dalamud.Game.ClientState; 5 | using Dalamud.Game.Gui; 6 | using Dalamud.IoC; 7 | using Dalamud.Logging; 8 | using Lumina.Excel; 9 | using Lumina.Excel.GeneratedSheets; 10 | using PartyIcons.Configuration; 11 | using PartyIcons.View; 12 | 13 | namespace PartyIcons.Runtime; 14 | 15 | public enum ZoneType 16 | { 17 | Overworld, 18 | Dungeon, 19 | Raid, 20 | AllianceRaid, 21 | Foray, 22 | } 23 | 24 | public sealed class ViewModeSetter 25 | { 26 | /// 27 | /// Whether the player is currently in a duty. 28 | /// 29 | public bool InDuty => ZoneType != ZoneType.Overworld; 30 | 31 | public ZoneType ZoneType { get; private set; } = ZoneType.Overworld; 32 | 33 | private readonly NameplateView _nameplateView; 34 | private readonly Settings _configuration; 35 | private readonly ChatNameUpdater _chatNameUpdater; 36 | private readonly PartyListHUDUpdater _partyListHudUpdater; 37 | 38 | private ExcelSheet _contentFinderConditionsSheet; 39 | 40 | public ViewModeSetter(NameplateView nameplateView, Settings configuration, ChatNameUpdater chatNameUpdater, 41 | PartyListHUDUpdater partyListHudUpdater) 42 | { 43 | _nameplateView = nameplateView; 44 | _configuration = configuration; 45 | _chatNameUpdater = chatNameUpdater; 46 | _partyListHudUpdater = partyListHudUpdater; 47 | 48 | _configuration.OnSave += OnConfigurationSave; 49 | } 50 | 51 | private void OnConfigurationSave() 52 | { 53 | ForceRefresh(); 54 | } 55 | 56 | public void Enable() 57 | { 58 | _contentFinderConditionsSheet = Service.DataManager.GameData.GetExcelSheet() ?? throw new InvalidOperationException(); 59 | 60 | ForceRefresh(); 61 | Service.ClientState.TerritoryChanged += OnTerritoryChanged; 62 | } 63 | 64 | public void ForceRefresh() 65 | { 66 | _nameplateView.OthersMode = _configuration.NameplateOthers; 67 | _chatNameUpdater.OthersMode = _configuration.ChatOthers; 68 | 69 | OnTerritoryChanged(null, 0); 70 | } 71 | 72 | public void Disable() 73 | { 74 | Service.ClientState.TerritoryChanged -= OnTerritoryChanged; 75 | } 76 | 77 | public void Dispose() 78 | { 79 | _configuration.OnSave -= OnConfigurationSave; 80 | Disable(); 81 | } 82 | 83 | private void OnTerritoryChanged(object? sender, ushort e) 84 | { 85 | var content = 86 | _contentFinderConditionsSheet.FirstOrDefault(t => t.TerritoryType.Row == Service.ClientState.TerritoryType); 87 | 88 | if (content == null) 89 | { 90 | PluginLog.Verbose($"Content null {Service.ClientState.TerritoryType}"); 91 | _nameplateView.PartyMode = _configuration.NameplateOverworld; 92 | _chatNameUpdater.PartyMode = _configuration.ChatOverworld; 93 | ZoneType = ZoneType.Overworld; 94 | } 95 | else 96 | { 97 | if (_configuration.ChatContentMessage) 98 | { 99 | Service.ChatGui.Print($"Entering {content.Name}."); 100 | } 101 | 102 | var memberType = content.ContentMemberType.Row; 103 | 104 | if (content.RowId == 16 || content.RowId == 15) 105 | { 106 | // Praetorium and Castrum Meridianum 107 | memberType = 2; 108 | } 109 | 110 | if (content.RowId == 735 || content.RowId == 778) 111 | { 112 | // Bozja 113 | memberType = 127; 114 | } 115 | 116 | PluginLog.Verbose( 117 | $"Territory changed {content.Name} (id {content.RowId} type {content.ContentType.Row}, terr {Service.ClientState.TerritoryType}, memtype {content.ContentMemberType.Row}, overriden {memberType})"); 118 | 119 | switch (memberType) 120 | { 121 | case 2: 122 | ZoneType = ZoneType.Dungeon; 123 | _nameplateView.PartyMode = _configuration.NameplateDungeon; 124 | _nameplateView.OthersMode = _configuration.NameplateOthers; 125 | _chatNameUpdater.PartyMode = _configuration.ChatDungeon; 126 | 127 | break; 128 | 129 | case 3: 130 | ZoneType = ZoneType.Raid; 131 | _nameplateView.PartyMode = _configuration.NameplateRaid; 132 | _nameplateView.OthersMode = _configuration.NameplateOthers; 133 | _chatNameUpdater.PartyMode = _configuration.ChatRaid; 134 | 135 | break; 136 | 137 | case 4: 138 | ZoneType = ZoneType.AllianceRaid; 139 | _nameplateView.PartyMode = _configuration.NameplateAllianceRaid; 140 | _nameplateView.OthersMode = _configuration.NameplateOthers; 141 | _chatNameUpdater.PartyMode = _configuration.ChatAllianceRaid; 142 | 143 | break; 144 | 145 | case 127: 146 | ZoneType = ZoneType.Foray; 147 | _nameplateView.PartyMode = _configuration.NameplateBozjaParty; 148 | _nameplateView.OthersMode = _configuration.NameplateBozjaOthers; 149 | _chatNameUpdater.PartyMode = _configuration.ChatOverworld; 150 | 151 | break; 152 | 153 | default: 154 | ZoneType = ZoneType.Dungeon; 155 | _nameplateView.PartyMode = _configuration.NameplateDungeon; 156 | _nameplateView.OthersMode = _configuration.NameplateOthers; 157 | _chatNameUpdater.PartyMode = _configuration.ChatDungeon; 158 | 159 | break; 160 | } 161 | } 162 | 163 | _partyListHudUpdater.UpdateHUD = _nameplateView.PartyMode == NameplateMode.RoleLetters || 164 | _nameplateView.PartyMode == NameplateMode.SmallJobIconAndRole; 165 | 166 | PluginLog.Verbose($"Setting modes: nameplates party {_nameplateView.PartyMode} others {_nameplateView.OthersMode}, chat {_chatNameUpdater.PartyMode}, update HUD {_partyListHudUpdater.UpdateHUD}"); 167 | PluginLog.Debug($"Entered ZoneType {ZoneType.ToString()}"); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /PartyIcons/Runtime/ChatNameUpdater.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Dalamud.Game.ClientState.Objects.SubKinds; 4 | using Dalamud.Game.Text; 5 | using Dalamud.Game.Text.SeStringHandling; 6 | using Dalamud.Game.Text.SeStringHandling.Payloads; 7 | using Lumina.Excel.GeneratedSheets; 8 | using PartyIcons.Configuration; 9 | using PartyIcons.Stylesheet; 10 | 11 | namespace PartyIcons.Runtime; 12 | 13 | public sealed class ChatNameUpdater : IDisposable 14 | { 15 | private readonly RoleTracker _roleTracker; 16 | private readonly PlayerStylesheet _stylesheet; 17 | 18 | public ChatConfig PartyMode { get; set; } 19 | public ChatConfig OthersMode { get; set; } 20 | 21 | public ChatNameUpdater(RoleTracker roleTracker, PlayerStylesheet stylesheet) 22 | { 23 | _roleTracker = roleTracker; 24 | _stylesheet = stylesheet; 25 | } 26 | 27 | public void Enable() 28 | { 29 | Service.ChatGui.ChatMessage += OnChatMessage; 30 | } 31 | 32 | private void OnChatMessage(XivChatType type, uint senderid, ref SeString sender, ref SeString message, 33 | ref bool ishandled) 34 | { 35 | if (Service.ClientState.IsPvP) 36 | { 37 | return; 38 | } 39 | 40 | if (type == XivChatType.Say || type == XivChatType.Party || type == XivChatType.Alliance || 41 | type == XivChatType.Shout || type == XivChatType.Yell) 42 | { 43 | Parse(type, ref sender); 44 | } 45 | } 46 | 47 | public void Disable() 48 | { 49 | Service.ChatGui.ChatMessage -= OnChatMessage; 50 | } 51 | 52 | public void Dispose() 53 | { 54 | Disable(); 55 | } 56 | 57 | private PlayerPayload? GetPlayerPayload(SeString sender) 58 | { 59 | var playerPayload = sender.Payloads.FirstOrDefault(p => p is PlayerPayload) as PlayerPayload ?? null; 60 | 61 | if (playerPayload == null && Service.ClientState.LocalPlayer is { } localPlayer) 62 | { 63 | playerPayload = new PlayerPayload(localPlayer.Name.TextValue, localPlayer.HomeWorld.Id); 64 | } 65 | 66 | return playerPayload; 67 | } 68 | 69 | private bool CheckIfPlayerPayloadInParty(PlayerPayload playerPayload) 70 | { 71 | if (Plugin.Settings.TestingMode) 72 | { 73 | return true; 74 | } 75 | 76 | foreach (var member in Service.PartyList) 77 | { 78 | if (member.Name.ToString() == playerPayload.PlayerName && member.World.Id == playerPayload.World.RowId) 79 | { 80 | return true; 81 | } 82 | } 83 | 84 | return false; 85 | } 86 | 87 | private bool GetAndRemovePartyNumberPrefix(XivChatType type, SeString sender, out string prefix) 88 | { 89 | if (type == XivChatType.Party || type == XivChatType.Alliance) 90 | { 91 | var playerNamePayload = sender.Payloads.FirstOrDefault(p => p is TextPayload) as TextPayload; 92 | prefix = playerNamePayload.Text.Substring(0, 1); 93 | playerNamePayload.Text = playerNamePayload.Text.Substring(1); 94 | 95 | return true; 96 | } 97 | else 98 | { 99 | prefix = ""; 100 | 101 | return false; 102 | } 103 | } 104 | 105 | private void RemoveExistingForeground(SeString str) 106 | { 107 | str.Payloads.RemoveAll(p => p.Type == PayloadType.UIForeground); 108 | } 109 | 110 | private ClassJob? FindSenderJob(PlayerPayload playerPayload) 111 | { 112 | ClassJob? senderJob = null; 113 | 114 | foreach (var member in Service.PartyList) 115 | { 116 | if (member.Name.ToString() == playerPayload.PlayerName && member.World.Id == playerPayload.World.RowId) 117 | { 118 | senderJob = member.ClassJob.GameData; 119 | 120 | break; 121 | } 122 | } 123 | 124 | if (senderJob == null) 125 | { 126 | foreach (var obj in Service.ObjectTable) 127 | { 128 | if (obj is PlayerCharacter pc && pc.Name.ToString() == playerPayload.PlayerName && 129 | pc.HomeWorld.Id == playerPayload.World.RowId) 130 | { 131 | senderJob = pc.ClassJob.GameData; 132 | 133 | break; 134 | } 135 | } 136 | } 137 | 138 | return senderJob; 139 | } 140 | 141 | private void Parse(XivChatType chatType, ref SeString sender) 142 | { 143 | if (GetPlayerPayload(sender) is not { } playerPayload) 144 | { 145 | return; 146 | } 147 | 148 | var config = CheckIfPlayerPayloadInParty(playerPayload) ? PartyMode : OthersMode; 149 | 150 | if (config.Mode == ChatMode.Role && 151 | _roleTracker.TryGetAssignedRole(playerPayload.PlayerName, playerPayload.World.RowId, out var roleId)) 152 | { 153 | GetAndRemovePartyNumberPrefix(chatType, sender, out _); 154 | 155 | var prefixString = new SeString(); 156 | if (config.UseRoleColor) 157 | { 158 | RemoveExistingForeground(sender); 159 | prefixString.Append(new UIForegroundPayload(_stylesheet.GetRoleChatColor(roleId))); 160 | } 161 | prefixString.Append(_stylesheet.GetRoleChatPrefix(roleId)); 162 | prefixString.Append(new TextPayload(" ")); 163 | 164 | sender.Payloads.InsertRange(0, prefixString.Payloads); 165 | if (config.UseRoleColor) sender.Payloads.Add(UIForegroundPayload.UIForegroundOff); 166 | } 167 | else if (config.Mode != ChatMode.GameDefault || config.UseRoleColor) // still get in if GameDefault && Colored 168 | { 169 | var senderJob = FindSenderJob(playerPayload); 170 | 171 | if (senderJob == null || senderJob.RowId == 0) 172 | { 173 | return; 174 | } 175 | 176 | GetAndRemovePartyNumberPrefix(chatType, sender, out var numberPrefix); 177 | 178 | var prefixString = new SeString(); 179 | 180 | switch (config.Mode) 181 | { 182 | case ChatMode.Job: 183 | if (config.UseRoleColor) 184 | { 185 | RemoveExistingForeground(sender); 186 | prefixString.Append(new UIForegroundPayload(_stylesheet.GetJobChatColor(senderJob))); 187 | } 188 | 189 | if (numberPrefix.Length > 0) 190 | { 191 | prefixString.Append(new TextPayload(numberPrefix)); 192 | } 193 | 194 | prefixString.Append(_stylesheet.GetJobChatPrefix(senderJob, config.UseRoleColor).Payloads); 195 | prefixString.Append(new TextPayload(" ")); 196 | 197 | break; 198 | 199 | case ChatMode.Role: 200 | if (config.UseRoleColor) 201 | { 202 | RemoveExistingForeground(sender); 203 | prefixString.Append(new UIForegroundPayload(_stylesheet.GetGenericRoleChatColor(senderJob))); 204 | } 205 | 206 | if (numberPrefix.Length > 0) 207 | { 208 | prefixString.Append(new TextPayload(numberPrefix)); 209 | } 210 | 211 | prefixString.Append(_stylesheet.GetGenericRoleChatPrefix(senderJob, config.UseRoleColor).Payloads); 212 | prefixString.Append(new TextPayload(" ")); 213 | 214 | break; 215 | 216 | case ChatMode.GameDefault: 217 | // don't need to check Colored again 218 | RemoveExistingForeground(sender); 219 | prefixString.Append(new UIForegroundPayload(_stylesheet.GetGenericRoleChatColor(senderJob))); 220 | 221 | if (numberPrefix.Length > 0) 222 | { 223 | prefixString.Append(new TextPayload(numberPrefix)); 224 | } 225 | 226 | prefixString.Append(new TextPayload(" ")); 227 | 228 | break; 229 | 230 | default: 231 | throw new ArgumentException(); 232 | } 233 | 234 | sender.Payloads.InsertRange(0, prefixString.Payloads); 235 | if (config.UseRoleColor) sender.Payloads.Add(UIForegroundPayload.UIForegroundOff); 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /PartyIcons/Runtime/NameplateUpdater.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Dalamud.Game.ClientState; 4 | using Dalamud.Game.Text.SeStringHandling; 5 | using Dalamud.Game.Text.SeStringHandling.Payloads; 6 | using Dalamud.Hooking; 7 | using Dalamud.IoC; 8 | using Dalamud.Logging; 9 | using PartyIcons.Api; 10 | using PartyIcons.Configuration; 11 | using PartyIcons.Entities; 12 | using PartyIcons.Utils; 13 | using PartyIcons.View; 14 | 15 | namespace PartyIcons.Runtime; 16 | 17 | public sealed class NameplateUpdater : IDisposable 18 | { 19 | private readonly Settings _configuration; 20 | private readonly NameplateView _view; 21 | private readonly PluginAddressResolver _address; 22 | private readonly ViewModeSetter _modeSetter; 23 | private readonly Hook _hook; 24 | 25 | public int DebugIcon { get; set; } = -1; 26 | 27 | public NameplateUpdater(Settings configuration, PluginAddressResolver address, NameplateView view, ViewModeSetter modeSetter) 28 | { 29 | _configuration = configuration; 30 | _address = address; 31 | _view = view; 32 | _modeSetter = modeSetter; 33 | _hook = new Hook(_address.AddonNamePlate_SetNamePlatePtr, SetNamePlateDetour); 34 | } 35 | 36 | public void Enable() 37 | { 38 | _hook.Enable(); 39 | } 40 | 41 | public void Disable() 42 | { 43 | _hook.Disable(); 44 | } 45 | 46 | public void Dispose() 47 | { 48 | Disable(); 49 | _hook.Dispose(); 50 | } 51 | 52 | public IntPtr SetNamePlateDetour(IntPtr namePlateObjectPtr, bool isPrefixTitle, bool displayTitle, 53 | IntPtr title, IntPtr name, IntPtr fcName, IntPtr prefix, int iconID) 54 | // IntPtr titlePtr, IntPtr namePtr, IntPtr freeCompanyPtr, IntPtr prefixOrWhatever, int iconId 55 | { 56 | try 57 | { 58 | return SetNamePlate(namePlateObjectPtr, isPrefixTitle, displayTitle, title, name, fcName, prefix, iconID); 59 | } 60 | catch (Exception ex) 61 | { 62 | PluginLog.Error(ex, "SetNamePlateDetour encountered a critical error"); 63 | 64 | return _hook.Original(namePlateObjectPtr, isPrefixTitle, displayTitle, title, name, fcName, prefix, iconID); 65 | } 66 | } 67 | 68 | public IntPtr SetNamePlate(IntPtr namePlateObjectPtr, bool isPrefixTitle, bool displayTitle, IntPtr title, 69 | IntPtr name, IntPtr fcName, IntPtr prefix, int iconID) 70 | { 71 | if (Service.ClientState.IsPvP) 72 | { 73 | // disable in PvP 74 | return _hook.Original(namePlateObjectPtr, isPrefixTitle, displayTitle, title, name, fcName, prefix, iconID); 75 | } 76 | 77 | var originalTitle = title; 78 | var originalName = name; 79 | var originalFcName = fcName; 80 | 81 | var npObject = new XivApi.SafeNamePlateObject(namePlateObjectPtr); 82 | 83 | if (npObject == null) 84 | { 85 | _view.SetupDefault(npObject); 86 | 87 | return _hook.Original(namePlateObjectPtr, isPrefixTitle, displayTitle, title, name, fcName, prefix, iconID); 88 | } 89 | 90 | var npInfo = npObject.NamePlateInfo; 91 | 92 | if (npInfo == null) 93 | { 94 | _view.SetupDefault(npObject); 95 | 96 | return _hook.Original(namePlateObjectPtr, isPrefixTitle, displayTitle, title, name, fcName, prefix, iconID); 97 | } 98 | 99 | var actorID = npInfo.Data.ObjectID.ObjectID; 100 | 101 | if (actorID == 0xE0000000) 102 | { 103 | _view.SetupDefault(npObject); 104 | 105 | return _hook.Original(namePlateObjectPtr, isPrefixTitle, displayTitle, title, name, fcName, prefix, iconID); 106 | } 107 | 108 | if (!npObject.IsPlayer) 109 | { 110 | _view.SetupDefault(npObject); 111 | 112 | return _hook.Original(namePlateObjectPtr, isPrefixTitle, displayTitle, title, name, fcName, prefix, iconID); 113 | } 114 | 115 | var jobID = npInfo.GetJobID(); 116 | 117 | if (jobID < 1 || jobID >= Enum.GetValues(typeof(Job)).Length) 118 | { 119 | _view.SetupDefault(npObject); 120 | 121 | return _hook.Original(namePlateObjectPtr, isPrefixTitle, displayTitle, title, name, fcName, prefix, iconID); 122 | } 123 | 124 | var isPriorityIcon = IsPriorityIcon(iconID, out var priorityIconId); 125 | 126 | _view.NameplateDataForPC(npObject, ref isPrefixTitle, ref displayTitle, ref title, ref name, ref fcName, ref iconID); 127 | 128 | if (isPriorityIcon) 129 | { 130 | iconID = priorityIconId; 131 | } 132 | 133 | var result = _hook.Original(namePlateObjectPtr, isPrefixTitle, displayTitle, title, name, fcName, prefix, iconID); 134 | _view.SetupForPC(npObject, isPriorityIcon); 135 | 136 | if (originalName != name) 137 | { 138 | SeStringUtils.FreePtr(name); 139 | } 140 | 141 | if (originalTitle != title) 142 | { 143 | SeStringUtils.FreePtr(title); 144 | } 145 | 146 | if (originalFcName != fcName) 147 | { 148 | SeStringUtils.FreePtr(fcName); 149 | } 150 | 151 | return result; 152 | } 153 | 154 | /// 155 | /// Check for an icon that should take priority over the job icon, 156 | /// taking into account whether or not the player is in a duty. 157 | /// 158 | /// The incoming icon id that is being overwritten by the plugin. 159 | /// The icon id that should be used. 160 | /// Whether a priority icon was found. 161 | private bool IsPriorityIcon(int iconId, out int priorityIconId) 162 | { 163 | // PluginLog.Verbose($"Icon ID: {iconId}, Debug Icon ID: {DebugIcon}"); 164 | priorityIconId = iconId; 165 | 166 | if (_configuration.UsePriorityIcons == false && 167 | iconId != (int)Icon.Disconnecting && iconId != (int)Icon.Disconnecting + 50) 168 | { 169 | return false; 170 | } 171 | 172 | // Select which set of priority icons to use based on whether we're in a duty 173 | // In the future, there can be a third list used when in combat 174 | var priorityIcons = GetPriorityIcons(); 175 | 176 | // Determine whether the incoming icon should take priority over the job icon 177 | // Check the id plus 50 as that's an alternately sized version 178 | bool isPriorityIcon = priorityIcons.Contains(iconId) || priorityIcons.Contains(iconId + 50); 179 | 180 | // Save the id of the icon 181 | priorityIconId = iconId; 182 | 183 | // If an icon was set with the plugin's debug command, always use that 184 | if (DebugIcon >= 0) 185 | { 186 | isPriorityIcon = true; 187 | priorityIconId = DebugIcon; 188 | PluginLog.Verbose($"Setting debug icon. Id: {DebugIcon}"); 189 | 190 | DebugIcon++; 191 | } 192 | 193 | return isPriorityIcon; 194 | } 195 | 196 | private int[] GetPriorityIcons() 197 | { 198 | if (_modeSetter.ZoneType == ZoneType.Foray) 199 | { 200 | return priorityIconsInForay; 201 | } 202 | 203 | if (_modeSetter.InDuty) 204 | { 205 | return priorityIconsInDuty; 206 | } 207 | 208 | return priorityIconsOverworld; 209 | } 210 | 211 | public enum Icon 212 | { 213 | Disconnecting = 061503, 214 | } 215 | 216 | // This could be done as a range but 217 | private static readonly int[] priorityIconsOverworld = 218 | { 219 | 061503, // Disconnecting 220 | 061506, // In Duty 221 | 061508, // Viewing Cutscene 222 | 061509, // Busy 223 | 061511, // Idle 224 | 061514, // Looking for meld 225 | 061515, // Looking for party 226 | 061517, // Duty Finder 227 | 061521, // Party Leader 228 | 061522, // Party Member 229 | 061524, // Game Master 230 | 061532, // Game Master 231 | 061533, // Event Participant 232 | 061545, // Role Playing 233 | 061546, // Group Pose 234 | }; 235 | 236 | private static readonly int[] priorityIconsInDuty = 237 | { 238 | 061503, // Disconnecting 239 | 061508, // Viewing Cutscene 240 | 061511, // Idle 241 | 061546, // Group Pose 242 | }; 243 | 244 | private static readonly int[] priorityIconsInForay = 245 | { 246 | // This allows you to see which players don't have a party 247 | 061506, // In Duty 248 | 249 | 061503, // Disconnecting 250 | 061508, // Viewing Cutscene 251 | 061511, // Idle 252 | 061546, // Group Pose 253 | }; 254 | } 255 | -------------------------------------------------------------------------------- /PartyIcons/Utils/PartyListHUDView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using Dalamud.Game.ClientState.Party; 4 | using Dalamud.Game.Gui; 5 | using Dalamud.IoC; 6 | using Dalamud.Logging; 7 | using FFXIVClientStructs.FFXIV.Client.UI; 8 | using FFXIVClientStructs.FFXIV.Client.UI.Agent; 9 | using PartyIcons.Entities; 10 | using PartyIcons.Stylesheet; 11 | 12 | namespace PartyIcons.Utils; 13 | 14 | public unsafe class PartyListHUDView : IDisposable 15 | { 16 | // [PluginService] 17 | // public PartyList PartyList { get; set; } 18 | 19 | private readonly PlayerStylesheet _stylesheet; 20 | private readonly GameGui _gameGui; 21 | 22 | public PartyListHUDView(GameGui gameGui, PlayerStylesheet stylesheet) 23 | { 24 | _gameGui = gameGui; 25 | _stylesheet = stylesheet; 26 | } 27 | 28 | public void Dispose() 29 | { 30 | RevertSlotNumbers(); 31 | } 32 | 33 | public void RevertSlotNumbers() 34 | { 35 | for (uint i = 0; i < 8; i++) 36 | { 37 | var memberStructOptional = GetPartyMemberStruct(i); 38 | 39 | if (!memberStructOptional.HasValue) 40 | { 41 | PluginLog.Warning($"Failed to dispose member HUD changes - struct null!"); 42 | 43 | continue; 44 | } 45 | 46 | var memberStruct = memberStructOptional.Value; 47 | var nameNode = memberStruct.Name; 48 | nameNode->AtkResNode.SetPositionShort(19, 0); 49 | 50 | var numberNode = nameNode->AtkResNode.PrevSiblingNode->GetAsAtkTextNode(); 51 | numberNode->AtkResNode.SetPositionShort(0, 0); 52 | numberNode->SetText(_stylesheet.BoxedCharacterString((i + 1).ToString())); 53 | } 54 | } 55 | 56 | public uint? GetPartySlotIndex(uint objectId) 57 | { 58 | var hud = 59 | FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance()->GetUiModule()->GetAgentModule()-> 60 | GetAgentHUD(); 61 | 62 | if (hud == null) 63 | { 64 | PluginLog.Warning("AgentHUD null!"); 65 | 66 | return null; 67 | } 68 | 69 | // 9 instead of 8 is used here, in case the player has a pet out 70 | if (hud->PartyMemberCount > 9) 71 | { 72 | // hud->PartyMemberCount gives out special (?) value when in trust 73 | PluginLog.Verbose("GetPartySlotIndex - trust detected, returning null"); 74 | 75 | return null; 76 | } 77 | 78 | var list = (HudPartyMember*) hud->PartyMemberList; 79 | 80 | for (var i = 0; i < hud->PartyMemberCount; i++) 81 | { 82 | if (list[i].ObjectId == objectId) 83 | { 84 | return (uint) i; 85 | } 86 | } 87 | 88 | return null; 89 | } 90 | 91 | public void SetPartyMemberRole(string name, uint objectId, RoleId roleId) 92 | { 93 | var index = GetPartySlotIndex(objectId); 94 | 95 | for (uint i = 0; i < 8; i++) 96 | { 97 | var memberStruct = GetPartyMemberStruct(i); 98 | 99 | if (memberStruct.HasValue) 100 | { 101 | var nameString = memberStruct.Value.Name->NodeText.ToString(); 102 | var strippedName = StripSpecialCharactersFromName(nameString); 103 | 104 | if (name.Contains(strippedName)) 105 | { 106 | if (!index.HasValue || index.Value != i) 107 | { 108 | PluginLog.Warning("PartyHUD and HUDAgent id's mismatch!"); 109 | // PluginLog.Warning(GetDebugInfo()); 110 | } 111 | 112 | SetPartyMemberRole(i, roleId); 113 | 114 | return; 115 | } 116 | } 117 | } 118 | 119 | PluginLog.Verbose($"Member struct by the name {name} not found."); 120 | } 121 | 122 | public void SetPartyMemberRole(uint index, RoleId roleId) 123 | { 124 | var memberStructOptional = GetPartyMemberStruct(index); 125 | 126 | if (!memberStructOptional.HasValue) 127 | { 128 | PluginLog.Warning($"Failed to set party member HUD role to {roleId} - struct null!"); 129 | 130 | return; 131 | } 132 | 133 | var memberStruct = memberStructOptional.Value; 134 | 135 | var nameNode = memberStruct.Name; 136 | nameNode->AtkResNode.SetPositionShort(29, 0); 137 | 138 | var numberNode = nameNode->AtkResNode.PrevSiblingNode->GetAsAtkTextNode(); 139 | numberNode->AtkResNode.SetPositionShort(6, 0); 140 | 141 | var seString = _stylesheet.GetRolePlate(roleId); 142 | var buf = seString.Encode(); 143 | 144 | fixed (byte* ptr = buf) 145 | { 146 | numberNode->SetText(ptr); 147 | } 148 | } 149 | 150 | public string GetDebugInfo() 151 | { 152 | var hud = 153 | FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance()->GetUiModule()->GetAgentModule()-> 154 | GetAgentHUD(); 155 | 156 | if (hud == null) 157 | { 158 | PluginLog.Warning("AgentHUD null!"); 159 | 160 | return null; 161 | } 162 | 163 | if (hud->PartyMemberCount > 9) 164 | { 165 | // hud->PartyMemberCount gives out special (?) value when in trust 166 | PluginLog.Verbose("GetPartySlotIndex - trust detected, returning null"); 167 | 168 | return null; 169 | } 170 | 171 | var result = new StringBuilder(); 172 | result.AppendLine($"PARTY ({Service.PartyList.Length}):"); 173 | 174 | foreach (var member in Service.PartyList) 175 | { 176 | var index = GetPartySlotIndex(member.ObjectId); 177 | result.AppendLine( 178 | $"PartyList name {member.Name} oid {member.ObjectId} worldid {member.World.Id} slot index {index}"); 179 | } 180 | 181 | result.AppendLine("STRUCTS:"); 182 | var memberList = (HudPartyMember*) hud->PartyMemberList; 183 | 184 | for (var i = 0; i < Math.Min(hud->PartyMemberCount, 8u); i++) 185 | { 186 | var memberStruct = GetPartyMemberStruct((uint) i); 187 | 188 | if (memberStruct.HasValue) 189 | { 190 | /* 191 | for (var pi = 0; pi < memberStruct.Value.ClassJobIcon->PartsList->PartCount; pi++) 192 | { 193 | var part = memberStruct.Value.ClassJobIcon->PartsList->Parts[pi]; 194 | result.Append($"icon {part.UldAsset->AtkTexture.Resource->TexFileResourceHandle->ResourceHandle.FileName}"); 195 | } 196 | */ 197 | 198 | var strippedName = StripSpecialCharactersFromName(memberStruct.Value.Name->NodeText.ToString()); 199 | result.AppendLine( 200 | $"PartyMemberStruct index {i} name '{strippedName}', id matched {memberList[i].ObjectId}"); 201 | 202 | var byteCount = 0; 203 | 204 | while (byteCount < 16 && memberList[i].Name[byteCount++] != 0) { } 205 | 206 | var memberListName = Encoding.UTF8.GetString(memberList[i].Name, byteCount - 1); 207 | result.AppendLine($"HudPartyMember index {i} name {memberListName} {memberList[i].ObjectId}"); 208 | } 209 | else 210 | { 211 | result.AppendLine($"PartyMemberStruct null at {i}"); 212 | } 213 | } 214 | 215 | return result.ToString(); 216 | } 217 | 218 | private AddonPartyList.PartyListMemberStruct? GetPartyMemberStruct(uint idx) 219 | { 220 | var partyListAddon = (AddonPartyList*) _gameGui.GetAddonByName("_PartyList", 1); 221 | 222 | if (partyListAddon == null) 223 | { 224 | PluginLog.Warning("PartyListAddon null!"); 225 | 226 | return null; 227 | } 228 | 229 | return idx switch 230 | { 231 | 0 => partyListAddon->PartyMember.PartyMember0, 232 | 1 => partyListAddon->PartyMember.PartyMember1, 233 | 2 => partyListAddon->PartyMember.PartyMember2, 234 | 3 => partyListAddon->PartyMember.PartyMember3, 235 | 4 => partyListAddon->PartyMember.PartyMember4, 236 | 5 => partyListAddon->PartyMember.PartyMember5, 237 | 6 => partyListAddon->PartyMember.PartyMember6, 238 | 7 => partyListAddon->PartyMember.PartyMember7, 239 | _ => throw new ArgumentException($"Invalid index: {idx}") 240 | }; 241 | } 242 | 243 | private string StripSpecialCharactersFromName(string name) 244 | { 245 | var result = new StringBuilder(); 246 | 247 | for (var i = 0; i < name.Length; i++) 248 | { 249 | var ch = name[i]; 250 | 251 | if (ch >= 65 && ch <= 90 || ch >= 97 && ch <= 122 || ch == 45 || ch == 32 || ch == 39) 252 | { 253 | result.Append(name[i]); 254 | } 255 | } 256 | 257 | return result.ToString().Trim(); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /PartyIcons/UI/NameplateSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Numerics; 6 | using System.Reflection; 7 | using Dalamud.Interface; 8 | using Dalamud.Logging; 9 | using ImGuiNET; 10 | using ImGuiScene; 11 | using PartyIcons.Configuration; 12 | 13 | namespace PartyIcons.UI; 14 | 15 | public sealed class NameplateSettings 16 | { 17 | public NameplateSettings() 18 | { 19 | _nameplateExamples = new Dictionary(); 20 | } 21 | 22 | public void Initialize() 23 | { 24 | var assembly = Assembly.GetExecutingAssembly(); 25 | var examplesImageNames = new Dictionary 26 | { 27 | {NameplateMode.SmallJobIcon, "PartyIcons.Resources.1.png"}, 28 | {NameplateMode.BigJobIcon, "PartyIcons.Resources.2.png"}, 29 | {NameplateMode.BigJobIconAndPartySlot, "PartyIcons.Resources.3.png"}, 30 | {NameplateMode.RoleLetters, "PartyIcons.Resources.4.png"} 31 | }; 32 | 33 | foreach (var kv in examplesImageNames) 34 | { 35 | using var fileStream = assembly.GetManifestResourceStream(kv.Value); 36 | 37 | if (fileStream == null) 38 | { 39 | PluginLog.Error($"Failed to get resource stream for {kv.Value}"); 40 | 41 | continue; 42 | } 43 | 44 | using var memoryStream = new MemoryStream(); 45 | fileStream.CopyTo(memoryStream); 46 | 47 | _nameplateExamples[kv.Key] = Service.PluginInterface.UiBuilder.LoadImage(memoryStream.ToArray()); 48 | } 49 | } 50 | 51 | public void DrawNameplateSettings() 52 | { 53 | const float separatorPadding = 2f; 54 | 55 | ImGui.Dummy(new Vector2(0, 1f)); 56 | ImGui.TextDisabled("Please note, it usually takes time for nameplates to reload."); 57 | ImGui.Dummy(new Vector2(0, 10f)); 58 | 59 | var iconSetId = Plugin.Settings.IconSetId; 60 | ImGui.Text("Icon set:"); 61 | ImGui.SameLine(); 62 | SettingsWindow.SetComboWidth(Enum.GetValues().Select(IconSetIdToString)); 63 | 64 | if (ImGui.BeginCombo("##icon_set", IconSetIdToString(iconSetId))) 65 | { 66 | foreach (var id in Enum.GetValues()) 67 | { 68 | if (ImGui.Selectable(IconSetIdToString(id) + "##icon_set_" + id)) 69 | { 70 | Plugin.Settings.IconSetId = id; 71 | Plugin.Settings.Save(); 72 | } 73 | } 74 | 75 | ImGui.EndCombo(); 76 | } 77 | 78 | var iconSizeMode = Plugin.Settings.SizeMode; 79 | ImGui.Text("Nameplate size:"); 80 | ImGui.SameLine(); 81 | SettingsWindow.SetComboWidth(Enum.GetValues().Select(x => x.ToString())); 82 | 83 | if (ImGui.BeginCombo("##icon_size", iconSizeMode.ToString())) 84 | { 85 | foreach (var mode in Enum.GetValues()) 86 | { 87 | if (ImGui.Selectable(mode + "##icon_set_" + mode)) 88 | { 89 | Plugin.Settings.SizeMode = mode; 90 | Plugin.Settings.Save(); 91 | } 92 | } 93 | 94 | ImGui.EndCombo(); 95 | } 96 | 97 | SettingsWindow.ImGuiHelpTooltip("Affects all presets, except Game Default and Small Job Icon."); 98 | 99 | var hideLocalNameplate = Plugin.Settings.HideLocalPlayerNameplate; 100 | 101 | if (ImGui.Checkbox("##hidelocal", ref hideLocalNameplate)) 102 | { 103 | Plugin.Settings.HideLocalPlayerNameplate = hideLocalNameplate; 104 | Plugin.Settings.Save(); 105 | } 106 | 107 | ImGui.SameLine(); 108 | ImGui.Text("Hide own nameplate"); 109 | SettingsWindow.ImGuiHelpTooltip( 110 | "You can turn your own nameplate on and also turn this\nsetting own to only use nameplate to display own raid position.\nIf you don't want your position displayed with this setting you can simply disable\nyour nameplates in the Character settings."); 111 | 112 | ImGui.Dummy(new Vector2(0f, 10f)); 113 | 114 | ImGui.PushStyleColor(0, ImGuiHelpers.DefaultColorPalette()[0]); 115 | ImGui.Text("Overworld"); 116 | ImGui.PopStyleColor(); 117 | ImGui.Separator(); 118 | ImGui.Dummy(new Vector2(0, separatorPadding)); 119 | ImGui.Indent(15 * ImGuiHelpers.GlobalScale); 120 | { 121 | NameplateModeSection("##np_overworld", () => Plugin.Settings.NameplateOverworld, 122 | (mode) => Plugin.Settings.NameplateOverworld = mode, 123 | "Party:"); 124 | 125 | NameplateModeSection("##np_others", () => Plugin.Settings.NameplateOthers, 126 | (mode) => Plugin.Settings.NameplateOthers = mode, 127 | "Others:"); 128 | } 129 | ImGui.Indent(-15 * ImGuiHelpers.GlobalScale); 130 | ImGui.Dummy(new Vector2(0, 2f)); 131 | 132 | ImGui.PushStyleColor(0, ImGuiHelpers.DefaultColorPalette()[0]); 133 | ImGui.Text("Instances"); 134 | ImGui.PopStyleColor(); 135 | ImGui.Separator(); 136 | ImGui.Dummy(new Vector2(0, separatorPadding)); 137 | ImGui.Indent(15 * ImGuiHelpers.GlobalScale); 138 | { 139 | NameplateModeSection("##np_dungeon", () => Plugin.Settings.NameplateDungeon, 140 | (mode) => Plugin.Settings.NameplateDungeon = mode, 141 | "Dungeon:"); 142 | 143 | NameplateModeSection("##np_raid", () => Plugin.Settings.NameplateRaid, 144 | (mode) => Plugin.Settings.NameplateRaid = mode, 145 | "Raid:"); 146 | 147 | NameplateModeSection("##np_alliance", () => Plugin.Settings.NameplateAllianceRaid, 148 | (mode) => Plugin.Settings.NameplateAllianceRaid = mode, 149 | "Alliance:"); 150 | } 151 | ImGui.Indent(-15 * ImGuiHelpers.GlobalScale); 152 | ImGui.Dummy(new Vector2(0, 2f)); 153 | 154 | ImGui.PushStyleColor(0, ImGuiHelpers.DefaultColorPalette()[0]); 155 | ImGui.Text("Forays"); 156 | ImGui.PopStyleColor(); 157 | ImGui.Separator(); 158 | ImGui.Dummy(new Vector2(0, separatorPadding)); 159 | ImGui.Indent(15 * ImGuiHelpers.GlobalScale); 160 | { 161 | ImGui.TextDisabled("e.g. Eureka, Bozja"); 162 | 163 | NameplateModeSection("##np_bozja_party", () => Plugin.Settings.NameplateBozjaParty, 164 | mode => Plugin.Settings.NameplateBozjaParty = mode, "Party:"); 165 | 166 | NameplateModeSection("##np_bozja_others", () => Plugin.Settings.NameplateBozjaOthers, 167 | mode => Plugin.Settings.NameplateBozjaOthers = mode, "Others:"); 168 | } 169 | ImGui.Indent(-15 * ImGuiHelpers.GlobalScale); 170 | ImGui.Dummy(new Vector2(0, 2f)); 171 | 172 | ImGui.PushStyleColor(0, ImGuiHelpers.DefaultColorPalette()[0]); 173 | ImGui.Text("PvP"); 174 | ImGui.PopStyleColor(); 175 | ImGui.Separator(); 176 | ImGui.Dummy(new Vector2(0, 2f)); 177 | ImGui.Indent(15 * ImGuiHelpers.GlobalScale); 178 | { 179 | ImGui.TextDisabled("This plugin is intentionally disabled during PvP matches."); 180 | } 181 | ImGui.Indent(-15 * ImGuiHelpers.GlobalScale); 182 | ImGui.Dummy(new Vector2(0, 10f)); 183 | 184 | ImGui.Dummy(new Vector2(0, 10f)); 185 | 186 | if (ImGui.CollapsingHeader("Examples")) 187 | { 188 | foreach (var kv in _nameplateExamples) 189 | { 190 | CollapsibleExampleImage(kv.Key, kv.Value); 191 | } 192 | } 193 | } 194 | 195 | private void CollapsibleExampleImage(NameplateMode mode, TextureWrap tex) 196 | { 197 | if (ImGui.CollapsingHeader(NameplateModeToString(mode))) 198 | { 199 | ImGui.Image(tex.ImGuiHandle, new Vector2(tex.Width, tex.Height)); 200 | } 201 | } 202 | 203 | private static string IconSetIdToString(IconSetId id) 204 | { 205 | return id switch 206 | { 207 | IconSetId.Framed => "Framed, role colored", 208 | IconSetId.GlowingColored => "Glowing, role colored", 209 | IconSetId.GlowingGold => "Glowing, gold" 210 | }; 211 | } 212 | 213 | private static string NameplateModeToString(NameplateMode mode) 214 | { 215 | return mode switch 216 | { 217 | NameplateMode.Default => "Game default", 218 | NameplateMode.Hide => "Hide", 219 | NameplateMode.BigJobIcon => "Big job icon", 220 | NameplateMode.SmallJobIcon => "Small job icon and name", 221 | NameplateMode.SmallJobIconAndRole => "Small job icon, role and name", 222 | NameplateMode.BigJobIconAndPartySlot => "Big job icon and party number", 223 | NameplateMode.RoleLetters => "Role letters", 224 | _ => throw new ArgumentException() 225 | }; 226 | } 227 | 228 | private static void NameplateModeSection(string label, Func getter, Action setter, string title = "Nameplate: ") 229 | { 230 | ImGui.SetCursorPosY(ImGui.GetCursorPos().Y + 3f); 231 | ImGui.Text(title); 232 | ImGui.SameLine(100f); 233 | ImGui.SetCursorPosY(ImGui.GetCursorPos().Y - 3f); 234 | SettingsWindow.SetComboWidth(Enum.GetValues().Select(x => x.ToString())); 235 | 236 | // hack to fix incorrect configurations 237 | try 238 | { 239 | getter(); 240 | } 241 | catch (ArgumentException ex) 242 | { 243 | setter(NameplateMode.Default); 244 | Plugin.Settings.Save(); 245 | } 246 | 247 | if (ImGui.BeginCombo(label, NameplateModeToString(getter()))) 248 | { 249 | foreach (var mode in Enum.GetValues()) 250 | { 251 | if (ImGui.Selectable(NameplateModeToString(mode), mode == getter())) 252 | { 253 | setter(mode); 254 | Plugin.Settings.Save(); 255 | } 256 | } 257 | 258 | ImGui.EndCombo(); 259 | } 260 | } 261 | 262 | private readonly Dictionary _nameplateExamples; 263 | } -------------------------------------------------------------------------------- /PartyIcons/Stylesheet/PlayerStylesheet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using Dalamud.Game.Text.SeStringHandling; 4 | using Dalamud.Game.Text.SeStringHandling.Payloads; 5 | using Lumina.Excel.GeneratedSheets; 6 | using PartyIcons.Configuration; 7 | using PartyIcons.Entities; 8 | using PartyIcons.Utils; 9 | using PartyIcons.View; 10 | 11 | namespace PartyIcons.Stylesheet; 12 | 13 | public sealed class PlayerStylesheet 14 | { 15 | private readonly Settings _configuration; 16 | private ushort _fallbackColor = 1; 17 | 18 | public PlayerStylesheet(Settings configuration) 19 | { 20 | _configuration = configuration; 21 | } 22 | 23 | public ushort GetGenericRoleColor(GenericRole role) 24 | { 25 | switch (role) 26 | { 27 | case GenericRole.Tank: 28 | return 37; 29 | 30 | case GenericRole.Melee: 31 | return 524; 32 | 33 | case GenericRole.Ranged: 34 | return 32; 35 | 36 | case GenericRole.Healer: 37 | return 42; 38 | 39 | default: 40 | return _fallbackColor; 41 | } 42 | } 43 | 44 | public ushort GetRoleColor(RoleId roleId) 45 | { 46 | switch (roleId) 47 | { 48 | case RoleId.MT: 49 | case RoleId.OT: 50 | return GetGenericRoleColor(GenericRole.Tank); 51 | 52 | case RoleId.M1: 53 | case RoleId.M2: 54 | return GetGenericRoleColor(GenericRole.Melee); 55 | 56 | case RoleId.R1: 57 | case RoleId.R2: 58 | return GetGenericRoleColor(GenericRole.Ranged); 59 | 60 | case RoleId.H1: 61 | case RoleId.H2: 62 | return GetGenericRoleColor(GenericRole.Healer); 63 | 64 | default: 65 | return _fallbackColor; 66 | } 67 | } 68 | 69 | public string GetRoleIconset(RoleId roleId) 70 | { 71 | switch (_configuration.IconSetId) 72 | { 73 | case IconSetId.Framed: 74 | return "Framed"; 75 | 76 | case IconSetId.GlowingGold: 77 | return "Glowing"; 78 | 79 | case IconSetId.GlowingColored: 80 | return roleId switch 81 | { 82 | RoleId.MT => "Blue", 83 | RoleId.OT => "Blue", 84 | RoleId.M1 => "Red", 85 | RoleId.M2 => "Red", 86 | RoleId.R1 => "Orange", 87 | RoleId.R2 => "Orange", 88 | RoleId.H1 => "Green", 89 | RoleId.H2 => "Green", 90 | _ => "Grey" 91 | }; 92 | 93 | default: 94 | throw new ArgumentException($"Unknown icon set id: {_configuration.IconSetId}"); 95 | } 96 | } 97 | 98 | public string GetGenericRoleIconset(GenericRole role) 99 | { 100 | switch (_configuration.IconSetId) 101 | { 102 | case IconSetId.Framed: 103 | return "Framed"; 104 | 105 | case IconSetId.GlowingGold: 106 | return "Glowing"; 107 | 108 | case IconSetId.GlowingColored: 109 | return role switch 110 | { 111 | GenericRole.Tank => "Blue", 112 | GenericRole.Melee => "Red", 113 | GenericRole.Ranged => "Orange", 114 | GenericRole.Healer => "Green", 115 | _ => "Grey" 116 | }; 117 | 118 | default: 119 | throw new ArgumentException($"Unknown icon set id: {_configuration.IconSetId}"); 120 | } 121 | } 122 | 123 | public string GetRoleName(RoleId roleId) 124 | { 125 | return roleId switch 126 | { 127 | RoleId.MT => "MT", 128 | RoleId.OT => _configuration.EasternNamingConvention ? "ST" : "OT", 129 | RoleId.M1 => _configuration.EasternNamingConvention ? "D1" : "M1", 130 | RoleId.M2 => _configuration.EasternNamingConvention ? "D2" : "M2", 131 | RoleId.R1 => _configuration.EasternNamingConvention ? "D3" : "R1", 132 | RoleId.R2 => _configuration.EasternNamingConvention ? "D4" : "R2", 133 | _ => roleId.ToString() 134 | }; 135 | } 136 | 137 | public SeString GetGenericRolePlate(GenericRole genericRole) 138 | { 139 | return GetGenericRolePlate(genericRole, true); 140 | } 141 | public SeString GetGenericRolePlate(GenericRole genericRole, bool colored) 142 | { 143 | return colored ? 144 | genericRole switch 145 | { 146 | GenericRole.Tank => SeStringUtils.Text(BoxedCharacterString("T"), GetGenericRoleColor(genericRole)), 147 | GenericRole.Melee => SeStringUtils.Text( 148 | BoxedCharacterString(_configuration.EasternNamingConvention ? "D" : "M"), 149 | GetGenericRoleColor(genericRole)), 150 | GenericRole.Ranged => SeStringUtils.Text( 151 | BoxedCharacterString(_configuration.EasternNamingConvention ? "D" : "R"), 152 | GetGenericRoleColor(genericRole)), 153 | GenericRole.Healer => SeStringUtils.Text(BoxedCharacterString("H"), 154 | GetGenericRoleColor(genericRole)), 155 | _ => "" 156 | } : 157 | genericRole switch 158 | { 159 | GenericRole.Tank => SeStringUtils.Text(BoxedCharacterString("T")), 160 | GenericRole.Melee => SeStringUtils.Text( 161 | BoxedCharacterString(_configuration.EasternNamingConvention ? "D" : "M")), 162 | GenericRole.Ranged => SeStringUtils.Text( 163 | BoxedCharacterString(_configuration.EasternNamingConvention ? "D" : "R")), 164 | GenericRole.Healer => SeStringUtils.Text(BoxedCharacterString("H")), 165 | _ => "" 166 | }; 167 | } 168 | 169 | public SeString GetRolePlate(RoleId roleId) 170 | { 171 | switch (roleId) 172 | { 173 | case RoleId.MT: 174 | return SeStringUtils.Text(BoxedCharacterString("MT"), GetRoleColor(roleId)); 175 | 176 | case RoleId.OT: 177 | return SeStringUtils.Text( 178 | BoxedCharacterString(_configuration.EasternNamingConvention ? "ST" : "OT"), 179 | GetRoleColor(roleId)); 180 | 181 | case RoleId.M1: 182 | case RoleId.M2: 183 | return SeStringUtils.Text( 184 | BoxedCharacterString(_configuration.EasternNamingConvention ? "D" : "M") + 185 | GetRolePlateNumber(roleId), GetRoleColor(roleId)); 186 | 187 | case RoleId.R1: 188 | case RoleId.R2: 189 | return SeStringUtils.Text( 190 | BoxedCharacterString(_configuration.EasternNamingConvention ? "D" : "R") + 191 | GetRolePlateNumber(roleId), GetRoleColor(roleId)); 192 | 193 | case RoleId.H1: 194 | case RoleId.H2: 195 | return SeStringUtils.Text(BoxedCharacterString("H") + GetRolePlateNumber(roleId), 196 | GetRoleColor(roleId)); 197 | 198 | default: 199 | return string.Empty; 200 | } 201 | } 202 | 203 | public SeString GetRolePlateNumber(RoleId roleId) 204 | { 205 | if (_configuration.EasternNamingConvention) 206 | { 207 | return roleId switch 208 | { 209 | RoleId.MT => SeStringUtils.Text(BoxedCharacterString("1"), GetRoleColor(roleId)), 210 | RoleId.OT => SeStringUtils.Text(BoxedCharacterString("2"), GetRoleColor(roleId)), 211 | RoleId.H1 => SeStringUtils.Text(BoxedCharacterString("1"), GetRoleColor(roleId)), 212 | RoleId.H2 => SeStringUtils.Text(BoxedCharacterString("2"), GetRoleColor(roleId)), 213 | RoleId.M1 => SeStringUtils.Text(BoxedCharacterString("1"), GetRoleColor(roleId)), 214 | RoleId.M2 => SeStringUtils.Text(BoxedCharacterString("2"), GetRoleColor(roleId)), 215 | RoleId.R1 => SeStringUtils.Text(BoxedCharacterString("3"), GetRoleColor(roleId)), 216 | RoleId.R2 => SeStringUtils.Text(BoxedCharacterString("4"), GetRoleColor(roleId)) 217 | }; 218 | } 219 | else 220 | { 221 | return roleId switch 222 | { 223 | RoleId.MT => SeStringUtils.Text(BoxedCharacterString("1"), GetRoleColor(roleId)), 224 | RoleId.OT => SeStringUtils.Text(BoxedCharacterString("2"), GetRoleColor(roleId)), 225 | RoleId.H1 => SeStringUtils.Text(BoxedCharacterString("1"), GetRoleColor(roleId)), 226 | RoleId.H2 => SeStringUtils.Text(BoxedCharacterString("2"), GetRoleColor(roleId)), 227 | RoleId.M1 => SeStringUtils.Text(BoxedCharacterString("1"), GetRoleColor(roleId)), 228 | RoleId.M2 => SeStringUtils.Text(BoxedCharacterString("2"), GetRoleColor(roleId)), 229 | RoleId.R1 => SeStringUtils.Text(BoxedCharacterString("1"), GetRoleColor(roleId)), 230 | RoleId.R2 => SeStringUtils.Text(BoxedCharacterString("2"), GetRoleColor(roleId)) 231 | }; 232 | } 233 | } 234 | 235 | public SeString GetPartySlotNumber(uint number, GenericRole genericRole) => 236 | SeStringUtils.Text(BoxedCharacterString(number.ToString()), GetGenericRoleColor(genericRole)); 237 | 238 | public SeString GetRoleChatPrefix(RoleId roleId) => GetRolePlate(roleId); 239 | 240 | public ushort GetRoleChatColor(RoleId roleId) => GetRoleColor(roleId); 241 | 242 | public SeString GetGenericRoleChatPrefix(ClassJob classJob, bool colored) => 243 | GetGenericRolePlate(JobExtensions.GetRole((Job) classJob.RowId), colored); 244 | 245 | public ushort GetGenericRoleChatColor(ClassJob classJob) => 246 | GetGenericRoleColor(JobExtensions.GetRole((Job) classJob.RowId)); 247 | 248 | 249 | public SeString GetJobChatPrefix(ClassJob classJob) 250 | { 251 | return GetJobChatPrefix(classJob, true); 252 | } 253 | public SeString GetJobChatPrefix(ClassJob classJob, bool colored) 254 | { 255 | if (true) 256 | { 257 | return colored ? 258 | new SeString( 259 | new UIGlowPayload(GetGenericRoleChatColor(classJob)), 260 | new UIForegroundPayload(GetGenericRoleChatColor(classJob)), 261 | new TextPayload(classJob.Abbreviation), 262 | UIForegroundPayload.UIForegroundOff, 263 | UIGlowPayload.UIGlowOff 264 | ) : 265 | new SeString( 266 | new TextPayload(classJob.Abbreviation) 267 | ); 268 | } 269 | } 270 | 271 | public ushort GetJobChatColor(ClassJob classJob) => 272 | GetGenericRoleColor(JobExtensions.GetRole((Job) classJob.RowId)); 273 | 274 | public string BoxedCharacterString(string str) 275 | { 276 | var builder = new StringBuilder(str.Length); 277 | 278 | foreach (var ch in str.ToLower()) 279 | { 280 | builder.Append(ch switch 281 | { 282 | _ when ch >= 'a' && ch <= 'z' => (char) (ch + 57360), 283 | _ when ch >= '0' && ch <= '9' => (char) (ch + 57439), 284 | 285 | _ => ch 286 | }); 287 | } 288 | 289 | return builder.ToString(); 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /PartyIcons/View/NameplateView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Numerics; 4 | using Dalamud.Game.ClientState; 5 | using Dalamud.Game.ClientState.Objects; 6 | using Dalamud.Game.ClientState.Objects.SubKinds; 7 | using Dalamud.Game.Text.SeStringHandling; 8 | using Dalamud.Game.Text.SeStringHandling.Payloads; 9 | using Dalamud.IoC; 10 | using Dalamud.Logging; 11 | using PartyIcons.Api; 12 | using PartyIcons.Configuration; 13 | using PartyIcons.Entities; 14 | using PartyIcons.Runtime; 15 | using PartyIcons.Stylesheet; 16 | using PartyIcons.Utils; 17 | 18 | namespace PartyIcons.View; 19 | 20 | public sealed class NameplateView : IDisposable 21 | { 22 | // [PluginService] 23 | // private ObjectTable ObjectTable { get; set; } 24 | 25 | private readonly Settings _configuration; 26 | private readonly PlayerStylesheet _stylesheet; 27 | private readonly RoleTracker _roleTracker; 28 | private readonly PartyListHUDView _partyListHudView; 29 | 30 | private readonly IconSet _iconSet; 31 | 32 | public NameplateMode PartyMode { get; set; } 33 | public NameplateMode OthersMode { get; set; } 34 | 35 | public NameplateView(RoleTracker roleTracker, Settings configuration, PlayerStylesheet stylesheet, 36 | PartyListHUDView partyListHudView) 37 | { 38 | _roleTracker = roleTracker; 39 | _configuration = configuration; 40 | _stylesheet = stylesheet; 41 | _partyListHudView = partyListHudView; 42 | _iconSet = new IconSet(); 43 | } 44 | 45 | public void Dispose() { } 46 | 47 | /// True if the icon or name scale was changed (different from default). 48 | public bool SetupDefault(XivApi.SafeNamePlateObject npObject) 49 | { 50 | return npObject.SetIconScale(1f) && 51 | npObject.SetNameScale(0.5f); 52 | } 53 | 54 | /// 55 | /// Position and scale nameplate elements based on the current mode. 56 | /// 57 | /// The nameplate object to modify. 58 | /// Whether modes that do not normally support icons should be adjusted to show an icon. 59 | public void SetupForPC(XivApi.SafeNamePlateObject npObject, bool forceIcon) 60 | { 61 | var nameScale = 0.75f; 62 | var iconScale = 1f; 63 | var iconOffset = new Vector2(0, 0); 64 | 65 | switch (GetModeForNameplate(npObject)) 66 | { 67 | case NameplateMode.Default: 68 | case NameplateMode.SmallJobIcon: 69 | case NameplateMode.SmallJobIconAndRole: 70 | SetupDefault(npObject); 71 | 72 | return; 73 | 74 | case NameplateMode.Hide: 75 | nameScale = 0f; 76 | iconScale = 0f; 77 | 78 | break; 79 | 80 | case NameplateMode.BigJobIcon: 81 | nameScale = 0.75f; 82 | 83 | switch (_configuration.SizeMode) 84 | { 85 | case NameplateSizeMode.Smaller: 86 | iconOffset = new Vector2(9, 50); 87 | iconScale = 1.5f; 88 | 89 | break; 90 | 91 | case NameplateSizeMode.Medium: 92 | iconOffset = new Vector2(-12, 24); 93 | iconScale = 3f; 94 | 95 | break; 96 | 97 | case NameplateSizeMode.Bigger: 98 | iconOffset = new Vector2(-27, -12); 99 | iconScale = 4f; 100 | 101 | break; 102 | } 103 | 104 | break; 105 | 106 | case NameplateMode.BigJobIconAndPartySlot: 107 | switch (_configuration.SizeMode) 108 | { 109 | case NameplateSizeMode.Smaller: 110 | iconOffset = new Vector2(12, 62); 111 | iconScale = 1.2f; 112 | nameScale = 0.6f; 113 | 114 | break; 115 | 116 | case NameplateSizeMode.Medium: 117 | iconOffset = new Vector2(-14, 41); 118 | iconScale = 2.3f; 119 | nameScale = 1f; 120 | 121 | break; 122 | 123 | case NameplateSizeMode.Bigger: 124 | iconOffset = new Vector2(-32, 15); 125 | iconScale = 3f; 126 | nameScale = 1.5f; 127 | 128 | break; 129 | } 130 | 131 | break; 132 | 133 | case NameplateMode.RoleLetters: 134 | iconScale = 0f; 135 | 136 | // Allow an icon to be displayed in roles only mode 137 | if (forceIcon) 138 | { 139 | iconScale = _configuration.SizeMode switch 140 | { 141 | NameplateSizeMode.Smaller => 1f, 142 | NameplateSizeMode.Medium => 1.5f, 143 | NameplateSizeMode.Bigger => 2f 144 | }; 145 | iconOffset = _configuration.SizeMode switch 146 | { 147 | NameplateSizeMode.Smaller => new Vector2(-6, 74), 148 | NameplateSizeMode.Medium => new Vector2(-42, 55), 149 | NameplateSizeMode.Bigger => new Vector2(-78, 35) 150 | }; 151 | } 152 | 153 | nameScale = _configuration.SizeMode switch 154 | { 155 | NameplateSizeMode.Smaller => 0.5f, 156 | NameplateSizeMode.Medium => 1f, 157 | NameplateSizeMode.Bigger => 1.5f 158 | }; 159 | 160 | break; 161 | } 162 | 163 | npObject.SetIconPosition((short) iconOffset.X, (short) iconOffset.Y); 164 | _ = npObject.SetIconScale(iconScale); 165 | _ = npObject.SetNameScale(nameScale); 166 | } 167 | 168 | public void NameplateDataForPC( 169 | XivApi.SafeNamePlateObject npObject, 170 | ref bool isPrefixTitle, 171 | ref bool displayTitle, 172 | ref IntPtr title, 173 | ref IntPtr name, 174 | ref IntPtr fcName, 175 | ref int iconID 176 | ) 177 | { 178 | //name = SeStringUtils.SeStringToPtr(SeStringUtils.Text("Plugin Enjoyer")); 179 | var uid = npObject.NamePlateInfo.Data.ObjectID.ObjectID; 180 | var mode = GetModeForNameplate(npObject); 181 | 182 | if (_configuration.HideLocalPlayerNameplate && uid == Service.ClientState.LocalPlayer?.ObjectId) 183 | { 184 | switch (mode) 185 | { 186 | case NameplateMode.Default: 187 | case NameplateMode.Hide: 188 | case NameplateMode.SmallJobIcon: 189 | case NameplateMode.BigJobIcon: 190 | case NameplateMode.BigJobIconAndPartySlot: 191 | name = SeStringUtils.emptyPtr; 192 | fcName = SeStringUtils.emptyPtr; 193 | displayTitle = false; 194 | iconID = 0; 195 | 196 | return; 197 | 198 | case NameplateMode.RoleLetters: 199 | if (!_configuration.TestingMode && !npObject.NamePlateInfo.IsPartyMember()) 200 | { 201 | name = SeStringUtils.emptyPtr; 202 | fcName = SeStringUtils.emptyPtr; 203 | displayTitle = false; 204 | iconID = 0; 205 | 206 | return; 207 | } 208 | 209 | break; 210 | } 211 | } 212 | 213 | var playerCharacter = Service.ObjectTable.SearchById(uid) as PlayerCharacter; 214 | 215 | if (playerCharacter == null) 216 | { 217 | return; 218 | } 219 | 220 | var hasRole = _roleTracker.TryGetAssignedRole(playerCharacter.Name.TextValue, playerCharacter.HomeWorld.Id, 221 | out var roleId); 222 | 223 | switch (mode) 224 | { 225 | case NameplateMode.Default: 226 | case NameplateMode.Hide: 227 | break; 228 | 229 | case NameplateMode.SmallJobIcon: 230 | var nameString = GetStateNametext(iconID, ""); 231 | var originalName = SeStringUtils.SeStringFromPtr(name); 232 | nameString.Append(originalName); 233 | 234 | name = SeStringUtils.SeStringToPtr(nameString); 235 | iconID = GetClassIcon(npObject.NamePlateInfo); 236 | 237 | break; 238 | 239 | case NameplateMode.SmallJobIconAndRole: 240 | nameString = new SeString(); 241 | 242 | if (hasRole) 243 | { 244 | nameString.Append(_stylesheet.GetRolePlate(roleId)); 245 | nameString.Append(" "); 246 | } 247 | 248 | originalName = SeStringUtils.SeStringFromPtr(name); 249 | nameString.Append(originalName); 250 | 251 | name = SeStringUtils.SeStringToPtr(nameString); 252 | iconID = GetClassIcon(npObject.NamePlateInfo); 253 | 254 | break; 255 | 256 | case NameplateMode.BigJobIcon: 257 | name = SeStringUtils.SeStringToPtr(GetStateNametext(iconID, " ")); 258 | fcName = SeStringUtils.emptyPtr; 259 | displayTitle = false; 260 | iconID = GetClassIcon(npObject.NamePlateInfo); 261 | 262 | break; 263 | 264 | case NameplateMode.BigJobIconAndPartySlot: 265 | fcName = SeStringUtils.emptyPtr; 266 | displayTitle = false; 267 | var partySlot = _partyListHudView.GetPartySlotIndex(npObject.NamePlateInfo.Data.ObjectID.ObjectID) + 268 | 1; 269 | 270 | if (partySlot != null) 271 | { 272 | var genericRole = JobExtensions.GetRole((Job) npObject.NamePlateInfo.GetJobID()); 273 | var str = _stylesheet.GetPartySlotNumber(partySlot.Value, genericRole); 274 | str.Payloads.Insert(0, new TextPayload(" ")); 275 | name = SeStringUtils.SeStringToPtr(str); 276 | iconID = GetClassIcon(npObject.NamePlateInfo); 277 | } 278 | else 279 | { 280 | name = SeStringUtils.emptyPtr; 281 | iconID = GetClassIcon(npObject.NamePlateInfo); 282 | } 283 | 284 | break; 285 | 286 | case NameplateMode.RoleLetters: 287 | if (hasRole) 288 | { 289 | name = SeStringUtils.SeStringToPtr(_stylesheet.GetRolePlate(roleId)); 290 | } 291 | else 292 | { 293 | var genericRole = JobExtensions.GetRole((Job) npObject.NamePlateInfo.GetJobID()); 294 | name = SeStringUtils.SeStringToPtr(_stylesheet.GetGenericRolePlate(genericRole)); 295 | } 296 | 297 | fcName = SeStringUtils.emptyPtr; 298 | displayTitle = false; 299 | 300 | break; 301 | } 302 | } 303 | 304 | private int GetClassIcon(XivApi.SafeNamePlateInfo info) 305 | { 306 | var genericRole = JobExtensions.GetRole((Job) info.GetJobID()); 307 | var iconSet = _stylesheet.GetGenericRoleIconset(genericRole); 308 | 309 | return _iconSet.GetJobIcon(iconSet, info.GetJobID()); 310 | } 311 | 312 | private SeString GetStateNametext(int iconId, string prefix) 313 | { 314 | switch (iconId) 315 | { 316 | case 061523: 317 | return SeStringUtils.Icon(BitmapFontIcon.NewAdventurer, prefix); 318 | 319 | case 061540: 320 | return SeStringUtils.Icon(BitmapFontIcon.Mentor, prefix); 321 | 322 | case 061542: 323 | return SeStringUtils.Icon(BitmapFontIcon.MentorPvE, prefix); 324 | 325 | case 061543: 326 | return SeStringUtils.Icon(BitmapFontIcon.MentorCrafting, prefix); 327 | 328 | case 061544: 329 | return SeStringUtils.Icon(BitmapFontIcon.MentorPvP, prefix); 330 | 331 | case 061547: 332 | return SeStringUtils.Icon(BitmapFontIcon.Returner, prefix); 333 | 334 | default: 335 | return SeStringUtils.Text(prefix + " "); 336 | } 337 | } 338 | 339 | private NameplateMode GetModeForNameplate(XivApi.SafeNamePlateObject npObject) 340 | { 341 | var uid = npObject.NamePlateInfo.Data.ObjectID.ObjectID; 342 | var mode = OthersMode; 343 | 344 | if (_configuration.TestingMode || npObject.NamePlateInfo.IsPartyMember() || 345 | uid == Service.ClientState.LocalPlayer?.ObjectId) 346 | { 347 | return PartyMode; 348 | } 349 | else 350 | { 351 | return OthersMode; 352 | } 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /PartyIcons/Runtime/RoleTracker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Text.RegularExpressions; 6 | using Dalamud.Game; 7 | using Dalamud.Game.ClientState; 8 | using Dalamud.Game.ClientState.Conditions; 9 | using Dalamud.Game.ClientState.Party; 10 | using Dalamud.Game.Gui; 11 | using Dalamud.Game.Gui.Toast; 12 | using Dalamud.Game.Text; 13 | using Dalamud.Game.Text.SeStringHandling; 14 | using Dalamud.Game.Text.SeStringHandling.Payloads; 15 | using Dalamud.IoC; 16 | using Dalamud.Logging; 17 | using PartyIcons.Configuration; 18 | using PartyIcons.Entities; 19 | 20 | namespace PartyIcons.Runtime; 21 | 22 | public sealed class RoleTracker : IDisposable 23 | { 24 | public event Action OnRoleOccupied; 25 | public event Action OnRoleSuggested; 26 | public event Action OnAssignedRolesUpdated; 27 | 28 | private readonly Settings _configuration; 29 | 30 | private bool _currentlyInParty; 31 | private uint _territoryId; 32 | private int _previousStateHash; 33 | 34 | private List<(RoleId, string)> _occupationMessages = new(); 35 | private List<(RoleId, Regex)> _suggestionRegexes = new(); 36 | 37 | private Dictionary _occupiedRoles = new(); 38 | private Dictionary _assignedRoles = new(); 39 | private Dictionary _suggestedRoles = new(); 40 | private HashSet _unassignedRoles = new(); 41 | 42 | public RoleTracker(Settings configuration) 43 | { 44 | _configuration = configuration; 45 | 46 | foreach (var role in Enum.GetValues()) 47 | { 48 | var roleIdentifier = role.ToString().ToLower(); 49 | var regex = new Regex($"\\W{roleIdentifier}\\W"); 50 | 51 | _occupationMessages.Add((role, $" {roleIdentifier} ")); 52 | _suggestionRegexes.Add((role, regex)); 53 | } 54 | 55 | _occupationMessages.Add((RoleId.OT, " st ")); 56 | _suggestionRegexes.Add((RoleId.OT, new Regex("\\Wst\\W"))); 57 | 58 | for (var i = 1; i < 5; i++) 59 | { 60 | var roleId = RoleId.M1 + i - 1; 61 | _occupationMessages.Add((roleId, $" d{i} ")); 62 | _suggestionRegexes.Add((roleId, new Regex($"\\Wd{i}\\W"))); 63 | } 64 | } 65 | 66 | public void Enable() 67 | { 68 | Service.ChatGui.ChatMessage += OnChatMessage; 69 | Service.Framework.Update += FrameworkOnUpdate; 70 | } 71 | 72 | public void Disable() 73 | { 74 | Service.ChatGui.ChatMessage -= OnChatMessage; 75 | Service.Framework.Update -= FrameworkOnUpdate; 76 | } 77 | 78 | public void Dispose() 79 | { 80 | Disable(); 81 | } 82 | 83 | public bool TryGetSuggestedRole(string name, uint worldId, out RoleId roleId) => 84 | _suggestedRoles.TryGetValue(PlayerId(name, worldId), out roleId); 85 | 86 | public bool TryGetAssignedRole(string name, uint worldId, out RoleId roleId) 87 | { 88 | // PluginLog.Verbose($"{_assignedRoles.Count}"); 89 | return _assignedRoles.TryGetValue(PlayerId(name, worldId), out roleId); 90 | } 91 | 92 | public void OccupyRole(string name, uint world, RoleId roleId) 93 | { 94 | foreach (var kv in _occupiedRoles.ToArray()) 95 | { 96 | if (kv.Value == roleId) 97 | { 98 | _occupiedRoles.Remove(kv.Key); 99 | } 100 | } 101 | 102 | _occupiedRoles[PlayerId(name, world)] = roleId; 103 | OnRoleOccupied?.Invoke(name, roleId); 104 | Service.ToastGui.ShowQuest($"{name} occupied {roleId}", new QuestToastOptions { DisplayCheckmark = true }); 105 | } 106 | 107 | public void SuggestRole(string name, uint world, RoleId roleId) 108 | { 109 | _suggestedRoles[PlayerId(name, world)] = roleId; 110 | OnRoleSuggested?.Invoke(name, roleId); 111 | // ToastGui.ShowQuest($"{roleId} is now suggested for {name}"); 112 | } 113 | 114 | public void ResetOccupations() 115 | { 116 | PluginLog.Verbose("Resetting occupation"); 117 | _occupiedRoles.Clear(); 118 | } 119 | 120 | public void ResetAssignments() 121 | { 122 | PluginLog.Verbose("Resetting assignments"); 123 | _assignedRoles.Clear(); 124 | _unassignedRoles.Clear(); 125 | 126 | foreach (var role in Enum.GetValues()) 127 | { 128 | if (role != default) 129 | { 130 | _unassignedRoles.Add(role); 131 | } 132 | } 133 | } 134 | 135 | public void CalculateUnassignedPartyRoles() 136 | { 137 | ResetAssignments(); 138 | 139 | PluginLog.Verbose($"Assigning current occupations ({_occupiedRoles.Count})"); 140 | 141 | foreach (var kv in _occupiedRoles) 142 | { 143 | PluginLog.Verbose($"{kv.Key} == {kv.Value} as per occupation"); 144 | 145 | _assignedRoles[kv.Key] = kv.Value; 146 | _unassignedRoles.Remove(kv.Value); 147 | } 148 | 149 | PluginLog.Verbose($"Assigning static assignments ({_configuration.StaticAssignments.Count})"); 150 | 151 | foreach (var kv in _configuration.StaticAssignments) 152 | { 153 | foreach (var member in Service.PartyList) 154 | { 155 | var playerId = PlayerId(member); 156 | 157 | if (_assignedRoles.ContainsKey(playerId)) 158 | { 159 | PluginLog.Verbose($"{PlayerId(member)} has already been assigned a role"); 160 | 161 | continue; 162 | } 163 | 164 | var playerDescription = $"{member.Name}@{member.World.GameData.Name}"; 165 | 166 | if (kv.Key.Equals(playerDescription)) 167 | { 168 | var applicableRoles = 169 | GetApplicableRolesForGenericRole( 170 | JobRoleExtensions.RoleFromByte(member.ClassJob.GameData.Role)); 171 | 172 | if (applicableRoles.Contains(kv.Value)) 173 | { 174 | PluginLog.Verbose($"{playerId} == {kv.Value} as per static assignments {playerDescription}"); 175 | _assignedRoles[playerId] = kv.Value; 176 | } 177 | else 178 | { 179 | PluginLog.Verbose( 180 | $"Skipping static assignment - applicable roles {string.Join(", ", applicableRoles)}, static role - {kv.Value}"); 181 | } 182 | } 183 | } 184 | } 185 | 186 | PluginLog.Verbose("Assigning the rest"); 187 | 188 | foreach (var member in Service.PartyList) 189 | { 190 | if (_assignedRoles.ContainsKey(PlayerId(member))) 191 | { 192 | PluginLog.Verbose($"{PlayerId(member)} has already been assigned a role"); 193 | 194 | continue; 195 | } 196 | 197 | var roleToAssign = 198 | FindUnassignedRoleForGenericRole(JobRoleExtensions.RoleFromByte(member.ClassJob.GameData.Role)); 199 | 200 | if (roleToAssign != default) 201 | { 202 | PluginLog.Verbose($"{PlayerId(member)} == {roleToAssign} as per first available"); 203 | _assignedRoles[PlayerId(member)] = roleToAssign; 204 | _unassignedRoles.Remove(roleToAssign); 205 | } 206 | } 207 | 208 | OnAssignedRolesUpdated?.Invoke(); 209 | } 210 | 211 | public string DebugDescription() 212 | { 213 | var sb = new StringBuilder(); 214 | sb.Append($"Assignments:\n"); 215 | 216 | foreach (var kv in _assignedRoles) 217 | { 218 | sb.Append($"Role {kv.Value} assigned to {kv.Key}\n"); 219 | } 220 | 221 | sb.Append($"\nOccupations:\n"); 222 | 223 | foreach (var kv in _occupiedRoles) 224 | { 225 | sb.Append($"Role {kv.Value} occupied by {kv.Key}\n"); 226 | } 227 | 228 | sb.Append("\nUnassigned roles:\n"); 229 | 230 | foreach (var k in _unassignedRoles) 231 | { 232 | sb.Append(" " + k); 233 | } 234 | 235 | return sb.ToString(); 236 | } 237 | 238 | private void FrameworkOnUpdate(Framework framework) 239 | { 240 | if (!Service.Condition[ConditionFlag.ParticipatingInCrossWorldPartyOrAlliance] 241 | && Service.PartyList.Length == 0 && 242 | _occupiedRoles.Any()) 243 | { 244 | PluginLog.Verbose("Resetting occupations, no longer in a party"); 245 | ResetOccupations(); 246 | 247 | return; 248 | } 249 | 250 | var partyHash = 17; 251 | 252 | foreach (var member in Service.PartyList) 253 | { 254 | unchecked 255 | { 256 | partyHash = partyHash * 23 + (int)member.ObjectId; 257 | } 258 | } 259 | 260 | if (partyHash != _previousStateHash) 261 | { 262 | PluginLog.Verbose($"Party hash changed ({partyHash}, prev {_previousStateHash}), recalculating roles"); 263 | CalculateUnassignedPartyRoles(); 264 | } 265 | 266 | _previousStateHash = partyHash; 267 | } 268 | 269 | private string PlayerId(string name, uint worldId) => $"{name}@{worldId}"; 270 | 271 | private string PlayerId(PartyMember member) => $"{member.Name.TextValue}@{member.World.Id}"; 272 | 273 | private RoleId FindUnassignedRoleForGenericRole(GenericRole role) 274 | { 275 | var applicableRoles = GetApplicableRolesForGenericRole(role); 276 | 277 | return applicableRoles.FirstOrDefault(r => _unassignedRoles.Contains(r)); 278 | } 279 | 280 | private IEnumerable GetApplicableRolesForGenericRole(GenericRole role) 281 | { 282 | switch (role) 283 | { 284 | case GenericRole.Tank: 285 | return new[] { RoleId.MT, RoleId.OT }; 286 | 287 | case GenericRole.Melee: 288 | return new[] { RoleId.M1, RoleId.M2, RoleId.R1, RoleId.R2 }; 289 | 290 | case GenericRole.Ranged: 291 | return new[] { RoleId.R1, RoleId.R2, RoleId.M1, RoleId.M2 }; 292 | 293 | case GenericRole.Healer: 294 | return new[] { RoleId.H1, RoleId.H2 }; 295 | 296 | default: 297 | return new[] { RoleId.Undefined }; 298 | } 299 | } 300 | 301 | private void OnChatMessage(XivChatType type, uint senderid, ref SeString sender, ref SeString message, 302 | ref bool ishandled) 303 | { 304 | if (_configuration.AssignFromChat && (type == XivChatType.Party || type == XivChatType.CrossParty || type == XivChatType.Say)) 305 | { 306 | string? playerName = null; 307 | uint? playerWorld = null; 308 | 309 | var playerPayload = sender.Payloads.FirstOrDefault(p => p is PlayerPayload) as PlayerPayload; 310 | 311 | if (playerPayload == null) 312 | { 313 | playerName = Service.ClientState.LocalPlayer?.Name.TextValue; 314 | playerWorld = Service.ClientState.LocalPlayer?.HomeWorld.Id; 315 | } 316 | else 317 | { 318 | playerName = playerPayload?.PlayerName; 319 | playerWorld = playerPayload?.World.RowId; 320 | } 321 | 322 | if (playerName == null || !playerWorld.HasValue) 323 | { 324 | PluginLog.Verbose($"Failed to get player data from {senderid}, {sender} ({sender.Payloads})"); 325 | 326 | return; 327 | } 328 | 329 | var text = message.TextValue.Trim().ToLower(); 330 | var paddedText = $" {text} "; 331 | 332 | var roleToOccupy = RoleId.Undefined; 333 | var occupationTainted = false; 334 | var roleToSuggest = RoleId.Undefined; 335 | var suggestionTainted = false; 336 | 337 | foreach (var tuple in _occupationMessages) 338 | { 339 | if (tuple.Item2.Equals(paddedText)) 340 | { 341 | PluginLog.Verbose( 342 | $"Message contained role occupation ({playerName}@{playerWorld} - {text}, detected role {tuple.Item1})"); 343 | 344 | if (roleToOccupy == RoleId.Undefined) 345 | { 346 | roleToOccupy = tuple.Item1; 347 | } 348 | else 349 | { 350 | PluginLog.Verbose($"Multiple role occupation matches, aborting"); 351 | occupationTainted = true; 352 | 353 | break; 354 | } 355 | } 356 | } 357 | 358 | foreach (var tuple in _suggestionRegexes) 359 | { 360 | if (tuple.Item2.IsMatch(paddedText)) 361 | { 362 | PluginLog.Verbose( 363 | $"Message contained role suggestion ({playerName}@{playerWorld}: {text}, detected {tuple.Item1}"); 364 | 365 | if (roleToSuggest == RoleId.Undefined) 366 | { 367 | roleToSuggest = tuple.Item1; 368 | } 369 | else 370 | { 371 | PluginLog.Verbose("Multiple role suggesting matches, aborting"); 372 | suggestionTainted = true; 373 | 374 | break; 375 | } 376 | } 377 | } 378 | 379 | if (!occupationTainted && roleToOccupy != RoleId.Undefined) 380 | { 381 | OccupyRole(playerName, playerWorld.Value, roleToOccupy); 382 | 383 | PluginLog.Verbose($"Recalculating assignments due to new occupations"); 384 | CalculateUnassignedPartyRoles(); 385 | } 386 | else if (!suggestionTainted && roleToSuggest != RoleId.Undefined) 387 | { 388 | SuggestRole(playerName, playerWorld.Value, roleToSuggest); 389 | } 390 | } 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /PartyIcons/Api/XivApi.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using Dalamud.Game.ClientState.Objects.Enums; 4 | using Dalamud.Game.ClientState.Objects.SubKinds; 5 | using Dalamud.Logging; 6 | using Dalamud.Plugin; 7 | using FFXIVClientStructs.FFXIV.Client.System.String; 8 | using FFXIVClientStructs.FFXIV.Client.UI; 9 | using FFXIVClientStructs.FFXIV.Client.System.Framework; 10 | using FFXIVClientStructs.FFXIV.Component.GUI; 11 | 12 | namespace PartyIcons.Api; 13 | 14 | public class XivApi : IDisposable 15 | { 16 | public static int ThreadID => System.Threading.Thread.CurrentThread.ManagedThreadId; 17 | 18 | private static Plugin _plugin; 19 | 20 | private readonly SetNamePlateDelegate SetNamePlate; 21 | private readonly Framework_GetUIModuleDelegate GetUIModule; 22 | private readonly GroupManager_IsObjectIDInPartyDelegate IsObjectIDInParty; 23 | private readonly GroupManager_IsObjectIDInAllianceDelegate IsObjectIDInAlliance; 24 | private readonly AtkResNode_SetScaleDelegate SetNodeScale; 25 | private readonly AtkResNode_SetPositionShortDelegate SetNodePosition; 26 | private readonly BattleCharaStore_LookupBattleCharaByObjectIDDelegate LookupBattleCharaByObjectID; 27 | 28 | public static void Initialize(Plugin plugin, PluginAddressResolver address) 29 | { 30 | _plugin ??= plugin; 31 | Instance ??= new XivApi(Service.PluginInterface, address); 32 | } 33 | 34 | private static XivApi Instance; 35 | 36 | private XivApi(DalamudPluginInterface pluginInterface, PluginAddressResolver address) 37 | { 38 | SetNamePlate = 39 | Marshal.GetDelegateForFunctionPointer(address.AddonNamePlate_SetNamePlatePtr); 40 | GetUIModule = 41 | Marshal.GetDelegateForFunctionPointer(address.Framework_GetUIModulePtr); 42 | IsObjectIDInParty = 43 | Marshal.GetDelegateForFunctionPointer( 44 | address.GroupManager_IsObjectIDInPartyPtr); 45 | IsObjectIDInAlliance = 46 | Marshal.GetDelegateForFunctionPointer( 47 | address.GroupManager_IsObjectIDInAlliancePtr); 48 | SetNodeScale = 49 | Marshal.GetDelegateForFunctionPointer(address.AtkResNode_SetScalePtr); 50 | SetNodePosition = 51 | Marshal.GetDelegateForFunctionPointer( 52 | address.AtkResNode_SetPositionShortPtr); 53 | LookupBattleCharaByObjectID = 54 | Marshal.GetDelegateForFunctionPointer( 55 | address.BattleCharaStore_LookupBattleCharaByObjectIDPtr); 56 | 57 | Service.ClientState.Logout += OnLogout_ResetRaptureAtkModule; 58 | } 59 | 60 | public static void DisposeInstance() => Instance.Dispose(); 61 | 62 | public void Dispose() 63 | { 64 | Service.ClientState.Logout -= OnLogout_ResetRaptureAtkModule; 65 | } 66 | 67 | #region RaptureAtkModule 68 | 69 | private static IntPtr _RaptureAtkModulePtr = IntPtr.Zero; 70 | 71 | public static IntPtr RaptureAtkModulePtr 72 | { 73 | get 74 | { 75 | if (_RaptureAtkModulePtr == IntPtr.Zero) 76 | { 77 | unsafe 78 | { 79 | var framework = Framework.Instance(); 80 | var uiModule = framework->GetUiModule(); 81 | 82 | _RaptureAtkModulePtr = new IntPtr(uiModule->GetRaptureAtkModule()); 83 | } 84 | } 85 | 86 | return _RaptureAtkModulePtr; 87 | } 88 | } 89 | 90 | private void OnLogout_ResetRaptureAtkModule(object sender, EventArgs evt) => _RaptureAtkModulePtr = IntPtr.Zero; 91 | 92 | #endregion 93 | 94 | public static SafeAddonNamePlate GetSafeAddonNamePlate() => new(Service.PluginInterface); 95 | 96 | public static bool IsLocalPlayer(uint actorID) => Service.ClientState.LocalPlayer?.ObjectId == actorID; 97 | 98 | public static bool IsPartyMember(uint actorID) => 99 | Instance.IsObjectIDInParty(_plugin.Address.GroupManagerPtr, actorID) == 1; 100 | 101 | public static bool IsAllianceMember(uint actorID) => 102 | Instance.IsObjectIDInParty(_plugin.Address.GroupManagerPtr, actorID) == 1; 103 | 104 | public static bool IsPlayerCharacter(uint actorID) 105 | { 106 | foreach (var obj in Service.ObjectTable) 107 | { 108 | if (obj == null) 109 | { 110 | continue; 111 | } 112 | 113 | if (obj.ObjectId == actorID) 114 | { 115 | return obj.ObjectKind == ObjectKind.Player; 116 | } 117 | } 118 | 119 | return false; 120 | } 121 | 122 | public static uint GetJobId(uint actorID) 123 | { 124 | foreach (var obj in Service.ObjectTable) 125 | { 126 | if (obj == null) 127 | { 128 | continue; 129 | } 130 | 131 | if (obj.ObjectId == actorID && obj is PlayerCharacter character) 132 | { 133 | return character.ClassJob.Id; 134 | } 135 | } 136 | 137 | return 0; 138 | } 139 | 140 | public class SafeAddonNamePlate 141 | { 142 | private readonly DalamudPluginInterface Interface; 143 | 144 | public IntPtr Pointer => Service.GameGui.GetAddonByName("NamePlate", 1); 145 | 146 | public SafeAddonNamePlate(DalamudPluginInterface pluginInterface) 147 | { 148 | Interface = pluginInterface; 149 | } 150 | 151 | public unsafe SafeNamePlateObject GetNamePlateObject(int index) 152 | { 153 | if (Pointer == IntPtr.Zero) 154 | { 155 | return null; 156 | } 157 | 158 | var npObjectArrayPtrPtr = Pointer + Marshal 159 | .OffsetOf(typeof(AddonNamePlate), nameof(AddonNamePlate.NamePlateObjectArray)).ToInt32(); 160 | var npObjectArrayPtr = Marshal.ReadIntPtr(npObjectArrayPtrPtr); 161 | 162 | if (npObjectArrayPtr == IntPtr.Zero) 163 | { 164 | PluginLog.Verbose($"[{GetType().Name}] NamePlateObjectArray was null"); 165 | 166 | return null; 167 | } 168 | 169 | var npObjectPtr = npObjectArrayPtr + Marshal.SizeOf(typeof(AddonNamePlate.NamePlateObject)) * index; 170 | 171 | return new SafeNamePlateObject(npObjectPtr, index); 172 | } 173 | } 174 | 175 | public class SafeNamePlateObject 176 | { 177 | public readonly IntPtr Pointer; 178 | public readonly AddonNamePlate.NamePlateObject Data; 179 | 180 | private int _Index; 181 | private SafeNamePlateInfo _NamePlateInfo; 182 | 183 | public SafeNamePlateObject(IntPtr pointer, int index = -1) 184 | { 185 | Pointer = pointer; 186 | Data = Marshal.PtrToStructure(pointer); 187 | _Index = index; 188 | } 189 | 190 | public int Index 191 | { 192 | get 193 | { 194 | if (_Index == -1) 195 | { 196 | var addon = GetSafeAddonNamePlate(); 197 | var npObject0 = addon.GetNamePlateObject(0); 198 | 199 | if (npObject0 == null) 200 | { 201 | PluginLog.Verbose($"[{GetType().Name}] NamePlateObject0 was null"); 202 | 203 | return -1; 204 | } 205 | 206 | var npObjectBase = npObject0.Pointer; 207 | var npObjectSize = Marshal.SizeOf(typeof(AddonNamePlate.NamePlateObject)); 208 | var index = (Pointer.ToInt64() - npObjectBase.ToInt64()) / npObjectSize; 209 | 210 | if (index < 0 || index >= 50) 211 | { 212 | PluginLog.Verbose($"[{GetType().Name}] NamePlateObject index was out of bounds"); 213 | 214 | return -1; 215 | } 216 | 217 | _Index = (int) index; 218 | } 219 | 220 | return _Index; 221 | } 222 | } 223 | 224 | public SafeNamePlateInfo NamePlateInfo 225 | { 226 | get 227 | { 228 | if (_NamePlateInfo == null) 229 | { 230 | var rapturePtr = RaptureAtkModulePtr; 231 | 232 | if (rapturePtr == IntPtr.Zero) 233 | { 234 | PluginLog.Verbose($"[{GetType().Name}] RaptureAtkModule was null"); 235 | 236 | return null; 237 | } 238 | 239 | var npInfoArrayPtr = RaptureAtkModulePtr + Marshal.OffsetOf(typeof(RaptureAtkModule), 240 | nameof(RaptureAtkModule.NamePlateInfoArray)).ToInt32(); 241 | var npInfoPtr = npInfoArrayPtr + Marshal.SizeOf(typeof(RaptureAtkModule.NamePlateInfo)) * Index; 242 | _NamePlateInfo = new SafeNamePlateInfo(npInfoPtr); 243 | } 244 | 245 | return _NamePlateInfo; 246 | } 247 | } 248 | 249 | #region Getters 250 | 251 | public unsafe IntPtr IconImageNodeAddress => Marshal.ReadIntPtr(Pointer + Marshal 252 | .OffsetOf(typeof(AddonNamePlate.NamePlateObject), nameof(AddonNamePlate.NamePlateObject.IconImageNode)) 253 | .ToInt32()); 254 | 255 | public unsafe IntPtr NameNodeAddress => Marshal.ReadIntPtr(Pointer + Marshal 256 | .OffsetOf(typeof(AddonNamePlate.NamePlateObject), nameof(AddonNamePlate.NamePlateObject.NameText)) 257 | .ToInt32()); 258 | 259 | public AtkImageNode IconImageNode => Marshal.PtrToStructure(IconImageNodeAddress); 260 | public AtkTextNode NameTextNode => Marshal.PtrToStructure(NameNodeAddress); 261 | 262 | #endregion 263 | 264 | public unsafe bool IsVisible => Data.IsVisible; 265 | 266 | public unsafe bool IsLocalPlayer => Data.IsLocalPlayer; 267 | 268 | public bool IsPlayer => Data.NameplateKind == 0; 269 | 270 | /// True if the icon scale was changed. 271 | public bool SetIconScale(float scale, bool force = false) 272 | { 273 | if (force || !IsIconScaleEqual(scale)) 274 | { 275 | Instance.SetNodeScale(IconImageNodeAddress, scale, scale); 276 | return true; 277 | } 278 | 279 | return false; 280 | } 281 | 282 | /// True if the name scale was changed. 283 | public bool SetNameScale(float scale, bool force = false) 284 | { 285 | if (force || !IsNameScaleEqual(scale)) 286 | { 287 | Instance.SetNodeScale(NameNodeAddress, scale, scale); 288 | return true; 289 | } 290 | 291 | return false; 292 | } 293 | 294 | public unsafe void SetName(IntPtr ptr) 295 | { 296 | NameTextNode.SetText("aaa"); 297 | } 298 | 299 | public void SetIcon(int icon) 300 | { 301 | IconImageNode.LoadIconTexture(icon, 1); 302 | } 303 | 304 | public void SetIconPosition(short x, short y) 305 | { 306 | var iconXAdjustPtr = Pointer + Marshal.OffsetOf(typeof(AddonNamePlate.NamePlateObject), 307 | nameof(AddonNamePlate.NamePlateObject.IconXAdjust)).ToInt32(); 308 | var iconYAdjustPtr = Pointer + Marshal.OffsetOf(typeof(AddonNamePlate.NamePlateObject), 309 | nameof(AddonNamePlate.NamePlateObject.IconYAdjust)).ToInt32(); 310 | Marshal.WriteInt16(iconXAdjustPtr, x); 311 | Marshal.WriteInt16(iconYAdjustPtr, y); 312 | } 313 | 314 | private static bool NearlyEqual(float left, float right, float tolerance) 315 | { 316 | return Math.Abs(left - right) <= tolerance; 317 | } 318 | 319 | private bool IsIconScaleEqual(float scale) => 320 | NearlyEqual(scale, IconImageNode.AtkResNode.ScaleX, ScaleTolerance) && 321 | NearlyEqual(scale, IconImageNode.AtkResNode.ScaleY, ScaleTolerance); 322 | 323 | private bool IsNameScaleEqual(float scale) => 324 | NearlyEqual(scale, NameTextNode.AtkResNode.ScaleX, ScaleTolerance) && 325 | NearlyEqual(scale, NameTextNode.AtkResNode.ScaleY, ScaleTolerance); 326 | 327 | private const float ScaleTolerance = 0.001f; 328 | } 329 | 330 | public class SafeNamePlateInfo 331 | { 332 | public readonly IntPtr Pointer; 333 | public readonly RaptureAtkModule.NamePlateInfo Data; 334 | 335 | public SafeNamePlateInfo(IntPtr pointer) 336 | { 337 | Pointer = pointer; //-0x10; 338 | Data = Marshal.PtrToStructure(Pointer); 339 | } 340 | 341 | #region Getters 342 | 343 | public IntPtr NameAddress => GetStringPtr(nameof(RaptureAtkModule.NamePlateInfo.Name)); 344 | 345 | public string Name => GetString(NameAddress); 346 | 347 | public IntPtr FcNameAddress => GetStringPtr(nameof(RaptureAtkModule.NamePlateInfo.FcName)); 348 | 349 | public string FcName => GetString(FcNameAddress); 350 | 351 | public IntPtr TitleAddress => GetStringPtr(nameof(RaptureAtkModule.NamePlateInfo.Title)); 352 | 353 | public string Title => GetString(TitleAddress); 354 | 355 | public IntPtr DisplayTitleAddress => GetStringPtr(nameof(RaptureAtkModule.NamePlateInfo.DisplayTitle)); 356 | 357 | public string DisplayTitle => GetString(DisplayTitleAddress); 358 | 359 | public IntPtr LevelTextAddress => GetStringPtr(nameof(RaptureAtkModule.NamePlateInfo.LevelText)); 360 | 361 | public string LevelText => GetString(LevelTextAddress); 362 | 363 | #endregion 364 | 365 | public bool IsPlayerCharacter() => XivApi.IsPlayerCharacter(Data.ObjectID.ObjectID); 366 | 367 | public bool IsPartyMember() => XivApi.IsPartyMember(Data.ObjectID.ObjectID); 368 | 369 | public bool IsAllianceMember() => XivApi.IsAllianceMember(Data.ObjectID.ObjectID); 370 | 371 | public uint GetJobID() => GetJobId(Data.ObjectID.ObjectID); 372 | 373 | private unsafe IntPtr GetStringPtr(string name) 374 | { 375 | var namePtr = Pointer + Marshal.OffsetOf(typeof(RaptureAtkModule.NamePlateInfo), name).ToInt32(); 376 | var stringPtrPtr = 377 | namePtr + Marshal.OffsetOf(typeof(Utf8String), nameof(Utf8String.StringPtr)).ToInt32(); 378 | var stringPtr = Marshal.ReadIntPtr(stringPtrPtr); 379 | 380 | return stringPtr; 381 | } 382 | 383 | private string GetString(IntPtr stringPtr) => Marshal.PtrToStringUTF8(stringPtr); 384 | } 385 | } 386 | --------------------------------------------------------------------------------