├── .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 |
--------------------------------------------------------------------------------