v1.0.3.0: Alliance frames fixes, PvP support, shared hotbar persistence, config save on teleport

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-01-31 20:35:25 -05:00
parent bda3762ac8
commit 11b4c268f0
17 changed files with 1145 additions and 77 deletions
+1 -1
View File
@@ -725,7 +725,7 @@ namespace HSUI.Interface.GeneralElements
displaySlot->Set(slotType, id);
displaySlot->LoadIconId();
}
module->SetAndSaveSlot(barId, (uint)idx, slotType, id);
ActionBarsManager.SetAndSaveSlotInternal(module, barId, (uint)idx, slotType, id, displaySlot);
try { dm->CancelDragDrop(true, true); }
catch (Exception ex2) { Plugin.Logger.Warning($"[HSUI DragDrop] CancelDragDrop: {ex2.Message}"); }
+5
View File
@@ -244,6 +244,11 @@ namespace HSUI.Interface
if (partyFrames?.Enabled == true)
AddElements(ElementKind.PartyList);
var alliance1 = ConfigurationManager.Instance?.GetConfigObject<AllianceFrames1Config>();
var alliance2 = ConfigurationManager.Instance?.GetConfigObject<AllianceFrames2Config>();
if ((alliance1?.Enabled == true || alliance2?.Enabled == true))
AddElements(ElementKind.AllianceList1, ElementKind.AllianceList2);
var enemyList = ConfigurationManager.Instance?.GetConfigObject<EnemyListConfig>();
if (enemyList?.Enabled == true)
AddElements(ElementKind.EnemyList);
+10
View File
@@ -247,6 +247,16 @@ namespace HSUI.Interface
_hudElements.Add(partyFramesConfig, partyFramesHud);
_hudElementsWithPreview.Add(partyFramesHud);
var allianceFrames1Config = ConfigurationManager.Instance.GetConfigObject<AllianceFrames1Config>();
var allianceFrames1Hud = new AllianceFramesHud(allianceFrames1Config, "Alliance Frames 1");
_hudElements.Add(allianceFrames1Config, allianceFrames1Hud);
_hudElementsWithPreview.Add(allianceFrames1Hud);
var allianceFrames2Config = ConfigurationManager.Instance.GetConfigObject<AllianceFrames2Config>();
var allianceFrames2Hud = new AllianceFramesHud(allianceFrames2Config, "Alliance Frames 2");
_hudElements.Add(allianceFrames2Config, allianceFrames2Hud);
_hudElementsWithPreview.Add(allianceFrames2Hud);
var enemyListConfig = ConfigurationManager.Instance.GetConfigObject<EnemyListConfig>();
var enemyListHud = new EnemyListHud(enemyListConfig, "Enemy List");
_hudElements.Add(enemyListConfig, enemyListHud);
+253
View File
@@ -0,0 +1,253 @@
using HSUI.Config;
using HSUI.Config.Attributes;
using HSUI.Enums;
using HSUI.Helpers;
using HSUI.Interface.Bars;
using HSUI.Interface.GeneralElements;
using HSUI.Interface.StatusEffects;
using Dalamud.Bindings.ImGui;
using System.Numerics;
namespace HSUI.Interface.Party
{
[Exportable(false)]
[Disableable(false)]
[DisableParentSettings("Position", "Anchor", "BackgroundColor", "FillColor", "HideWhenInactive", "DrawBorder", "BorderColor", "BorderThickness")]
[Section("Alliance Frames", true)]
[SubSection("Health Bar", 0)]
public class AllianceFramesHealthBarsConfig : PartyFramesHealthBarsConfig
{
public new static AllianceFramesHealthBarsConfig DefaultConfig()
{
var config = new AllianceFramesHealthBarsConfig(Vector2.Zero, new(100, 22), PluginConfigColor.Empty);
config.MouseoverAreaConfig.Enabled = false;
config.Padding = new Vector2(2, 1);
config.ShowLabels = false;
config.NameLabelConfig.Enabled = false;
config.HealthLabelConfig.Enabled = false;
config.OrderNumberConfig.Enabled = false;
return config;
}
public AllianceFramesHealthBarsConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor, BarDirection fillDirection = BarDirection.Right)
: base(position, size, fillColor, fillDirection)
{
}
}
[Exportable(false)]
[Section("Alliance Frames", true)]
[SubSection("Mana Bar", 0)]
public class AllianceFramesManaBarConfig : PartyFramesManaBarConfig
{
public new static AllianceFramesManaBarConfig DefaultConfig()
{
var config = new AllianceFramesManaBarConfig(new Vector2(0, 0), new Vector2(100, 6));
config.Enabled = false;
return config;
}
public AllianceFramesManaBarConfig(Vector2 position, Vector2 size) : base(position, size) { }
}
[Exportable(false)]
[Section("Alliance Frames", true)]
[SubSection("Castbar", 0)]
public class AllianceFramesCastbarConfig : PartyFramesCastbarConfig
{
public new static AllianceFramesCastbarConfig DefaultConfig()
{
var size = new Vector2(102, 10);
var pos = new Vector2(-1, 0);
var castNameConfig = new LabelConfig(new Vector2(5, 0), "", DrawAnchor.Left, DrawAnchor.Left);
var castTimeConfig = new NumericLabelConfig(new Vector2(-5, 0), "", DrawAnchor.Right, DrawAnchor.Right);
castTimeConfig.Enabled = false;
castTimeConfig.NumberFormat = 1;
var config = new AllianceFramesCastbarConfig(pos, size, castNameConfig, castTimeConfig);
config.Enabled = false;
return config;
}
public AllianceFramesCastbarConfig(Vector2 position, Vector2 size, LabelConfig castNameConfig, NumericLabelConfig castTimeConfig)
: base(position, size, castNameConfig, castTimeConfig) { }
}
[Disableable(false)]
[Exportable(false)]
[Section("Alliance Frames", true)]
[SubSection("Icons", 0)]
public class AllianceFramesIconsConfig : PartyFramesIconsConfig
{
public new static AllianceFramesIconsConfig DefaultConfig()
{
var config = new AllianceFramesIconsConfig();
config.Role.Enabled = false;
config.Sign.Enabled = false;
config.Leader.Enabled = false;
config.PlayerStatus.Enabled = false;
config.ReadyCheckStatus.Icon.Enabled = false;
config.WhosTalking.Enabled = false;
return config;
}
}
[Exportable(false)]
[Section("Alliance Frames", true)]
[SubSection("Buffs", 0)]
public class AllianceFramesBuffsConfig : PartyFramesBuffsConfig
{
public new static AllianceFramesBuffsConfig DefaultConfig()
{
var durationConfig = new LabelConfig(new Vector2(0, -4), "", DrawAnchor.Bottom, DrawAnchor.Center);
var stacksConfig = new LabelConfig(new Vector2(-3, 4), "", DrawAnchor.TopRight, DrawAnchor.Center);
stacksConfig.Color = new(Vector4.UnitW);
stacksConfig.OutlineColor = new(Vector4.One);
var iconConfig = new StatusEffectIconConfig(durationConfig, stacksConfig);
iconConfig.DispellableBorderConfig.Enabled = false;
iconConfig.Size = new Vector2(24, 24);
var pos = new Vector2(-2, 2);
var size = new Vector2(iconConfig.Size.X * 4 + 6, iconConfig.Size.Y);
var config = new AllianceFramesBuffsConfig(DrawAnchor.TopRight, pos, size, true, false, false, GrowthDirections.Left | GrowthDirections.Down, iconConfig);
config.Limit = 4;
config.Enabled = false;
return config;
}
public AllianceFramesBuffsConfig(DrawAnchor anchor, Vector2 position, Vector2 size, bool showBuffs, bool showDebuffs, bool showPermanentEffects,
GrowthDirections growthDirections, StatusEffectIconConfig iconConfig)
: base(anchor, position, size, showBuffs, showDebuffs, showPermanentEffects, growthDirections, iconConfig) { }
}
[Exportable(false)]
[Section("Alliance Frames", true)]
[SubSection("Debuffs", 0)]
public class AllianceFramesDebuffsConfig : PartyFramesDebuffsConfig
{
public new static AllianceFramesDebuffsConfig DefaultConfig()
{
var durationConfig = new LabelConfig(new Vector2(0, -4), "", DrawAnchor.Bottom, DrawAnchor.Center);
var stacksConfig = new LabelConfig(new Vector2(-3, 4), "", DrawAnchor.TopRight, DrawAnchor.Center);
stacksConfig.Color = new(Vector4.UnitW);
stacksConfig.OutlineColor = new(Vector4.One);
var iconConfig = new StatusEffectIconConfig(durationConfig, stacksConfig);
iconConfig.Size = new Vector2(20, 20);
iconConfig.DurationLabelConfig.Enabled = false;
iconConfig.StacksLabelConfig.Enabled = false;
var pos = new Vector2(-2, -2);
var size = new Vector2(iconConfig.Size.X * 4 + 4, iconConfig.Size.Y);
var config = new AllianceFramesDebuffsConfig(DrawAnchor.BottomRight, pos, size, false, true, false, GrowthDirections.Left | GrowthDirections.Up, iconConfig);
config.Limit = 4;
return config;
}
public AllianceFramesDebuffsConfig(DrawAnchor anchor, Vector2 position, Vector2 size, bool showBuffs, bool showDebuffs, bool showPermanentEffects,
GrowthDirections growthDirections, StatusEffectIconConfig iconConfig)
: base(anchor, position, size, showBuffs, showDebuffs, showPermanentEffects, growthDirections, iconConfig) { }
}
[Disableable(false)]
[Exportable(false)]
[Section("Alliance Frames", true)]
[SubSection("Trackers", 0)]
public class AllianceFramesTrackersConfig : PartyFramesTrackersConfig
{
public new static AllianceFramesTrackersConfig DefaultConfig()
{
var config = new AllianceFramesTrackersConfig();
config.Raise.Enabled = false;
config.Raise.Icon.Enabled = false;
config.Invuln.Enabled = false;
config.Invuln.Icon.Enabled = false;
config.Cleanse.Enabled = false;
return config;
}
}
[Exportable(false)]
[Section("Alliance Frames", true)]
[SubSection("Cooldowns", 0)]
public class AllianceFramesCooldownListConfig : PartyFramesCooldownListConfig
{
public new static AllianceFramesCooldownListConfig DefaultConfig()
{
var config = new AllianceFramesCooldownListConfig();
config.Position = new Vector2(-2, 0);
config.Size = new Vector2(40 * 8 + 6, 40);
config.Enabled = false;
return config;
}
}
/// <summary>Base config for alliance frames. Slot 0 and 1 = the two alliances the player is NOT in.</summary>
public abstract class AllianceFramesConfigBase : MovablePluginConfigObject
{
public abstract int SlotIndex { get; }
public abstract string FramesLabel { get; }
[Checkbox("Preview", isMonitored = true)]
[Order(4)]
public bool Preview = false;
[DragInt("Rows", spacing = true, isMonitored = true, min = 1, max = 8, velocity = 0.2f)]
[Order(10)]
public int Rows = 4;
[DragInt("Columns", isMonitored = true, min = 1, max = 8, velocity = 0.2f)]
[Order(11)]
public int Columns = 2;
[Anchor("Bars Anchor", isMonitored = true, spacing = true)]
[Order(15)]
public DrawAnchor BarsAnchor = DrawAnchor.TopLeft;
[Checkbox("Fill Rows First", isMonitored = true)]
[Order(20)]
public bool FillRowsFirst = true;
[Checkbox("Show when not in alliance raid", help = "When enabled, shows a placeholder frame outside raids so you can position it. Disable to hide frames entirely when not in a 24-player alliance raid.")]
[Order(25)]
public bool ShowWhenNotInRaid = false;
[NestedConfig("Title Label", 60)]
public AllianceFramesTitleLabel TitleLabelConfig = new AllianceFramesTitleLabel(Vector2.Zero, "", DrawAnchor.Left, DrawAnchor.Left);
[NestedConfig("Visibility", 200)]
public VisibilityConfig VisibilityConfig = new VisibilityConfig();
}
[Exportable(false)]
[DisableParentSettings("FrameAnchor", "UseJobColor", "UseRoleColor")]
public class AllianceFramesTitleLabel : LabelConfig
{
public AllianceFramesTitleLabel(Vector2 position, string text, DrawAnchor frameAnchor, DrawAnchor textAnchor)
: base(position, text, frameAnchor, textAnchor) { }
}
[Exportable(false)]
[Section("Alliance Frames", true)]
[SubSection("Alliance Frames 1", 0)]
public class AllianceFrames1Config : AllianceFramesConfigBase
{
public override int SlotIndex => 0;
public override string FramesLabel => "Alliance Frames 1";
public new static AllianceFrames1Config DefaultConfig()
{
var config = new AllianceFrames1Config();
config.Position = new Vector2(-ImGui.GetMainViewport().Size.X / 3 - 180, -350);
return config;
}
}
[Exportable(false)]
[Section("Alliance Frames", true)]
[SubSection("Alliance Frames 2", 0)]
public class AllianceFrames2Config : AllianceFramesConfigBase
{
public override int SlotIndex => 1;
public override string FramesLabel => "Alliance Frames 2";
public new static AllianceFrames2Config DefaultConfig()
{
var config = new AllianceFrames2Config();
config.Position = new Vector2(-ImGui.GetMainViewport().Size.X / 3 - 180, -230);
return config;
}
}
}
+285
View File
@@ -0,0 +1,285 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Bindings.ImGui;
using HSUI.Config;
using HSUI.Enums;
using HSUI.Helpers;
using HSUI.Interface.GeneralElements;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace HSUI.Interface.Party
{
public class AllianceFramesHud : DraggableHudElement, IHudElementWithMouseOver, IHudElementWithPreview, IHudElementWithVisibilityConfig
{
private AllianceFramesConfigBase Config => (AllianceFramesConfigBase)_config;
public VisibilityConfig VisibilityConfig => Config.VisibilityConfig;
private PartyFramesConfigs Configs;
private Vector2 _contentMargin = new Vector2(2, 2);
private static readonly int MaxMemberCount = 8;
private Vector2 _origin;
private LayoutInfo _layoutInfo;
private uint _memberCount = 0;
private bool _layoutDirty = true;
private readonly List<PartyFramesBar> bars;
private LabelHud _titleLabelHud;
public AllianceFramesHud(AllianceFramesConfigBase config, string displayName) : base(config, displayName)
{
Configs = PartyFramesConfigs.GetAllianceConfigs();
config.ValueChangeEvent += OnLayoutPropertyChanged;
Configs.HealthBar.ValueChangeEvent += OnLayoutPropertyChanged;
Configs.HealthBar.ColorsConfig.ValueChangeEvent += OnLayoutPropertyChanged;
bars = new List<PartyFramesBar>(MaxMemberCount);
for (int i = 0; i < bars.Capacity; i++)
{
var bar = new PartyFramesBar("HSUI_allianceFramesBar_" + Config.SlotIndex + "_" + i, Configs);
bars.Add(bar);
}
_titleLabelHud = new LabelHud(Config.TitleLabelConfig);
AllianceManager.Instance.MembersChangedEvent += OnMembersChanged;
}
protected override void InternalDispose()
{
foreach (var bar in bars)
{
try { bar.Dispose(); }
catch (Exception ex) { Plugin.Logger.Error($"Error disposing AllianceFramesBar: {ex.Message}"); }
}
bars.Clear();
_config.ValueChangeEvent -= OnLayoutPropertyChanged;
Configs.HealthBar.ValueChangeEvent -= OnLayoutPropertyChanged;
Configs.HealthBar.ColorsConfig.ValueChangeEvent -= OnLayoutPropertyChanged;
AllianceManager.Instance.MembersChangedEvent -= OnMembersChanged;
}
private void OnLayoutPropertyChanged(object sender, OnChangeBaseArgs args)
{
if (args.PropertyName == "Size" || args.PropertyName == "FillRowsFirst" ||
args.PropertyName == "BarsAnchor" || args.PropertyName == "Padding" ||
args.PropertyName == "Rows" || args.PropertyName == "Columns")
{
_layoutDirty = true;
}
}
private void OnMembersChanged(AllianceManager sender)
{
_layoutDirty = true;
}
private void UpdateBars(Vector2 contentStartPos)
{
var members = AllianceManager.Instance.GetMembersForOtherAlliance(Config.SlotIndex);
uint memberCount = (uint)members.Count;
uint row = 0, col = 0;
CalculateBarPosition(contentStartPos, Size, out float x, out float y);
for (int i = 0; i < bars.Count; i++)
{
var bar = bars[i];
if (i >= memberCount)
{
bar.Visible = false;
continue;
}
bar.Member = members[i];
bar.Visible = true;
bar.Position = new Vector2(
x + Configs.HealthBar.Size.X * col + (Configs.HealthBar.Padding.X - 1) * col,
y + Configs.HealthBar.Size.Y * row + (Configs.HealthBar.Padding.Y - 1) * row
);
if (Config.FillRowsFirst)
{
col++;
if (col >= _layoutInfo.TotalColCount) { col = 0; row++; }
}
else
{
row++;
if (row >= _layoutInfo.TotalRowCount) { row = 0; col++; }
}
}
}
private void UpdateBarsPosition(Vector2 delta)
{
foreach (var bar in bars)
bar.Position = bar.Position + delta;
}
private void CalculateBarPosition(Vector2 position, Vector2 spaceSize, out float x, out float y)
{
x = position.X;
y = position.Y;
if (Config.BarsAnchor == DrawAnchor.Top || Config.BarsAnchor == DrawAnchor.Center || Config.BarsAnchor == DrawAnchor.Bottom)
x += (spaceSize.X - _layoutInfo.ContentSize.X) / 2f;
else if (Config.BarsAnchor == DrawAnchor.TopRight || Config.BarsAnchor == DrawAnchor.Right || Config.BarsAnchor == DrawAnchor.BottomRight)
x += spaceSize.X - _layoutInfo.ContentSize.X;
if (Config.BarsAnchor == DrawAnchor.Left || Config.BarsAnchor == DrawAnchor.Center || Config.BarsAnchor == DrawAnchor.Right)
y += (spaceSize.Y - _layoutInfo.ContentSize.Y) / 2f;
else if (Config.BarsAnchor == DrawAnchor.BottomLeft || Config.BarsAnchor == DrawAnchor.Bottom || Config.BarsAnchor == DrawAnchor.BottomRight)
y += spaceSize.Y - _layoutInfo.ContentSize.Y;
}
private Vector2 Size => new Vector2(
Config.Columns * Configs.HealthBar.Size.X + (Config.Columns - 1) * Configs.HealthBar.Padding.X,
Config.Rows * Configs.HealthBar.Size.Y + (Config.Rows - 1) * Configs.HealthBar.Padding.Y
);
private void UpdateLayout(Vector2 origin)
{
var members = AllianceManager.Instance.GetMembersForOtherAlliance(Config.SlotIndex);
uint count = (uint)members.Count;
Vector2 contentStartPos = origin + Config.Position;
if (_layoutDirty || _memberCount != count)
{
_layoutInfo = LayoutHelper.CalculateLayout(
Size, Configs.HealthBar.Size, count, Configs.HealthBar.Padding, Config.FillRowsFirst
);
UpdateBars(contentStartPos);
}
else if (_origin != contentStartPos)
{
UpdateBarsPosition(contentStartPos - _origin);
}
_layoutDirty = false;
_origin = contentStartPos;
_memberCount = count;
}
public void StopPreview()
{
Config.Preview = false;
foreach (var bar in bars)
bar.StopPreview();
}
protected override (List<Vector2>, List<Vector2>) ChildrenPositionsAndSizes()
{
return (new List<Vector2>() { Config.Position + Size / 2f }, new List<Vector2>() { Size });
}
public void StopMouseover()
{
foreach (var bar in bars)
bar.StopMouseover();
}
public override void DrawChildren(Vector2 origin)
{
if (!_config.Enabled)
return;
bool inRaid = AllianceManager.Instance.IsInAllianceRaid;
var members = AllianceManager.Instance.GetMembersForOtherAlliance(Config.SlotIndex);
if (!inRaid && !Config.Preview && !Config.ShowWhenNotInRaid)
return;
if (members.Count < 1 && !Config.Preview && !Config.ShowWhenNotInRaid)
return;
// Show placeholder when not in raid (for positioning) or when Preview is on
if ((Config.Preview || Config.ShowWhenNotInRaid) && members.Count < 1)
{
_layoutInfo = LayoutHelper.CalculateLayout(Size, Configs.HealthBar.Size, 8, Configs.HealthBar.Padding, Config.FillRowsFirst);
for (int i = 0; i < 8; i++)
{
bars[i].Member = new FakePartyFramesMember(i);
bars[i].Visible = true;
}
for (int i = 8; i < bars.Count; i++)
bars[i].Visible = false;
CalculateBarPosition(origin + Config.Position, Size, out float px, out float py);
uint row = 0, col = 0;
for (int i = 0; i < 8; i++)
{
bars[i].Position = new Vector2(
px + Configs.HealthBar.Size.X * col + (Configs.HealthBar.Padding.X - 1) * col,
py + Configs.HealthBar.Size.Y * row + (Configs.HealthBar.Padding.Y - 1) * row
);
if (Config.FillRowsFirst) { col++; if (col >= _layoutInfo.TotalColCount) { col = 0; row++; } }
else { row++; if (row >= _layoutInfo.TotalRowCount) { row = 0; col++; } }
}
AddDrawAction(StrataLevel.LOWEST, () =>
{
var drawList = ImGui.GetWindowDrawList();
Vector2 bgPos = origin + Config.Position - _contentMargin;
Vector2 bgSize = Size + _contentMargin * 2;
drawList.AddRectFilled(bgPos, bgPos + bgSize, 0x66000000);
drawList.AddRect(bgPos, bgPos + bgSize, 0x66FFFFFF);
});
for (int i = 0; i < 8; i++)
AddDrawActions(bars[i].GetBarDrawActions(origin));
foreach (var bar in bars)
AddDrawActions(bar.GetElementsDrawActions(origin));
Config.TitleLabelConfig.SetText(Config.FramesLabel + " (Preview)");
AddDrawAction(Config.TitleLabelConfig.StrataLevel, () => _titleLabelHud.Draw(origin + Config.Position));
return;
}
if (Config.Preview)
{
AddDrawAction(StrataLevel.LOWEST, () =>
{
var drawList = ImGui.GetWindowDrawList();
Vector2 bgPos = origin + Config.Position - _contentMargin;
Vector2 bgSize = Size + _contentMargin * 2;
drawList.AddRectFilled(bgPos, bgPos + bgSize, 0x66000000);
drawList.AddRect(bgPos, bgPos + bgSize, 0x66FFFFFF);
});
}
UpdateLayout(origin);
IGameObject? target = Plugin.TargetManager.SoftTarget ?? Plugin.TargetManager.Target;
int targetIndex = -1;
for (int i = 0; i < members.Count; i++)
{
var member = bars[i].Member;
if (member != null && target != null && member.ObjectId == target.GameObjectId)
{
targetIndex = i;
continue;
}
AddDrawActions(bars[i].GetBarDrawActions(origin));
}
if (targetIndex >= 0)
AddDrawActions(bars[targetIndex].GetBarDrawActions(origin, Configs.HealthBar.ColorsConfig.TargetBordercolor));
foreach (var bar in bars)
AddDrawActions(bar.GetElementsDrawActions(origin));
string titleText = Config.FramesLabel;
if (inRaid && AllianceManager.Instance.TryGetAllianceIndexForSlot(Config.SlotIndex, out int allianceIdx)
&& AllianceManager.Instance.TryGetAllianceLetter(allianceIdx, out string letter))
{
titleText = $"Alliance {letter}";
}
Config.TitleLabelConfig.SetText(titleText);
AddDrawAction(Config.TitleLabelConfig.StrataLevel, () =>
{
_titleLabelHud.Draw(origin + Config.Position);
});
}
}
}
+422
View File
@@ -0,0 +1,422 @@
using Dalamud.Game.ClientState.Objects.Types;
using HSUI.Helpers;
using FFXIVClientStructs.FFXIV.Client.Game.Group;
using FFXIVClientStructs.FFXIV.Client.UI.Info;
using System;
using System.Collections.Generic;
using System.Linq;
namespace HSUI.Interface.Party
{
public delegate void AllianceMembersChangedEventHandler(AllianceManager sender);
/// <summary>
/// Provides member lists for Alliance A, B, and C when in 24-player raid content.
/// Only populated when CrossRealm has 3 groups (alliance raid).
/// </summary>
public unsafe class AllianceManager : IDisposable
{
public static AllianceManager Instance { get; private set; } = null!;
private readonly List<IPartyFramesMember>[] _allianceMembers = new List<IPartyFramesMember>[3];
private int[] _lastMemberCounts = new int[3];
private bool _wasInAllianceRaid = false;
private AllianceManager()
{
for (int i = 0; i < 3; i++)
_allianceMembers[i] = new List<IPartyFramesMember>();
}
public static void Initialize()
{
Instance = new AllianceManager();
}
public void Dispose()
{
for (int i = 0; i < 3; i++)
_allianceMembers[i].Clear();
Instance = null!;
}
/// <summary>Internal alliance index (0, 1, or 2) containing the local player, or -1 if not in alliance raid / unknown.</summary>
public int PlayerAllianceIndex
{
get
{
var player = Plugin.ObjectTable?.LocalPlayer;
if (player == null) return -1;
uint playerEntityId = player.EntityId;
ulong playerObjectId = (ulong)player.GameObjectId;
// Primary: Check our populated member lists first - most reliable (GroupManager/CrossRealm ordering may differ)
for (int i = 0; i < 3; i++)
{
foreach (var m in _allianceMembers[i])
{
if (m.ObjectId == playerEntityId || (ulong)m.ObjectId == playerObjectId) return i;
if (m.Character != null && m.Character.Address == player.Address) return i;
}
}
// Fallback: InfoProxyCrossRealm.LocalPlayerGroupIndex (when in duty finder / cross-world before instance)
var info = InfoProxyCrossRealm.Instance();
if (info != null && info->IsInAllianceRaid && info->GroupCount >= 3)
{
byte localIdx = info->LocalPlayerGroupIndex;
if (localIdx < 3) return localIdx;
}
// Fallback: CrossRealm iteration
if (info != null && info->IsInAllianceRaid && info->GroupCount >= 3)
{
for (int i = 0; i < 3; i++)
{
var group = info->CrossRealmGroups[i];
for (int j = 0; j < group.GroupMemberCount; j++)
{
var member = group.GroupMembers[j];
if (member.EntityId == playerEntityId) return i;
}
}
}
// Fallback 3: Match any of our party members - we're in the same alliance as them
var partyList = Plugin.PartyList;
if (partyList != null)
{
for (int i = 0; i < 3; i++)
{
foreach (var m in _allianceMembers[i])
{
foreach (var pm in partyList)
{
if (pm != null && pm.EntityId == m.ObjectId)
{
return i;
}
}
}
}
}
// Fallback: GroupManager - player is in MainGroup._partyMembers (their 8-man).
// Group layout: 0,1 = other alliances from _allianceMembers; 2 = main party from _partyMembers.
var gm = GroupManager.Instance();
if (gm != null && gm->MainGroup.IsAlliance)
{
ref var mainGroup = ref gm->MainGroup;
if (mainGroup.GetPartyMemberByEntityId(playerEntityId) != null)
{
return 2; // Player in main party = group 2 (our alliance)
}
}
return -1;
}
}
/// <summary>Slot 0 = first other alliance, slot 1 = second. Excludes the player's alliance.</summary>
public IReadOnlyList<IPartyFramesMember> GetMembersForOtherAlliance(int slotIndex)
{
if (slotIndex < 0 || slotIndex > 1) return Array.Empty<IPartyFramesMember>();
if (!TryGetAllianceIndexForSlot(slotIndex, out int allianceIdx)) return Array.Empty<IPartyFramesMember>();
return _allianceMembers[allianceIdx].AsReadOnly();
}
/// <summary>Gets the alliance index (0=A, 1=B, 2=C) for the given slot. Returns false when not in raid.</summary>
public bool TryGetAllianceIndexForSlot(int slotIndex, out int allianceIndex)
{
allianceIndex = -1;
if (slotIndex < 0 || slotIndex > 1) return false;
int playerIdx = PlayerAllianceIndex;
int idx = 0;
for (int i = 0; i < 3; i++)
{
if (i == playerIdx) continue;
if (idx == slotIndex)
{
allianceIndex = i;
return true;
}
idx++;
}
return false;
}
/// <summary>Gets the alliance letter (A/B/C) for an internal group index. Uses member EntityId matching to CrossRealm when GroupManager/CrossRealm use different orderings.</summary>
public bool TryGetAllianceLetter(int allianceIndex, out string letter)
{
letter = "";
if (allianceIndex < 0 || allianceIndex > 2) return false;
var info = InfoProxyCrossRealm.Instance();
if (info != null && info->GroupCount >= 3)
{
// Get first member EntityId from our group (works for both GroupManager and CrossRealm data)
uint matchEntityId = 0;
if (_allianceMembers[allianceIndex].Count > 0)
matchEntityId = _allianceMembers[allianceIndex][0].ObjectId;
// Find which CrossRealm group contains this member - they may use different ordering than GroupManager
for (int crIdx = 0; crIdx < 3 && matchEntityId != 0; crIdx++)
{
var crGroup = info->CrossRealmGroups[crIdx];
for (int j = 0; j < crGroup.GroupMemberCount; j++)
{
if (crGroup.GroupMembers[j].EntityId == matchEntityId)
{
byte displayIdx = crGroup.GroupMembers[j].GroupIndex;
if (displayIdx < 3)
{
letter = ((char)('A' + displayIdx)).ToString();
return true;
}
}
}
}
// Direct lookup when CrossRealm index matches ours (e.g. both use same ordering)
var group = info->CrossRealmGroups[allianceIndex];
if (group.GroupMemberCount > 0)
{
byte displayIdx = group.GroupMembers[0].GroupIndex;
if (displayIdx < 3)
{
letter = ((char)('A' + displayIdx)).ToString();
return true;
}
}
// GetGroupIndex(displayLetter) returns internal index - find which letter maps to our index
for (byte d = 0; d < 3; d++)
{
if (InfoProxyCrossRealm.GetGroupIndex(d) == allianceIndex)
{
letter = ((char)('A' + d)).ToString();
return true;
}
}
}
// Fallback when no CrossRealm data (PvP, in-instance)
letter = ((char)('A' + allianceIndex)).ToString();
return true;
}
/// <summary>True when in 24-player content with 3 alliances (PvE) or PvP Frontlines/Rival Wings.</summary>
public bool IsInAllianceRaid
{
get
{
var gm = GroupManager.Instance();
if (gm != null)
{
// GroupManager: IsAlliance = 3x8 (alliance raids, Frontlines), IsSmallGroupAlliance = 6x4 (Rival Wings)
if (gm->MainGroup.IsAlliance || gm->MainGroup.IsSmallGroupAlliance)
return true;
}
var info = InfoProxyCrossRealm.Instance();
if (info == null) return false;
if (info->IsInAllianceRaid) return true;
return info->IsCrossRealm && info->GroupCount >= 3;
}
}
public event AllianceMembersChangedEventHandler? MembersChangedEvent;
public void Update()
{
var gm = GroupManager.Instance();
// Use GroupManager for PvE alliance raids and PvP (Frontlines, Rival Wings) - CrossRealm is not populated in PvP
bool useGroupManager = gm != null && (gm->MainGroup.IsAlliance || gm->MainGroup.IsSmallGroupAlliance);
if (useGroupManager)
{
UpdateFromGroupManager(gm);
return;
}
var info = InfoProxyCrossRealm.Instance();
if (info == null || !info->IsInAllianceRaid || info->GroupCount < 3)
{
if (_wasInAllianceRaid)
{
_wasInAllianceRaid = false;
for (int i = 0; i < 3; i++)
_allianceMembers[i].Clear();
MembersChangedEvent?.Invoke(this);
}
return;
}
_wasInAllianceRaid = true;
UpdateFromCrossRealm(info);
}
private void UpdateFromGroupManager(GroupManager* gm)
{
_wasInAllianceRaid = true;
bool anyChanged = false;
ref var mainGroup = ref gm->MainGroup;
for (int allianceIdx = 0; allianceIdx < 3; allianceIdx++)
{
int count = 0;
var list = new List<IPartyFramesMember>();
for (int slot = 0; slot < 8; slot++)
{
var pm = mainGroup.GetAllianceMemberByGroupAndIndex(allianceIdx, slot);
if (pm == null || pm->EntityId == 0) continue;
var pfMember = new PartyFramesMember(
pm->EntityId,
count,
count,
EnmityLevel.Last,
PartyMemberStatus.None,
ReadyCheckStatus.None,
false,
false
);
pfMember.Update(EnmityLevel.Last, PartyMemberStatus.None, ReadyCheckStatus.None, false, pm->ClassJob);
list.Add(pfMember);
count++;
}
if (count != _lastMemberCounts[allianceIdx])
{
_allianceMembers[allianceIdx].Clear();
_allianceMembers[allianceIdx].AddRange(list);
_lastMemberCounts[allianceIdx] = count;
anyChanged = true;
}
else
{
int ourIdx = 0;
for (int slot = 0; slot < 8 && ourIdx < _allianceMembers[allianceIdx].Count; slot++)
{
var pm = mainGroup.GetAllianceMemberByGroupAndIndex(allianceIdx, slot);
if (pm == null || pm->EntityId == 0) continue;
if (_allianceMembers[allianceIdx][ourIdx] is PartyFramesMember pfMember)
pfMember.Update(EnmityLevel.Last, PartyMemberStatus.None, ReadyCheckStatus.None, false, pm->ClassJob);
ourIdx++;
}
}
}
if (anyChanged)
MembersChangedEvent?.Invoke(this);
}
/// <summary>Dumps alliance detection and letter-mapping debug info to the log. Run /hsui debug alliance while in an alliance raid.</summary>
public static void DumpAllianceDebugToLog()
{
var inst = Instance;
if (inst == null) { Plugin.Logger.Information("[HSUI Alliance DBG] AllianceManager not initialized."); return; }
var player = Plugin.ObjectTable?.LocalPlayer;
uint playerEntityId = player?.EntityId ?? 0;
string playerName = player?.Name.ToString() ?? "(null)";
Plugin.Logger.Information("=== HSUI Alliance Debug ===");
Plugin.Logger.Information($"[Alliance] IsInAllianceRaid={inst.IsInAllianceRaid}");
Plugin.Logger.Information($"[Alliance] Player: EntityId={playerEntityId} Name={playerName}");
var gm = GroupManager.Instance();
bool useGM = gm != null && (gm->MainGroup.IsAlliance || gm->MainGroup.IsSmallGroupAlliance);
Plugin.Logger.Information($"[Alliance] Data source: GroupManager={useGM} (IsAlliance={gm != null && gm->MainGroup.IsAlliance}, IsSmallGroupAlliance={gm != null && gm->MainGroup.IsSmallGroupAlliance})");
var info = InfoProxyCrossRealm.Instance();
if (info != null)
{
Plugin.Logger.Information($"[Alliance] InfoProxyCrossRealm: GroupCount={info->GroupCount} IsInAllianceRaid={info->IsInAllianceRaid} LocalPlayerGroupIndex={info->LocalPlayerGroupIndex}");
if (info->GroupCount >= 3)
{
for (byte d = 0; d < 3; d++)
Plugin.Logger.Information($"[Alliance] GetGroupIndex({d}) => internal index {InfoProxyCrossRealm.GetGroupIndex(d)}");
}
}
else
Plugin.Logger.Information("[Alliance] InfoProxyCrossRealm: null");
int playerIdx = inst.PlayerAllianceIndex;
Plugin.Logger.Information($"[Alliance] PlayerAllianceIndex={playerIdx} (internal index we treat as 'our' alliance, will exclude from display)");
for (int i = 0; i < 3; i++)
{
int count = inst._allianceMembers[i].Count;
string firstEntityId = count > 0 ? inst._allianceMembers[i][0].ObjectId.ToString() : "none";
string firstName = count > 0 ? inst._allianceMembers[i][0].Name : "";
bool hasPlayer = playerEntityId != 0 && inst._allianceMembers[i].Any(m => m.ObjectId == playerEntityId);
string letter = inst.TryGetAllianceLetter(i, out string l) ? l : "?";
Plugin.Logger.Information($"[Alliance] _allianceMembers[{i}]: count={count} firstEntityId={firstEntityId} firstName={firstName} hasPlayer={hasPlayer} resolvedLetter={letter}");
}
if (info != null && info->GroupCount >= 3)
{
for (int crIdx = 0; crIdx < 3; crIdx++)
{
var grp = info->CrossRealmGroups[crIdx];
int gc = grp.GroupMemberCount;
string crFirstEntity = gc > 0 ? grp.GroupMembers[0].EntityId.ToString() : "none";
string crFirstName = gc > 0 ? grp.GroupMembers[0].NameString : "";
byte crGroupIndex = gc > 0 ? grp.GroupMembers[0].GroupIndex : (byte)255;
char crLetter = crGroupIndex < 3 ? (char)('A' + crGroupIndex) : '?';
Plugin.Logger.Information($"[Alliance] CrossRealmGroups[{crIdx}]: count={gc} firstEntityId={crFirstEntity} firstName={crFirstName} GroupIndex={crGroupIndex} => letter '{crLetter}'");
}
}
for (int slot = 0; slot <= 1; slot++)
{
if (inst.TryGetAllianceIndexForSlot(slot, out int aidx) && inst.TryGetAllianceLetter(aidx, out string let))
Plugin.Logger.Information($"[Alliance] Display slot {slot}: allianceIndex={aidx} letter={let}");
else
Plugin.Logger.Information($"[Alliance] Display slot {slot}: no data");
}
Plugin.Logger.Information("=== End Alliance Debug ===");
}
private void UpdateFromCrossRealm(InfoProxyCrossRealm* info)
{
bool anyChanged = false;
for (int allianceIdx = 0; allianceIdx < 3; allianceIdx++)
{
var group = info->CrossRealmGroups[allianceIdx];
int count = group.GroupMemberCount;
if (count != _lastMemberCounts[allianceIdx])
{
_allianceMembers[allianceIdx].Clear();
_lastMemberCounts[allianceIdx] = count;
for (int i = 0; i < count; i++)
{
CrossRealmMember member = group.GroupMembers[i];
var partyMember = new PartyFramesMember(
member,
i,
i,
PartyMemberStatus.None,
ReadyCheckStatus.None,
member.IsPartyLeader,
false
);
_allianceMembers[allianceIdx].Add(partyMember);
}
anyChanged = true;
}
else
{
for (int i = 0; i < _allianceMembers[allianceIdx].Count; i++)
{
if (_allianceMembers[allianceIdx][i] is PartyFramesMember pfMember)
{
pfMember.Update(EnmityLevel.Last, PartyMemberStatus.None, ReadyCheckStatus.None, pfMember.IsPartyLeader, pfMember.JobId);
}
}
}
}
if (anyChanged)
MembersChangedEvent?.Invoke(this);
}
}
}
+60 -57
View File
@@ -576,82 +576,85 @@ namespace HSUI.Interface.Party
}
));
// name
bool drawName = ShouldDrawName(character, showingRaise, showingInvuln);
if (drawName)
if (_configs.HealthBar.ShowLabels)
{
drawActions.Add((_configs.HealthBar.NameLabelConfig.StrataLevel, () =>
// name
bool drawName = ShouldDrawName(character, showingRaise, showingInvuln);
if (drawName)
{
bool? playerName = null;
if (character == null || character.ObjectKind == ObjectKind.Player)
drawActions.Add((_configs.HealthBar.NameLabelConfig.StrataLevel, () =>
{
playerName = true;
bool? playerName = null;
if (character == null || character.ObjectKind == ObjectKind.Player)
{
playerName = true;
}
_nameLabelHud.Draw(Position, _configs.HealthBar.Size, character, Member.Name, isPlayerName: playerName);
}
_nameLabelHud.Draw(Position, _configs.HealthBar.Size, character, Member.Name, isPlayerName: playerName);
));
}
));
}
// health label
if (Member.MaxHP > 0)
{
drawActions.Add((_configs.HealthBar.HealthLabelConfig.StrataLevel, () =>
// health label
if (Member.MaxHP > 0)
{
_healthLabelHud.Draw(Position, _configs.HealthBar.Size, character, null, Member.HP, Member.MaxHP);
drawActions.Add((_configs.HealthBar.HealthLabelConfig.StrataLevel, () =>
{
_healthLabelHud.Draw(Position, _configs.HealthBar.Size, character, null, Member.HP, Member.MaxHP);
}
));
}
));
}
// order
if (character == null || character?.ObjectKind != ObjectKind.BattleNpc)
{
string str = char.ConvertFromUtf32(0xE090 + Member.Order).ToString();
drawActions.Add((_configs.HealthBar.OrderNumberConfig.StrataLevel, () =>
// order
if (character == null || character?.ObjectKind != ObjectKind.BattleNpc)
{
_configs.HealthBar.OrderNumberConfig.SetText(str);
_orderLabelHud.Draw(Position, _configs.HealthBar.Size, character);
}
));
}
string str = char.ConvertFromUtf32(0xE090 + Member.Order).ToString();
// status
string? statusString = StringForStatus(Member.Status);
if (PlayerStatus.Enabled && PlayerStatus.Label.Enabled && statusString != null)
{
drawActions.Add((PlayerStatus.Label.StrataLevel, () =>
drawActions.Add((_configs.HealthBar.OrderNumberConfig.StrataLevel, () =>
{
_configs.HealthBar.OrderNumberConfig.SetText(str);
_orderLabelHud.Draw(Position, _configs.HealthBar.Size, character);
}
));
}
// status
string? statusString = StringForStatus(Member.Status);
if (PlayerStatus.Enabled && PlayerStatus.Label.Enabled && statusString != null)
{
PlayerStatus.Label.SetText(statusString);
_statusLabelHud.Draw(Position, _configs.HealthBar.Size);
drawActions.Add((PlayerStatus.Label.StrataLevel, () =>
{
PlayerStatus.Label.SetText(statusString);
_statusLabelHud.Draw(Position, _configs.HealthBar.Size);
}
));
}
));
}
// raise label
if (showingRaise)
{
float duration = Math.Abs(Member.RaiseTime!.Value);
drawActions.Add((RaiseTracker.Icon.NumericLabel.StrataLevel, () =>
// raise label
if (showingRaise)
{
RaiseTracker.Icon.NumericLabel.SetValue(duration);
_raiseLabelHud.Draw(Position, _configs.HealthBar.Size);
float duration = Math.Abs(Member.RaiseTime!.Value);
drawActions.Add((RaiseTracker.Icon.NumericLabel.StrataLevel, () =>
{
RaiseTracker.Icon.NumericLabel.SetValue(duration);
_raiseLabelHud.Draw(Position, _configs.HealthBar.Size);
}
));
}
));
}
// invuln label
if (showingInvuln)
{
float duration = Math.Abs(Member.InvulnStatus!.InvulnTime);
drawActions.Add((InvulnTracker.Icon.NumericLabel.StrataLevel, () =>
// invuln label
if (showingInvuln)
{
InvulnTracker.Icon.NumericLabel.SetValue(duration);
_invulnLabelHud.Draw(Position, _configs.HealthBar.Size);
float duration = Math.Abs(Member.InvulnStatus!.InvulnTime);
drawActions.Add((InvulnTracker.Icon.NumericLabel.StrataLevel, () =>
{
InvulnTracker.Icon.NumericLabel.SetValue(duration);
_invulnLabelHud.Draw(Position, _configs.HealthBar.Size);
}
));
}
));
}
return drawActions;
+4
View File
@@ -88,6 +88,10 @@ namespace HSUI.Interface.Party
[Order(31)]
public Vector2 Padding = new Vector2(0, 0);
[Checkbox("Show Labels", help = "Show name, health, order number, and status text on the bar.")]
[Order(32)]
public bool ShowLabels = true;
[NestedConfig("Name Label", 44)]
public EditableLabelConfig NameLabelConfig = new EditableLabelConfig(Vector2.Zero, "[name:initials].", DrawAnchor.Center, DrawAnchor.Center);
+13 -1
View File
@@ -459,7 +459,19 @@ namespace HSUI.Interface.Party
);
}
public static PartyFramesConfigs GetAllianceConfigs()
{
return new PartyFramesConfigs(
ConfigurationManager.Instance.GetConfigObject<AllianceFramesHealthBarsConfig>(),
ConfigurationManager.Instance.GetConfigObject<AllianceFramesManaBarConfig>(),
ConfigurationManager.Instance.GetConfigObject<AllianceFramesCastbarConfig>(),
ConfigurationManager.Instance.GetConfigObject<AllianceFramesIconsConfig>(),
ConfigurationManager.Instance.GetConfigObject<AllianceFramesBuffsConfig>(),
ConfigurationManager.Instance.GetConfigObject<AllianceFramesDebuffsConfig>(),
ConfigurationManager.Instance.GetConfigObject<AllianceFramesTrackersConfig>(),
ConfigurationManager.Instance.GetConfigObject<AllianceFramesCooldownListConfig>()
);
}
}
#endregion
}