Initial release: HSUI v1.0.0.0 - HUD replacement with configurable hotbars

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-01-30 23:52:46 -05:00
commit f37369cdda
202 changed files with 40137 additions and 0 deletions
+748
View File
@@ -0,0 +1,748 @@
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Statuses;
using Dalamud.Interface.Textures.TextureWraps;
using HSUI.Config;
using HSUI.Enums;
using HSUI.Helpers;
using HSUI.Interface.Bars;
using HSUI.Interface.GeneralElements;
using HSUI.Interface.StatusEffects;
using Dalamud.Bindings.ImGui;
using System;
using System.Collections.Generic;
using System.Numerics;
namespace HSUI.Interface.Party
{
public class PartyFramesBar
{
public delegate void PartyFramesBarEventHandler(PartyFramesBar bar);
public PartyFramesBarEventHandler? OpenContextMenuEvent;
private PartyFramesConfigs _configs;
private LabelHud _nameLabelHud;
private LabelHud _healthLabelHud;
private LabelHud _orderLabelHud;
private LabelHud _statusLabelHud;
private LabelHud _raiseLabelHud;
private LabelHud _invulnLabelHud;
private PrimaryResourceHud _manaBarHud;
private CastbarHud _castbarHud;
private StatusEffectsListHud _buffsListHud;
private StatusEffectsListHud _debuffsListHud;
private PartyFramesCooldownListHud _cooldownListHud;
private IDalamudTextureWrap? _readyCheckTexture =>
TexturesHelper.GetTextureFromPath("ui/uld/ReadyCheck_hr1.tex") ??
TexturesHelper.GetTextureFromPath("ui/uld/ReadyCheck.tex");
public bool Visible = false;
public Vector2 Position;
private SmoothHPHelper _smoothHPHelper = new SmoothHPHelper();
private bool _wasHovering = false;
public IPartyFramesMember? Member;
public PartyFramesBar(string id, PartyFramesConfigs configs)
{
_configs = configs;
_nameLabelHud = new LabelHud(_configs.HealthBar.NameLabelConfig);
_healthLabelHud = new LabelHud(_configs.HealthBar.HealthLabelConfig);
_orderLabelHud = new LabelHud(_configs.HealthBar.OrderNumberConfig);
_statusLabelHud = new LabelHud(PlayerStatus.Label);
_raiseLabelHud = new LabelHud(RaiseTracker.Icon.NumericLabel);
_invulnLabelHud = new LabelHud(InvulnTracker.Icon.NumericLabel);
_manaBarHud = new PrimaryResourceHud(_configs.ManaBar);
_castbarHud = new CastbarHud(_configs.CastBar);
_buffsListHud = new StatusEffectsListHud(_configs.Buffs);
_debuffsListHud = new StatusEffectsListHud(_configs.Debuffs);
_cooldownListHud = new PartyFramesCooldownListHud(_configs.CooldownList);
}
public PluginConfigColor GetColor(float scale)
{
if (Member == null || Member.MaxHP <= 0)
{
return _configs.HealthBar.ColorsConfig.OutOfReachBackgroundColor;
}
bool cleanseCheck = true;
if (CleanseTracker.CleanseJobsOnly)
{
cleanseCheck = Utils.IsOnCleanseJob();
}
if (CleanseTracker.Enabled && CleanseTracker.ChangeHealthBarCleanseColor && Member.HasDispellableDebuff && cleanseCheck)
{
return CleanseTracker.HealthBarColor;
}
else if (_configs.HealthBar.ColorsConfig.ColorByHealth.Enabled)
{
if (_configs.HealthBar.ColorsConfig.ColorByHealth.UseJobColorAsMaxHealth)
{
return ColorUtils.GetColorByScale(scale, _configs.HealthBar.ColorsConfig.ColorByHealth.LowHealthColorThreshold / 100f, _configs.HealthBar.ColorsConfig.ColorByHealth.FullHealthColorThreshold / 100f, _configs.HealthBar.ColorsConfig.ColorByHealth.LowHealthColor, _configs.HealthBar.ColorsConfig.ColorByHealth.FullHealthColor,
GlobalColors.Instance.SafeColorForJobId(Member.JobId), _configs.HealthBar.ColorsConfig.ColorByHealth.UseMaxHealthColor, _configs.HealthBar.ColorsConfig.ColorByHealth.BlendMode);
}
else if (_configs.HealthBar.ColorsConfig.ColorByHealth.UseRoleColorAsMaxHealth)
{
return ColorUtils.GetColorByScale(scale, _configs.HealthBar.ColorsConfig.ColorByHealth.LowHealthColorThreshold / 100f, _configs.HealthBar.ColorsConfig.ColorByHealth.FullHealthColorThreshold / 100f, _configs.HealthBar.ColorsConfig.ColorByHealth.LowHealthColor, _configs.HealthBar.ColorsConfig.ColorByHealth.FullHealthColor,
GlobalColors.Instance.SafeRoleColorForJobId(Member.JobId), _configs.HealthBar.ColorsConfig.ColorByHealth.UseMaxHealthColor, _configs.HealthBar.ColorsConfig.ColorByHealth.BlendMode);
}
return ColorUtils.GetColorByScale(scale, _configs.HealthBar.ColorsConfig.ColorByHealth);
}
else if (Member.JobId > 0)
{
return _configs.HealthBar.ColorsConfig.UseRoleColors switch
{
true => GlobalColors.Instance.SafeRoleColorForJobId(Member.JobId),
_ => GlobalColors.Instance.SafeColorForJobId(Member.JobId)
};
}
return Member.Character?.ObjectKind switch
{
ObjectKind.BattleNpc => GlobalColors.Instance.NPCFriendlyColor,
_ => _configs.HealthBar.ColorsConfig.OutOfReachBackgroundColor
};
}
public void StopPreview()
{
_castbarHud.StopPreview();
_buffsListHud.StopPreview();
_debuffsListHud.StopPreview();
_cooldownListHud.StopPreview();
_configs.HealthBar.MouseoverAreaConfig.Preview = false;
}
public void StopMouseover()
{
if (_wasHovering)
{
InputsHelper.Instance.ClearTarget();
_wasHovering = false;
}
}
public void Dispose()
{
_cooldownListHud.Dispose();
}
public List<(StrataLevel, Action)> GetBarDrawActions(Vector2 origin, PluginConfigColor? borderColor = null)
{
List<(StrataLevel, Action)> drawActions = new List<(StrataLevel, Action)>();
if (!Visible || Member is null)
{
StopMouseover();
return drawActions;
}
// click
var (areaStart, areaEnd) = _configs.HealthBar.MouseoverAreaConfig.GetArea(Position, _configs.HealthBar.Size);
bool isHovering = ImGui.IsMouseHoveringRect(areaStart, areaEnd);
bool ignoreMouseover = _configs.HealthBar.MouseoverAreaConfig.Enabled && _configs.HealthBar.MouseoverAreaConfig.Ignore;
ICharacter? character = Member.Character;
if (isHovering)
{
_wasHovering = true;
InputsHelper.Instance.SetTarget(character, ignoreMouseover);
// left click
if (InputsHelper.Instance.LeftButtonClicked && character != null)
{
Plugin.TargetManager.Target = character;
}
// right click (context menu)
else if (InputsHelper.Instance.RightButtonClicked)
{
OpenContextMenuEvent?.Invoke(this);
}
}
else if (_wasHovering)
{
InputsHelper.Instance.ClearTarget();
_wasHovering = false;
}
// bg
PluginConfigColor bgColor = _configs.HealthBar.ColorsConfig.BackgroundColor;
if (Member.RaiseTime != null && RaiseTracker.Enabled && RaiseTracker.ChangeBackgroundColorWhenRaised)
{
bgColor = RaiseTracker.BackgroundColor;
}
else if (Member.InvulnStatus?.InvulnTime != null && InvulnTracker.Enabled && InvulnTracker.ChangeBackgroundColorWhenInvuln)
{
bgColor = Member.InvulnStatus?.InvulnId == 811 ? InvulnTracker.WalkingDeadBackgroundColor : InvulnTracker.BackgroundColor;
}
else if (_configs.HealthBar.ColorsConfig.UseDeathIndicatorBackgroundColor && Member.HP <= 0 && character != null)
{
bgColor = _configs.HealthBar.RangeConfig.Enabled
? GetDistanceColor(character, _configs.HealthBar.ColorsConfig.DeathIndicatorBackgroundColor)
: _configs.HealthBar.ColorsConfig.DeathIndicatorBackgroundColor;
}
else if (_configs.HealthBar.ColorsConfig.UseJobColorAsBackgroundColor && character is IBattleChara)
{
bgColor = GlobalColors.Instance.SafeColorForJobId(character.ClassJob.RowId);
}
else if (_configs.HealthBar.ColorsConfig.UseRoleColorAsBackgroundColor && character is IBattleChara)
{
bgColor = _configs.HealthBar.RangeConfig.Enabled
? GetDistanceColor(character, GlobalColors.Instance.SafeRoleColorForJobId(character.ClassJob.RowId))
: GlobalColors.Instance.SafeRoleColorForJobId(character.ClassJob.RowId);
}
Rect background = new Rect(Position, _configs.HealthBar.Size, bgColor);
// hp
uint currentHp = Member.HP;
uint maxHp = Member.MaxHP;
if (_configs.HealthBar.SmoothHealthConfig.Enabled)
{
currentHp = _smoothHPHelper.GetNextHp((int)currentHp, (int)maxHp, _configs.HealthBar.SmoothHealthConfig.Velocity);
}
float hpScale = maxHp > 0 ? (float)currentHp / (float)maxHp : 1;
PluginConfigColor? hpColor = _configs.HealthBar.RangeConfig.Enabled && character != null
? GetDistanceColor(character, GetColor(hpScale))
: GetColor(hpScale);
Rect healthFill = BarUtilities.GetFillRect(Position, _configs.HealthBar.Size, _configs.HealthBar.FillDirection, hpColor, currentHp, maxHp);
// bar
int thickness = borderColor != null ? _configs.HealthBar.ColorsConfig.ActiveBorderThickness : _configs.HealthBar.ColorsConfig.InactiveBorderThickness;
if (WhosTalkingIcon.ChangeBorders && Member.WhosTalkingState != WhosTalkingState.None)
{
thickness = WhosTalkingIcon.BorderThickness;
}
borderColor = borderColor ?? GetBorderColor(character);
BarHud bar = new BarHud(
_configs.HealthBar.ID,
_configs.HealthBar.ColorsConfig.ShowBorder,
borderColor,
thickness,
actor: character,
current: currentHp,
max: maxHp,
shadowConfig: _configs.HealthBar.ShadowConfig,
barTextureName: _configs.HealthBar.BarTextureName,
barTextureDrawMode: _configs.HealthBar.BarTextureDrawMode
);
bar.NeedsInputs = true;
bar.SetBackground(background);
bar.AddForegrounds(healthFill);
// missing health
if (_configs.HealthBar.ColorsConfig.UseMissingHealthBar)
{
Vector2 healthMissingSize = _configs.HealthBar.Size - BarUtilities.GetFillDirectionOffset(healthFill.Size, _configs.HealthBar.FillDirection);
Vector2 healthMissingPos = _configs.HealthBar.FillDirection.IsInverted() ? Position : Position + BarUtilities.GetFillDirectionOffset(healthFill.Size, _configs.HealthBar.FillDirection);
PluginConfigColor? missingHealthColor = _configs.HealthBar.ColorsConfig.UseJobColorAsMissingHealthColor && character is IBattleChara
? GlobalColors.Instance.SafeColorForJobId(character!.ClassJob.RowId)
: _configs.HealthBar.ColorsConfig.UseRoleColorAsMissingHealthColor && character is IBattleChara
? GlobalColors.Instance.SafeRoleColorForJobId(character!.ClassJob.RowId)
: _configs.HealthBar.ColorsConfig.HealthMissingColor;
if (_configs.HealthBar.ColorsConfig.UseDeathIndicatorBackgroundColor && Member.HP <= 0 && character != null)
{
missingHealthColor = _configs.HealthBar.ColorsConfig.DeathIndicatorBackgroundColor;
}
if (_configs.Trackers.Invuln.ChangeBackgroundColorWhenInvuln && character is IBattleChara battleChara)
{
IStatus? tankInvuln = Utils.GetTankInvulnerabilityID(battleChara);
if (tankInvuln is not null)
{
missingHealthColor = _configs.Trackers.Invuln.BackgroundColor;
}
}
if (_configs.HealthBar.RangeConfig.Enabled)
{
missingHealthColor = GetDistanceColor(character, missingHealthColor);
}
bar.AddForegrounds(new Rect(healthMissingPos, healthMissingSize, missingHealthColor));
}
// shield
if (_configs.HealthBar.ShieldConfig.Enabled)
{
if (Member.Shield > 0f)
{
bar.AddForegrounds(
BarUtilities.GetShieldForeground(
_configs.HealthBar.ShieldConfig,
Position,
_configs.HealthBar.Size,
healthFill.Size,
_configs.HealthBar.FillDirection,
Member.Shield,
currentHp,
maxHp
)
);
}
}
// highlight
bool isSoftTarget = character != null && character == Plugin.TargetManager.SoftTarget;
if (_configs.HealthBar.ColorsConfig.ShowHighlight && (isHovering || isSoftTarget))
{
Rect highlight = new Rect(Position, _configs.HealthBar.Size, _configs.HealthBar.ColorsConfig.HighlightColor);
bar.AddForegrounds(highlight);
}
drawActions = bar.GetDrawActions(Vector2.Zero, _configs.HealthBar.StrataLevel);
// mouseover area
BarHud? mouseoverAreaBar = _configs.HealthBar.MouseoverAreaConfig.GetBar(
Position,
_configs.HealthBar.Size,
_configs.HealthBar.ID + "_mouseoverArea"
);
if (mouseoverAreaBar != null)
{
drawActions.AddRange(mouseoverAreaBar.GetDrawActions(Vector2.Zero, StrataLevel.HIGHEST));
}
return drawActions;
}
private PluginConfigColor GetBorderColor(ICharacter? character)
{
IGameObject? target = Plugin.TargetManager.Target ?? Plugin.TargetManager.SoftTarget;
return character != null && character == target ? _configs.HealthBar.ColorsConfig.TargetBordercolor : _configs.HealthBar.ColorsConfig.BorderColor;
}
private PluginConfigColor GetDistanceColor(ICharacter? character, PluginConfigColor color)
{
byte distance = character != null ? character.YalmDistanceX : byte.MaxValue;
float currentAlpha = color.Vector.W * 100f;
float alpha = _configs.HealthBar.RangeConfig.AlphaForDistance(distance, currentAlpha) / 100f;
return color.WithAlpha(alpha);
}
// need to separate elements that have their own window so clipping doesn't get messy
public List<(StrataLevel, Action)> GetElementsDrawActions(Vector2 origin)
{
List<(StrataLevel, Action)> drawActions = new List<(StrataLevel, Action)>();
IPlayerCharacter? player = Plugin.ObjectTable.LocalPlayer;
if (!Visible || Member is null || player == null)
{
StopMouseover();
return drawActions;
}
ICharacter? character = Member.Character;
// who's talking
bool drawingWhosTalking = false;
if (WhosTalkingIcon.Enabled && WhosTalkingIcon.Icon.Enabled && WhosTalkingIcon.EnabledForState(Member.WhosTalkingState))
{
IDalamudTextureWrap? texture = WhosTalkingHelper.Instance.GetTextureForState(Member.WhosTalkingState);
if (texture != null)
{
Vector2 parentPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, WhosTalkingIcon.Icon.FrameAnchor);
Vector2 iconPos = Utils.GetAnchoredPosition(parentPos + WhosTalkingIcon.Icon.Position, WhosTalkingIcon.Icon.Size, WhosTalkingIcon.Icon.Anchor);
drawActions.Add((WhosTalkingIcon.Icon.StrataLevel, () =>
{
DrawHelper.DrawInWindow(WhosTalkingIcon.Icon.ID, iconPos, WhosTalkingIcon.Icon.Size, false, (drawList) =>
{
ImGui.SetCursorPos(iconPos);
ImGui.Image(texture.Handle, WhosTalkingIcon.Icon.Size);
});
}
));
drawingWhosTalking = true;
}
}
// role/job icon
if (RoleIcon.Enabled && (!drawingWhosTalking || !WhosTalkingIcon.ReplaceRoleJobIcon))
{
uint iconId = 0;
// chocobo icon
if (character is IBattleNpc battleNpc && battleNpc.BattleNpcKind == BattleNpcSubKind.Chocobo)
{
iconId = JobsHelper.RoleIconIDForBattleCompanion + (uint)RoleIcon.Style * 100;
}
// role/job icon
else if (Member.JobId > 0)
{
iconId = RoleIcon.UseRoleIcons ?
JobsHelper.RoleIconIDForJob(Member.JobId, RoleIcon.UseSpecificDPSRoleIcons) :
JobsHelper.IconIDForJob(Member.JobId, (uint)RoleIcon.Style);
}
if (iconId > 0)
{
Vector2 parentPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, RoleIcon.FrameAnchor);
Vector2 iconPos = Utils.GetAnchoredPosition(parentPos + RoleIcon.Position, RoleIcon.Size, RoleIcon.Anchor);
drawActions.Add((RoleIcon.StrataLevel, () =>
{
DrawHelper.DrawInWindow(RoleIcon.ID, iconPos, RoleIcon.Size, false, (drawList) =>
{
DrawHelper.DrawIcon(iconId, iconPos, RoleIcon.Size, false, drawList);
});
}
));
}
}
// sign icon
if (SignIcon.Enabled)
{
uint? iconId = SignIcon.IconID(character);
if (iconId.HasValue)
{
Vector2 parentPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, SignIcon.FrameAnchor);
Vector2 iconPos = Utils.GetAnchoredPosition(parentPos + SignIcon.Position, SignIcon.Size, SignIcon.Anchor);
drawActions.Add((SignIcon.StrataLevel, () =>
{
DrawHelper.DrawInWindow(SignIcon.ID, iconPos, SignIcon.Size, false, (drawList) =>
{
DrawHelper.DrawIcon(iconId.Value, iconPos, SignIcon.Size, false, drawList);
});
}
));
}
}
// leader icon
if (LeaderIcon.Enabled && Member.IsPartyLeader)
{
Vector2 parentPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, LeaderIcon.FrameAnchor);
Vector2 iconPos = Utils.GetAnchoredPosition(parentPos + LeaderIcon.Position, LeaderIcon.Size, LeaderIcon.Anchor);
drawActions.Add((LeaderIcon.StrataLevel, () =>
{
DrawHelper.DrawInWindow(LeaderIcon.ID, iconPos, LeaderIcon.Size, false, (drawList) =>
{
DrawHelper.DrawIcon(61521, iconPos, LeaderIcon.Size, false, drawList);
});
}
));
}
// player status icon
if (PlayerStatus.Enabled && PlayerStatus.Icon.Enabled)
{
uint? iconId = IconIdForStatus(Member.Status);
if (iconId.HasValue)
{
Vector2 parentPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, PlayerStatus.Icon.FrameAnchor);
Vector2 iconPos = Utils.GetAnchoredPosition(parentPos + PlayerStatus.Icon.Position, PlayerStatus.Icon.Size, PlayerStatus.Icon.Anchor);
drawActions.Add((PlayerStatus.Icon.StrataLevel, () =>
{
DrawHelper.DrawInWindow(PlayerStatus.Icon.ID, iconPos, PlayerStatus.Icon.Size, false, (drawList) =>
{
DrawHelper.DrawIcon(iconId.Value, iconPos, PlayerStatus.Icon.Size, false, drawList);
});
}
));
}
}
// ready check status icon
if (Member.ReadyCheckStatus != ReadyCheckStatus.None && ReadyCheckIcon.Enabled && ReadyCheckIcon.Icon.Enabled && _readyCheckTexture != null)
{
Vector2 parentPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, ReadyCheckIcon.Icon.FrameAnchor);
Vector2 iconPos = Utils.GetAnchoredPosition(parentPos + ReadyCheckIcon.Icon.Position, ReadyCheckIcon.Icon.Size, ReadyCheckIcon.Icon.Anchor);
drawActions.Add((ReadyCheckIcon.Icon.StrataLevel, () =>
{
DrawHelper.DrawInWindow(ReadyCheckIcon.Icon.ID, iconPos, ReadyCheckIcon.Icon.Size, false, (drawList) =>
{
Vector2 uv0 = new Vector2(0.5f * (int)Member.ReadyCheckStatus, 0f);
Vector2 uv1 = new Vector2(0.5f + 0.5f * (int)Member.ReadyCheckStatus, 1f);
drawList.AddImage(_readyCheckTexture.Handle, iconPos, iconPos + ReadyCheckIcon.Icon.Size, uv0, uv1);
});
}
));
}
// raise icon
bool showingRaise = ShowingRaise();
if (showingRaise && RaiseTracker.Icon.Enabled)
{
Vector2 parentPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, RaiseTracker.Icon.FrameAnchor);
Vector2 iconPos = Utils.GetAnchoredPosition(parentPos + RaiseTracker.Icon.Position, RaiseTracker.Icon.Size, RaiseTracker.Icon.Anchor);
drawActions.Add((RaiseTracker.Icon.StrataLevel, () =>
{
DrawHelper.DrawInWindow(RaiseTracker.Icon.ID, iconPos, RaiseTracker.Icon.Size, false, (drawList) =>
{
DrawHelper.DrawIcon(411, iconPos, RaiseTracker.Icon.Size, true, drawList);
});
}
));
}
// invuln icon
bool showingInvuln = ShowingInvuln();
if (showingInvuln && InvulnTracker.Icon.Enabled)
{
Vector2 parentPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, InvulnTracker.Icon.FrameAnchor);
Vector2 iconPos = Utils.GetAnchoredPosition(parentPos + InvulnTracker.Icon.Position, InvulnTracker.Icon.Size, InvulnTracker.Icon.Anchor);
drawActions.Add((InvulnTracker.Icon.StrataLevel, () =>
{
DrawHelper.DrawInWindow(InvulnTracker.Icon.ID, iconPos, InvulnTracker.Icon.Size, false, (drawList) =>
{
DrawHelper.DrawIcon(Member.InvulnStatus!.InvulnIcon, iconPos, InvulnTracker.Icon.Size, true, drawList);
});
}
));
}
// mana
if (ShowMana())
{
Vector2 parentPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, _configs.ManaBar.HealthBarAnchor);
drawActions.Add((_configs.ManaBar.StrataLevel, () =>
{
_manaBarHud.Actor = character;
_manaBarHud.PartyMember = Member;
_manaBarHud.PrepareForDraw(parentPos);
_manaBarHud.Draw(parentPos);
}
));
}
// buffs / debuffs
Vector2 buffsPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, _configs.Buffs.HealthBarAnchor);
drawActions.Add((_configs.Buffs.StrataLevel, () =>
{
_buffsListHud.Actor = character;
_buffsListHud.PrepareForDraw(buffsPos);
_buffsListHud.Draw(buffsPos);
}
));
Vector2 debuffsPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, _configs.Debuffs.HealthBarAnchor);
drawActions.Add((_configs.Debuffs.StrataLevel, () =>
{
_debuffsListHud.Actor = character;
_debuffsListHud.PrepareForDraw(debuffsPos);
_debuffsListHud.Draw(debuffsPos);
}
));
// cooldown list
Vector2 cooldownListPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, _configs.CooldownList.HealthBarAnchor);
drawActions.Add((_configs.CooldownList.StrataLevel, () =>
{
_cooldownListHud.Actor = character;
_cooldownListHud.PrepareForDraw(cooldownListPos);
_cooldownListHud.Draw(cooldownListPos);
}
));
// castbar
Vector2 castbarPos = Utils.GetAnchoredPosition(Position, -_configs.HealthBar.Size, _configs.CastBar.HealthBarAnchor);
drawActions.Add((_configs.CastBar.StrataLevel, () =>
{
_castbarHud.Actor = character;
_castbarHud.PrepareForDraw(castbarPos);
_castbarHud.Draw(castbarPos);
}
));
// name
bool drawName = ShouldDrawName(character, showingRaise, showingInvuln);
if (drawName)
{
drawActions.Add((_configs.HealthBar.NameLabelConfig.StrataLevel, () =>
{
bool? playerName = null;
if (character == null || character.ObjectKind == ObjectKind.Player)
{
playerName = true;
}
_nameLabelHud.Draw(Position, _configs.HealthBar.Size, character, Member.Name, isPlayerName: playerName);
}
));
}
// health label
if (Member.MaxHP > 0)
{
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, () =>
{
_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)
{
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, () =>
{
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, () =>
{
InvulnTracker.Icon.NumericLabel.SetValue(duration);
_invulnLabelHud.Draw(Position, _configs.HealthBar.Size);
}
));
}
return drawActions;
}
private bool ShouldDrawName(ICharacter? character, bool showingRaise, bool showingInvuln)
{
if (showingRaise && RaiseTracker.HideNameWhenRaised)
{
return false;
}
if (showingInvuln && InvulnTracker.HideNameWhenInvuln)
{
return false;
}
if (Member != null && PlayerStatus.Enabled && PlayerStatus.HideName && Member.Status != PartyMemberStatus.None)
{
return false;
}
if (Member != null && ReadyCheckIcon.Enabled && ReadyCheckIcon.HideName && Member.ReadyCheckStatus != ReadyCheckStatus.None)
{
return false;
}
if (Utils.IsActorCasting(character) && _configs.CastBar.Enabled && _configs.CastBar.HideNameWhenCasting)
{
return false;
}
return true;
}
private bool ShowingRaise() =>
Member != null && Member.RaiseTime.HasValue && RaiseTracker.Enabled &&
(Member.RaiseTime.Value > 0 || RaiseTracker.KeepIconAfterCastFinishes);
private bool ShowingInvuln() => Member != null && Member.InvulnStatus != null && InvulnTracker.Enabled && Member.InvulnStatus.InvulnTime > 0;
private bool ShowMana()
{
if (Member == null)
{
return false;
}
var isHealer = JobsHelper.IsJobHealer(Member.JobId);
return _configs.ManaBar.Enabled && Member.MaxHP > 0 && _configs.ManaBar.ManaBarDisplayMode switch
{
PartyFramesManaBarDisplayMode.Always => true,
PartyFramesManaBarDisplayMode.HealersOnly => isHealer,
PartyFramesManaBarDisplayMode.HealersAndRaiseJobs => isHealer || JobsHelper.IsJobWithRaise(Member.JobId, Member.Level),
_ => true
};
}
private static uint? IconIdForStatus(PartyMemberStatus status)
{
return status switch
{
PartyMemberStatus.ViewingCutscene => 61508,
PartyMemberStatus.Offline => 61504,
PartyMemberStatus.Dead => 61502,
_ => null
};
}
private static string? StringForStatus(PartyMemberStatus status)
{
return status switch
{
PartyMemberStatus.ViewingCutscene => "[Viewing Cutscene]",
PartyMemberStatus.Offline => "[Offline]",
PartyMemberStatus.Dead => "[Dead]",
_ => null
};
}
#region convenience
private PartyFramesRoleIconConfig RoleIcon => _configs.Icons.Role;
private SignIconConfig SignIcon => _configs.Icons.Sign;
private PartyFramesLeaderIconConfig LeaderIcon => _configs.Icons.Leader;
private PartyFramesPlayerStatusConfig PlayerStatus => _configs.Icons.PlayerStatus;
private PartyFramesReadyCheckStatusConfig ReadyCheckIcon => _configs.Icons.ReadyCheckStatus;
private PartyFramesWhosTalkingConfig WhosTalkingIcon => _configs.Icons.WhosTalking;
private PartyFramesRaiseTrackerConfig RaiseTracker => _configs.Trackers.Raise;
private PartyFramesInvulnTrackerConfig InvulnTracker => _configs.Trackers.Invuln;
private PartyFramesCleanseTrackerConfig CleanseTracker => _configs.Trackers.Cleanse;
#endregion
}
}
@@ -0,0 +1,80 @@
using Dalamud.Game.ClientState.Objects.Types;
using HSUI.Config;
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.ClientState.Objects.SubKinds;
using HSUI.Helpers;
using Dalamud.Game.ClientState.Statuses;
namespace HSUI.Interface.Party
{
public class PartyFramesCleanseTracker : IDisposable
{
private PartyFramesCleanseTrackerConfig _config = null!;
public PartyFramesCleanseTracker()
{
ConfigurationManager.Instance.ResetEvent += OnConfigReset;
OnConfigReset(ConfigurationManager.Instance);
}
~PartyFramesCleanseTracker()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected void Dispose(bool disposing)
{
if (!disposing)
{
return;
}
ConfigurationManager.Instance.ResetEvent -= OnConfigReset;
}
public void OnConfigReset(ConfigurationManager sender)
{
_config = ConfigurationManager.Instance.GetConfigObject<PartyFramesTrackersConfig>().Cleanse;
}
public void Update(List<IPartyFramesMember> partyMembers)
{
if (!_config.Enabled)
{
return;
}
foreach (var member in partyMembers)
{
member.HasDispellableDebuff = false;
if (member.Character is not IBattleChara battleChara)
{
continue;
}
// check for disspellable debuff
IEnumerable<IStatus> statusList = Utils.StatusListForBattleChara(battleChara);
foreach (IStatus status in statusList)
{
if (!status.GameData.Value.CanDispel)
{
continue;
}
// apply raise data based on buff
member.HasDispellableDebuff = true;
break;
}
}
}
}
}
+852
View File
@@ -0,0 +1,852 @@
using HSUI.Config;
using HSUI.Config.Attributes;
using HSUI.Enums;
using HSUI.Helpers;
using HSUI.Interface.Bars;
using HSUI.Interface.GeneralElements;
using HSUI.Interface.PartyCooldowns;
using HSUI.Interface.StatusEffects;
using Dalamud.Bindings.ImGui;
using System;
using System.Numerics;
namespace HSUI.Interface.Party
{
[Exportable(false)]
[Section("Party Frames", true)]
[SubSection("General", 0)]
public class PartyFramesConfig : MovablePluginConfigObject
{
public new static PartyFramesConfig DefaultConfig()
{
var config = new PartyFramesConfig();
config.Position = new Vector2(-ImGui.GetMainViewport().Size.X / 3 - 180, -120);
return config;
}
[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 Solo", spacing = true)]
[Order(50)]
public bool ShowWhenSolo = false;
[Checkbox("Show Chocobo", isMonitored = true)]
[Order(55)]
public bool ShowChocobo = true;
[NestedConfig("Party Title Label", 60)]
public PartyFramesTitleLabel ShowPartyTitleConfig = new PartyFramesTitleLabel(Vector2.Zero, "", DrawAnchor.Left, DrawAnchor.Left);
[NestedConfig("Visibility", 200)]
public VisibilityConfig VisibilityConfig = new VisibilityConfig();
}
[Exportable(false)]
[DisableParentSettings("FrameAnchor", "UseJobColor", "UseRoleColor")]
public class PartyFramesTitleLabel : LabelConfig
{
public PartyFramesTitleLabel(Vector2 position, string text, DrawAnchor frameAnchor, DrawAnchor textAnchor) : base(position, text, frameAnchor, textAnchor)
{
}
}
[Exportable(false)]
[Disableable(false)]
[DisableParentSettings("Position", "Anchor", "BackgroundColor", "FillColor", "HideWhenInactive", "DrawBorder", "BorderColor", "BorderThickness")]
[Section("Party Frames", true)]
[SubSection("Health Bar", 0)]
public class PartyFramesHealthBarsConfig : BarConfig
{
public new static PartyFramesHealthBarsConfig DefaultConfig()
{
var config = new PartyFramesHealthBarsConfig(Vector2.Zero, new(180, 80), PluginConfigColor.Empty);
config.MouseoverAreaConfig.Enabled = false;
return config;
}
[DragInt2("Padding", isMonitored = true, min = 0)]
[Order(31)]
public Vector2 Padding = new Vector2(0, 0);
[NestedConfig("Name Label", 44)]
public EditableLabelConfig NameLabelConfig = new EditableLabelConfig(Vector2.Zero, "[name:initials].", DrawAnchor.Center, DrawAnchor.Center);
[NestedConfig("Health Label", 45)]
public EditableLabelConfig HealthLabelConfig = new EditableLabelConfig(Vector2.Zero, "[health:current-short]", DrawAnchor.Right, DrawAnchor.Right);
[NestedConfig("Order Label", 50)]
public DefaultFontLabelConfig OrderNumberConfig = new DefaultFontLabelConfig(new Vector2(2, 4), "", DrawAnchor.TopLeft, DrawAnchor.TopLeft);
[NestedConfig("Colors", 55)]
public PartyFramesColorsConfig ColorsConfig = new PartyFramesColorsConfig();
[NestedConfig("Shield", 60)]
public ShieldConfig ShieldConfig = new ShieldConfig();
[NestedConfig("Change Alpha Based on Range", 65)]
public PartyFramesRangeConfig RangeConfig = new PartyFramesRangeConfig();
[NestedConfig("Use Smooth Transitions", 70)]
public SmoothHealthConfig SmoothHealthConfig = new SmoothHealthConfig();
[NestedConfig("Custom Mouseover Area", 75)]
public MouseoverAreaConfig MouseoverAreaConfig = new MouseoverAreaConfig();
public PartyFramesHealthBarsConfig(Vector2 position, Vector2 size, PluginConfigColor fillColor, BarDirection fillDirection = BarDirection.Right)
: base(position, size, fillColor, fillDirection)
{
}
}
[Disableable(false)]
[Exportable(false)]
public class PartyFramesColorsConfig : PluginConfigObject
{
[Checkbox("Show Border")]
[Order(4)]
public bool ShowBorder = true;
[ColorEdit4("Border Color")]
[Order(5, collapseWith = nameof(ShowBorder))]
public PluginConfigColor BorderColor = new PluginConfigColor(new Vector4(0f / 255f, 0f / 255f, 0f / 255f, 100f / 100f));
[ColorEdit4("Target Border Color")]
[Order(6, collapseWith = nameof(ShowBorder))]
public PluginConfigColor TargetBordercolor = new PluginConfigColor(new Vector4(255f / 255f, 255f / 255f, 255f / 255f, 100f / 100f));
[DragInt("Inactive Border Thickness", min = 1, max = 10, help = "This is the border thickness that will be used when the border is in the default state (aka not targetted, not showing enmity, etc).")]
[Order(6, collapseWith = nameof(ShowBorder))]
public int InactiveBorderThickness = 1;
[DragInt("Active Border Thickness", min = 1, max = 10, help = "This is the border thickness that will be used when the border active (aka targetted, showing enmity, etc).")]
[Order(7, collapseWith = nameof(ShowBorder))]
public int ActiveBorderThickness = 1;
[ColorEdit4("Background Color", spacing = true)]
[Order(15)]
public PluginConfigColor BackgroundColor = new PluginConfigColor(new Vector4(0f / 255f, 0f / 255f, 0f / 255f, 70f / 100f));
[ColorEdit4("Out of Reach Background Color", help = "This background color will be used when the player's data couldn't be retreived (i.e. player is disconnected)")]
[Order(15)]
public PluginConfigColor OutOfReachBackgroundColor = new PluginConfigColor(new Vector4(50f / 255f, 50f / 255f, 50f / 255f, 70f / 100f));
[Checkbox("Use Death Indicator Background Color", isMonitored = true, spacing = true)]
[Order(18)]
public bool UseDeathIndicatorBackgroundColor = false;
[ColorEdit4("Death Indicator Background Color")]
[Order(19, collapseWith = nameof(UseDeathIndicatorBackgroundColor))]
public PluginConfigColor DeathIndicatorBackgroundColor = new PluginConfigColor(new Vector4(204f / 255f, 3f / 255f, 3f / 255f, 80f / 100f));
[Checkbox("Use Role Colors", isMonitored = true, spacing = true)]
[Order(20)]
public bool UseRoleColors = false;
[NestedConfig("Color Based On Health Value", 30, collapsingHeader = false)]
public ColorByHealthValueConfig ColorByHealth = new ColorByHealthValueConfig();
[Checkbox("Highlight When Hovering With Cursor Or Soft Targeting", spacing = true)]
[Order(40)]
public bool ShowHighlight = true;
[ColorEdit4("Highlight Color")]
[Order(45, collapseWith = nameof(ShowHighlight))]
public PluginConfigColor HighlightColor = new PluginConfigColor(new Vector4(255f / 255f, 255f / 255f, 255f / 255f, 5f / 100f));
[Checkbox("Missing Health Color", spacing = true)]
[Order(46)]
public bool UseMissingHealthBar = false;
[Checkbox("Job Color As Missing Health Color")]
[Order(47, collapseWith = nameof(UseMissingHealthBar))]
public bool UseJobColorAsMissingHealthColor = false;
[Checkbox("Role Color As Missing Health Color")]
[Order(48, collapseWith = nameof(UseMissingHealthBar))]
public bool UseRoleColorAsMissingHealthColor = false;
[ColorEdit4("Color" + "##MissingHealth")]
[Order(49, collapseWith = nameof(UseMissingHealthBar))]
public PluginConfigColor HealthMissingColor = new(new Vector4(255f / 255f, 0f / 255f, 0f / 255f, 100f / 100f));
[Checkbox("Job Color As Background Color")]
[Order(50)]
public bool UseJobColorAsBackgroundColor = false;
[Checkbox("Role Color As Background Color")]
[Order(51)]
public bool UseRoleColorAsBackgroundColor = false;
[Checkbox("Show Enmity Border Colors", spacing = true)]
[Order(54)]
public bool ShowEnmityBorderColors = true;
[ColorEdit4("Enmity Leader Color")]
[Order(55, collapseWith = nameof(ShowEnmityBorderColors))]
public PluginConfigColor EnmityLeaderBordercolor = new PluginConfigColor(new Vector4(255f / 255f, 40f / 255f, 40f / 255f, 100f / 100f));
[Checkbox("Show Second Enmity")]
[Order(60, collapseWith = nameof(ShowEnmityBorderColors))]
public bool ShowSecondEnmity = true;
[Checkbox("Hide Second Enmity in Light Parties")]
[Order(65, collapseWith = nameof(ShowSecondEnmity))]
public bool HideSecondEnmityInLightParties = true;
[ColorEdit4("Enmity Second Color")]
[Order(70, collapseWith = nameof(ShowSecondEnmity))]
public PluginConfigColor EnmitySecondBordercolor = new PluginConfigColor(new Vector4(255f / 255f, 175f / 255f, 40f / 255f, 100f / 100f));
}
[Exportable(false)]
public class PartyFramesRangeConfig : PluginConfigObject
{
[DragInt("Range (yalms)", min = 1, max = 500)]
[Order(5)]
public int Range = 30;
[DragFloat("Alpha", min = 1, max = 100)]
[Order(10)]
public float Alpha = 25;
[Checkbox("Use Additional Range Check")]
[Order(15)]
public bool UseAdditionalRangeCheck = false;
[DragInt("Additional Range (yalms)", min = 1, max = 500)]
[Order(20, collapseWith = nameof(UseAdditionalRangeCheck))]
public int AdditionalRange = 15;
[DragFloat("Additional Alpha", min = 1, max = 100)]
[Order(25, collapseWith = nameof(UseAdditionalRangeCheck))]
public float AdditionalAlpha = 60;
public float AlphaForDistance(int distance, float alpha = 100f)
{
if (!Enabled)
{
return 100f;
}
if (!UseAdditionalRangeCheck)
{
return distance > Range ? Alpha : alpha;
}
if (Range > AdditionalRange)
{
return distance > Range ? Alpha : (distance > AdditionalRange ? AdditionalAlpha : alpha);
}
return distance > AdditionalRange ? AdditionalAlpha : (distance > Range ? Alpha : alpha);
}
}
public class PartyFramesManaBarConfigConverter : PluginConfigObjectConverter
{
public PartyFramesManaBarConfigConverter()
{
NewTypeFieldConverter<bool, PartyFramesManaBarDisplayMode> converter;
converter = new NewTypeFieldConverter<bool, PartyFramesManaBarDisplayMode>(
"PartyFramesManaBarDisplayMode",
PartyFramesManaBarDisplayMode.HealersOnly,
(oldValue) =>
{
return oldValue ? PartyFramesManaBarDisplayMode.HealersOnly : PartyFramesManaBarDisplayMode.Always;
});
FieldConvertersMap.Add("ShowOnlyForHealers", converter);
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(PartyFramesManaBarConfig);
}
}
public enum PartyFramesManaBarDisplayMode
{
HealersAndRaiseJobs,
HealersOnly,
Always,
}
[DisableParentSettings("HideWhenInactive", "Label")]
[Exportable(false)]
[Section("Party Frames", true)]
[SubSection("Mana Bar", 0)]
public class PartyFramesManaBarConfig : PrimaryResourceConfig
{
public new static PartyFramesManaBarConfig DefaultConfig()
{
var config = new PartyFramesManaBarConfig(Vector2.Zero, new(180, 6));
config.HealthBarAnchor = DrawAnchor.Bottom;
config.Anchor = DrawAnchor.Bottom;
config.ValueLabel.Enabled = false;
return config;
}
[Anchor("Health Bar Anchor")]
[Order(14)]
public DrawAnchor HealthBarAnchor = DrawAnchor.BottomLeft;
[RadioSelector("Show For All Jobs With Raise", "Show Only For Healers", "Show For All Jobs")]
[Order(42)]
public PartyFramesManaBarDisplayMode ManaBarDisplayMode = PartyFramesManaBarDisplayMode.HealersOnly;
public PartyFramesManaBarConfig(Vector2 position, Vector2 size)
: base(position, size)
{
}
}
[Exportable(false)]
[Section("Party Frames", true)]
[SubSection("Castbar", 0)]
public class PartyFramesCastbarConfig : CastbarConfig
{
public new static PartyFramesCastbarConfig DefaultConfig()
{
var size = new Vector2(182, 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 PartyFramesCastbarConfig(pos, size, castNameConfig, castTimeConfig);
config.HealthBarAnchor = DrawAnchor.BottomLeft;
config.Anchor = DrawAnchor.TopLeft;
config.ShowIcon = false;
config.Enabled = false;
return config;
}
[Checkbox("Hide Name When Casting")]
[Order(6)]
public bool HideNameWhenCasting = false;
[Anchor("Health Bar Anchor")]
[Order(16)]
public DrawAnchor HealthBarAnchor = DrawAnchor.BottomLeft;
public PartyFramesCastbarConfig(Vector2 position, Vector2 size, LabelConfig castNameConfig, NumericLabelConfig castTimeConfig)
: base(position, size, castNameConfig, castTimeConfig)
{
}
}
[Disableable(false)]
[Exportable(false)]
[Section("Party Frames", true)]
[SubSection("Icons", 0)]
public class PartyFramesIconsConfig : PluginConfigObject
{
public new static PartyFramesIconsConfig DefaultConfig() { return new PartyFramesIconsConfig(); }
[NestedConfig("Role / Job", 10, separator = false)]
public PartyFramesRoleIconConfig Role = new PartyFramesRoleIconConfig(
new Vector2(20, 0),
new Vector2(20, 20),
DrawAnchor.TopLeft,
DrawAnchor.TopLeft
);
[NestedConfig("Sign", 11)]
public SignIconConfig Sign = new SignIconConfig(
new Vector2(0, -10),
new Vector2(30, 30),
DrawAnchor.Top,
DrawAnchor.Top
);
[NestedConfig("Leader", 12)]
public PartyFramesLeaderIconConfig Leader = new PartyFramesLeaderIconConfig(
new Vector2(-12, -12),
new Vector2(24, 24),
DrawAnchor.TopLeft,
DrawAnchor.TopLeft
);
[NestedConfig("Player Status", 13)]
public PartyFramesPlayerStatusConfig PlayerStatus = new PartyFramesPlayerStatusConfig();
[NestedConfig("Ready Check Status", 14)]
public PartyFramesReadyCheckStatusConfig ReadyCheckStatus = new PartyFramesReadyCheckStatusConfig();
[NestedConfig("Who's Talking", 15)]
public PartyFramesWhosTalkingConfig WhosTalking = new PartyFramesWhosTalkingConfig();
}
[Exportable(false)]
public class PartyFramesRoleIconConfig : RoleJobIconConfig
{
public PartyFramesRoleIconConfig() : base() { }
public PartyFramesRoleIconConfig(Vector2 position, Vector2 size, DrawAnchor anchor, DrawAnchor frameAnchor)
: base(position, size, anchor, frameAnchor)
{
}
}
[Exportable(false)]
public class PartyFramesLeaderIconConfig : IconConfig
{
public PartyFramesLeaderIconConfig() : base() { }
public PartyFramesLeaderIconConfig(Vector2 position, Vector2 size, DrawAnchor anchor, DrawAnchor frameAnchor)
: base(position, size, anchor, frameAnchor)
{
}
}
[Exportable(false)]
public class PartyFramesPlayerStatusConfig : PluginConfigObject
{
public new static PartyFramesPlayerStatusConfig DefaultConfig()
{
var config = new PartyFramesPlayerStatusConfig();
config.Label.Enabled = false;
return config;
}
[Checkbox("Hide Name When Showing Status")]
[Order(5)]
public bool HideName = false;
[NestedConfig("Icon", 10)]
public IconConfig Icon = new IconConfig(
new Vector2(0, 5),
new Vector2(16, 16),
DrawAnchor.Top,
DrawAnchor.Top
);
[NestedConfig("Label", 15)]
public LabelConfig Label = new LabelConfig(Vector2.Zero, "", DrawAnchor.Center, DrawAnchor.Center);
}
[Exportable(false)]
public class PartyFramesReadyCheckStatusConfig : PluginConfigObject
{
public new static PartyFramesReadyCheckStatusConfig DefaultConfig() => new PartyFramesReadyCheckStatusConfig();
[Checkbox("Hide Name When Showing Status")]
[Order(5)]
public bool HideName = false;
[DragInt("Duration (seconds)", min = 1, max = 60, help = "Determines for how long the icons will show after a ready check is finished.")]
[Order(6)]
public int Duration = 10;
[NestedConfig("Icon", 10)]
public IconConfig Icon = new IconConfig(
new Vector2(0, 0),
new Vector2(24, 24),
DrawAnchor.TopRight,
DrawAnchor.TopRight
);
}
[Exportable(false)]
public class PartyFramesWhosTalkingConfig : PluginConfigObject
{
public new static PartyFramesWhosTalkingConfig DefaultConfig() => new PartyFramesWhosTalkingConfig();
[Checkbox("Replace Role/Job Icon when active")]
[Order(5)]
public bool ReplaceRoleJobIcon = false;
[Checkbox("Show Speaking State", spacing = true)]
[Order(10)]
public bool ShowSpeaking = true;
[Checkbox("Show Muted State")]
[Order(10)]
public bool ShowMuted = true;
[Checkbox("Show Deafened State")]
[Order(10)]
public bool ShowDeafened = true;
[NestedConfig("Icon", 20)]
public IconConfig Icon = new IconConfig(
new Vector2(0, 0),
new Vector2(24, 24),
DrawAnchor.TopRight,
DrawAnchor.TopRight
);
[Checkbox("Change Health Bar Border when active", spacing = true, help = "Enabling this will override other border settings!")]
[Order(30)]
public bool ChangeBorders = false;
[DragInt("Border Thickness", min = 1, max = 10)]
[Order(31, collapseWith = nameof(ChangeBorders))]
public int BorderThickness = 1;
[ColorEdit4("Speaking Border Color")]
[Order(32, collapseWith = nameof(ChangeBorders))]
public PluginConfigColor SpeakingBorderColor = PluginConfigColor.FromHex(0xFF40BB40);
[ColorEdit4("Muted Border Color")]
[Order(33, collapseWith = nameof(ChangeBorders))]
public PluginConfigColor MutedBorderColor = PluginConfigColor.FromHex(0xFF008080);
[ColorEdit4("Deafened Border Color")]
[Order(34, collapseWith = nameof(ChangeBorders))]
public PluginConfigColor DeafenedBorderColor = PluginConfigColor.FromHex(0xFFFF4444);
public bool EnabledForState(WhosTalkingState state)
{
switch (state)
{
case WhosTalkingState.Speaking: return ShowSpeaking;
case WhosTalkingState.Muted: return ShowMuted;
case WhosTalkingState.Deafened: return ShowDeafened;
}
return false;
}
public PluginConfigColor? ColorForState(WhosTalkingState state)
{
if (state == WhosTalkingState.Speaking && ShowSpeaking) { return SpeakingBorderColor; }
if (state == WhosTalkingState.Muted && ShowMuted) { return MutedBorderColor; }
if (ShowDeafened) { return DeafenedBorderColor; }
return null;
}
}
[Exportable(false)]
[Section("Party Frames", true)]
[SubSection("Buffs", 0)]
public class PartyFramesBuffsConfig : PartyFramesStatusEffectsListConfig
{
public new static PartyFramesBuffsConfig 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 PartyFramesBuffsConfig(DrawAnchor.TopRight, pos, size, true, false, false, GrowthDirections.Left | GrowthDirections.Down, iconConfig);
config.Limit = 4;
return config;
}
public PartyFramesBuffsConfig(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("Party Frames", true)]
[SubSection("Debuffs", 0)]
public class PartyFramesDebuffsConfig : PartyFramesStatusEffectsListConfig
{
public new static PartyFramesDebuffsConfig 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(24, 24);
var pos = new Vector2(-2, -2);
var size = new Vector2(iconConfig.Size.X * 4 + 6, iconConfig.Size.Y);
var config = new PartyFramesDebuffsConfig(DrawAnchor.BottomRight, pos, size, false, true, false, GrowthDirections.Left | GrowthDirections.Up, iconConfig);
config.Limit = 4;
return config;
}
public PartyFramesDebuffsConfig(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)
{
}
}
public class PartyFramesStatusEffectsListConfig : StatusEffectsListConfig
{
[Anchor("Health Bar Anchor")]
[Order(4)]
public DrawAnchor HealthBarAnchor = DrawAnchor.BottomLeft;
public PartyFramesStatusEffectsListConfig(DrawAnchor anchor, Vector2 position, Vector2 size, bool showBuffs, bool showDebuffs, bool showPermanentEffects,
GrowthDirections growthDirections, StatusEffectIconConfig iconConfig)
: base(position, size, showBuffs, showDebuffs, showPermanentEffects, growthDirections, iconConfig)
{
HealthBarAnchor = anchor;
}
}
[Disableable(false)]
[Exportable(false)]
[Section("Party Frames", true)]
[SubSection("Trackers", 0)]
public class PartyFramesTrackersConfig : PluginConfigObject
{
public new static PartyFramesTrackersConfig DefaultConfig() { return new PartyFramesTrackersConfig(); }
[NestedConfig("Raise Tracker", 10, separator = false)]
public PartyFramesRaiseTrackerConfig Raise = new PartyFramesRaiseTrackerConfig();
[NestedConfig("Invulnerabilities Tracker", 15)]
public PartyFramesInvulnTrackerConfig Invuln = new PartyFramesInvulnTrackerConfig();
[NestedConfig("Cleanse Tracker", 15)]
public PartyFramesCleanseTrackerConfig Cleanse = new PartyFramesCleanseTrackerConfig();
}
[Exportable(false)]
public class PartyFramesRaiseTrackerConfig : PluginConfigObject
{
public new static PartyFramesRaiseTrackerConfig DefaultConfig() { return new PartyFramesRaiseTrackerConfig(); }
[Checkbox("Hide Name When Raised")]
[Order(10)]
public bool HideNameWhenRaised = true;
[Checkbox("Keep Icon After Cast Finishes")]
[Order(15)]
public bool KeepIconAfterCastFinishes = true;
[Checkbox("Change Background Color When Raised", spacing = true)]
[Order(20)]
public bool ChangeBackgroundColorWhenRaised = true;
[ColorEdit4("Raise Background Color")]
[Order(25, collapseWith = nameof(ChangeBackgroundColorWhenRaised))]
public PluginConfigColor BackgroundColor = new(new Vector4(211f / 255f, 235f / 255f, 215f / 245f, 50f / 100f));
[Checkbox("Change Border Color When Raised", spacing = true)]
[Order(30)]
public bool ChangeBorderColorWhenRaised = true;
[ColorEdit4("Raise Border Color")]
[Order(35, collapseWith = nameof(ChangeBorderColorWhenRaised))]
public PluginConfigColor BorderColor = new(new Vector4(47f / 255f, 169f / 255f, 215f / 255f, 100f / 100f));
[NestedConfig("Icon", 50)]
public IconWithLabelConfig Icon = new IconWithLabelConfig(
new Vector2(0, 0),
new Vector2(50, 50),
DrawAnchor.Center,
DrawAnchor.Center
);
}
[Exportable(false)]
public class PartyFramesInvulnTrackerConfig : PluginConfigObject
{
public new static PartyFramesInvulnTrackerConfig DefaultConfig() { return new PartyFramesInvulnTrackerConfig(); }
[Checkbox("Hide Name When Invuln is Up")]
[Order(10)]
public bool HideNameWhenInvuln = true;
[Checkbox("Change Background Color When Invuln is Up", spacing = true)]
[Order(15)]
public bool ChangeBackgroundColorWhenInvuln = true;
[ColorEdit4("Invuln Background Color")]
[Order(20, collapseWith = nameof(ChangeBackgroundColorWhenInvuln))]
public PluginConfigColor BackgroundColor = new(new Vector4(211f / 255f, 235f / 255f, 215f / 245f, 50f / 100f));
[Checkbox("Walking Dead Custom Color")]
[Order(25, collapseWith = nameof(ChangeBackgroundColorWhenInvuln))]
public bool UseCustomWalkingDeadColor = true;
[ColorEdit4("Walking Dead Background Color")]
[Order(30, collapseWith = nameof(UseCustomWalkingDeadColor))]
public PluginConfigColor WalkingDeadBackgroundColor = new(new Vector4(158f / 255f, 158f / 255f, 158f / 255f, 50f / 100f));
[NestedConfig("Icon", 50)]
public IconWithLabelConfig Icon = new IconWithLabelConfig(
new Vector2(0, 0),
new Vector2(50, 50),
DrawAnchor.Center,
DrawAnchor.Center
);
}
public class PartyFramesTrackerConfigConverter : PluginConfigObjectConverter
{
public PartyFramesTrackerConfigConverter()
{
SameTypeFieldConverter<Vector2> pos = new SameTypeFieldConverter<Vector2>("Icon.Position", Vector2.Zero);
FieldConvertersMap.Add("Position", pos);
SameTypeFieldConverter<Vector2> size = new SameTypeFieldConverter<Vector2>("Icon.Size", new Vector2(50, 50));
FieldConvertersMap.Add("IconSize", size);
SameTypeFieldConverter<DrawAnchor> anchor = new SameTypeFieldConverter<DrawAnchor>("Icon.Anchor", DrawAnchor.Center);
FieldConvertersMap.Add("Anchor", anchor);
SameTypeFieldConverter<DrawAnchor> frameAnchor = new SameTypeFieldConverter<DrawAnchor>("Icon.FrameAnchor", DrawAnchor.Center);
FieldConvertersMap.Add("HealthBarAnchor", frameAnchor);
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(PartyFramesRaiseTrackerConfig) ||
objectType == typeof(PartyFramesInvulnTrackerConfig);
}
}
[DisableParentSettings("Position", "Strata")]
[Exportable(false)]
public class PartyFramesCleanseTrackerConfig : MovablePluginConfigObject
{
public new static PartyFramesCleanseTrackerConfig DefaultConfig() { return new PartyFramesCleanseTrackerConfig(); }
[Checkbox("Show only on jobs with cleanses", spacing = true)]
[Order(10)]
public bool CleanseJobsOnly = true;
[Checkbox("Change Health Bar Color ", spacing = true)]
[Order(15)]
public bool ChangeHealthBarCleanseColor = true;
[ColorEdit4("Health Bar Color")]
[Order(20, collapseWith = nameof(ChangeHealthBarCleanseColor))]
public PluginConfigColor HealthBarColor = new(new Vector4(255f / 255f, 0f / 255f, 104f / 255f, 100f / 100f));
[Checkbox("Change Border Color", spacing = true)]
[Order(25)]
public bool ChangeBorderCleanseColor = true;
[ColorEdit4("Border Color")]
[Order(30, collapseWith = nameof(ChangeBorderCleanseColor))]
public PluginConfigColor BorderColor = new(new Vector4(255f / 255f, 0f / 255f, 104f / 255f, 100f / 100f));
}
[Exportable(false)]
[DisableParentSettings("Anchor")]
[Section("Party Frames", true)]
[SubSection("Cooldowns", 0)]
public class PartyFramesCooldownListConfig : AnchorablePluginConfigObject
{
public new static PartyFramesCooldownListConfig DefaultConfig()
{
PartyFramesCooldownListConfig config = new PartyFramesCooldownListConfig();
config.Position = new Vector2(-2, 0);
config.Size = new Vector2(40 * 8 + 6, 40);
return config;
}
[Anchor("Health Bar Anchor")]
[Order(3)]
public DrawAnchor HealthBarAnchor = DrawAnchor.Left;
[Checkbox("Tooltips", spacing = true)]
[Order(20)]
public bool ShowTooltips = true;
[Checkbox("Preview", isMonitored = true)]
[Order(21)]
public bool Preview;
[DragInt2("Icon Size", min = 1, max = 4000, spacing = true)]
[Order(30)]
public Vector2 IconSize = new Vector2(40, 40);
[DragInt2("Icon Padding", min = 0, max = 500)]
[Order(31)]
public Vector2 IconPadding = new(4, 4);
[Checkbox("Fill Rows First")]
[Order(32)]
public bool FillRowsFirst = true;
[Combo("Icons Growth Direction",
"Right and Down",
"Right and Up",
"Left and Down",
"Left and Up",
"Centered and Up",
"Centered and Down",
"Centered and Left",
"Centered and Right"
)]
[Order(33)]
public int Directions = 3; // left & up
[Checkbox("Show Border", spacing = true)]
[Order(35)]
public bool DrawBorder = true;
[ColorEdit4("Border Color")]
[Order(36, collapseWith = nameof(DrawBorder))]
public PluginConfigColor BorderColor = new PluginConfigColor(new Vector4(0f / 255f, 0f / 255f, 0f / 255f, 100f / 100f));
[DragInt("Border Thickness", min = 1, max = 10)]
[Order(37, collapseWith = nameof(DrawBorder))]
public int BorderThickness = 1;
[Checkbox("Change Icon Border When Active")]
[Order(45, collapseWith = nameof(DrawBorder))]
public bool ChangeIconBorderWhenActive = true;
[ColorEdit4("Icon Active Border Color")]
[Order(46, collapseWith = nameof(ChangeIconBorderWhenActive))]
public PluginConfigColor IconActiveBorderColor = new PluginConfigColor(new Vector4(255f / 255f, 200f / 255f, 35f / 255f, 100f / 100f));
[DragInt("Icon Active Border Thickness", min = 1, max = 10)]
[Order(47, collapseWith = nameof(ChangeIconBorderWhenActive))]
public int IconActiveBorderThickness = 3;
[Checkbox("Change Label Color When Active", spacing = true)]
[Order(50)]
public bool ChangeLabelsColorWhenActive = false;
[ColorEdit4("Label Active Color")]
[Order(51, collapseWith = nameof(ChangeLabelsColorWhenActive))]
public PluginConfigColor LabelsActiveColor = new PluginConfigColor(new Vector4(255f / 255f, 200f / 255f, 35f / 255f, 100f / 100f));
[NestedConfig("Time Label", 80)]
public PartyCooldownTimeLabelConfig TimeLabel = new PartyCooldownTimeLabelConfig(new Vector2(0, 0), "", DrawAnchor.Center, DrawAnchor.Center) { NumberFormat = 1 };
}
}
@@ -0,0 +1,295 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
using HSUI.Config;
using HSUI.Enums;
using HSUI.Helpers;
using HSUI.Interface.GeneralElements;
using HSUI.Interface.PartyCooldowns;
using Dalamud.Bindings.ImGui;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace HSUI.Interface.Party
{
public class PartyFramesCooldownListHud : ParentAnchoredDraggableHudElement, IHudElementWithActor, IHudElementWithAnchorableParent, IHudElementWithPreview
{
private PartyFramesCooldownListConfig Config => (PartyFramesCooldownListConfig)_config;
private PartyCooldownsDataConfig _dataConfig = null!;
private LabelHud _timeLabel;
private bool _needsUpdate = true;
private LayoutInfo _layoutInfo;
private List<PartyCooldown> _cooldowns = new List<PartyCooldown>();
private List<PartyCooldown>? _fakeCooldowns = null;
public IGameObject? Actor { get; set; }
protected override bool AnchorToParent => true;
protected override DrawAnchor ParentAnchor => Config is PartyFramesCooldownListConfig config ? config.HealthBarAnchor : DrawAnchor.Center;
public PartyFramesCooldownListHud(PartyFramesCooldownListConfig config, string? displayName = null) : base(config, displayName)
{
_timeLabel = new LabelHud(config.TimeLabel);
ConfigurationManager.Instance.ResetEvent += OnConfigReset;
PartyCooldownsManager.Instance.CooldownsChangedEvent += OnCooldownsChanged;
_config.ValueChangeEvent += OnConfigPropertyChanged;
OnConfigReset(ConfigurationManager.Instance);
}
private void OnConfigReset(ConfigurationManager sender)
{
if (_dataConfig != null)
_dataConfig.CooldownsDataChangedEvent -= OnCooldownsDataChanged;
_dataConfig = ConfigurationManager.Instance.GetConfigObject<PartyCooldownsDataConfig>();
_dataConfig.CooldownsDataChangedEvent += OnCooldownsDataChanged;
}
protected override void InternalDispose()
{
ConfigurationManager.Instance?.ResetEvent -= OnConfigReset;
_config.ValueChangeEvent -= OnConfigPropertyChanged;
_dataConfig?.CooldownsDataChangedEvent -= OnCooldownsDataChanged;
PartyCooldownsManager.Instance?.CooldownsChangedEvent -= OnCooldownsChanged;
}
private void OnConfigPropertyChanged(object? sender, OnChangeBaseArgs args)
{
if (args.PropertyName == "Preview")
{
UpdatePreview();
}
}
private unsafe void UpdatePreview()
{
if (!Config.Preview)
{
_fakeCooldowns = null;
return;
}
var RNG = new Random((int)ImGui.GetTime());
_fakeCooldowns = new List<PartyCooldown>();
for (int i = 0; i < 10; i++)
{
int index = RNG.Next(0, _dataConfig.Cooldowns.Count);
PartyCooldown cooldown = new PartyCooldown(_dataConfig.Cooldowns[index], 0, 90, null);
int rng = RNG.Next(100);
if (rng > 80)
{
cooldown.LastTimeUsed = ImGui.GetTime() - 30;
}
else if (rng > 50)
{
cooldown.LastTimeUsed = ImGui.GetTime() + 1;
}
_fakeCooldowns.Add(cooldown);
}
}
public void StopPreview()
{
Config.Preview = false;
UpdatePreview();
}
private void OnCooldownsDataChanged(PartyCooldownsDataConfig sender)
{
_needsUpdate = true;
}
private void OnCooldownsChanged(PartyCooldownsManager sender)
{
_needsUpdate = true;
}
private void UpdateCooldowns()
{
_cooldowns.Clear();
if (Actor == null || PartyCooldownsManager.Instance?.CooldownsMap == null) { return; }
if (PartyCooldownsManager.Instance.CooldownsMap.TryGetValue((uint)Actor.GameObjectId, out Dictionary<uint, PartyCooldown>? dict) && dict != null)
{
_cooldowns = dict.Values.Where(o => o.Data.IsEnabledForPartyFrames()).ToList();
}
_cooldowns.Sort((a, b) =>
{
int aOrder = a.Data.Column * 1000 + a.Data.Priority;
int bOrder = b.Data.Column * 1000 + b.Data.Priority;
return aOrder.CompareTo(bOrder);
});
_needsUpdate = false;
}
private void CalculateLayout(uint count)
{
if (count <= 0) { return; }
_layoutInfo = LayoutHelper.CalculateLayout(
Config.Size,
Config.IconSize,
count,
Config.IconPadding,
LayoutHelper.GetFillsRowsFirst(Config.FillRowsFirst, LayoutHelper.GrowthDirectionsFromIndex(Config.Directions))
);
}
public override void DrawChildren(Vector2 origin)
{
if (!Config.Enabled) { return; }
if (_needsUpdate)
{
UpdateCooldowns();
}
List<PartyCooldown> list = _fakeCooldowns != null ? _fakeCooldowns : _cooldowns;
if (list.Count == 0) { return; }
// area
GrowthDirections growthDirections = LayoutHelper.GrowthDirectionsFromIndex(Config.Directions);
Vector2 position = origin + GetAnchoredPosition(Config.Position, Config.Size, DrawAnchor.TopLeft);
Vector2 areaPos = LayoutHelper.CalculateStartPosition(position, Config.Size, growthDirections);
Vector2 margin = new Vector2(14, 10);
ImDrawListPtr drawList = ImGui.GetWindowDrawList();
// calculate icon positions
uint count = (uint)list.Count;
CalculateLayout(count);
var (iconPositions, minPos, maxPos) = LayoutHelper.CalculateIconPositions(
growthDirections,
count,
position,
Config.Size,
Config.IconSize,
Config.IconPadding,
LayoutHelper.GetFillsRowsFirst(Config.FillRowsFirst, growthDirections),
_layoutInfo
);
// window
// imgui clips the left and right borders inside windows for some reason
// we make the window bigger so the actual drawable size is the expected one
Vector2 windowPos = minPos - margin;
Vector2 windowSize = maxPos - minPos;
AddDrawAction(Config.StrataLevel, () =>
{
DrawHelper.DrawInWindow(ID, windowPos, windowSize + margin * 2, false, (drawList) =>
{
// area
if (Config.Preview)
{
drawList.AddRectFilled(areaPos, areaPos + Config.Size, 0x88000000);
}
for (int i = 0; i < count; i++)
{
Vector2 iconPos = iconPositions[i];
PartyCooldown cooldown = list[i];
float cooldownTime = cooldown.CooldownTimeRemaining();
float effectTime = cooldown.EffectTimeRemaining();
// icon
bool recharging = effectTime == 0 && cooldownTime > 0;
uint color = recharging ? 0xAAFFFFFF : 0xFFFFFFFF;
bool shouldDrawCooldown = ClipRectsHelper.Instance.GetClipRectForArea(iconPos, Config.IconSize) == null;
DrawHelper.DrawIcon(cooldown.Data.IconId, iconPos, Config.IconSize, false, color, drawList);
if (shouldDrawCooldown && effectTime == 0 && cooldownTime > 0)
{
DrawHelper.DrawIconCooldown(iconPos, Config.IconSize, cooldownTime, cooldown.Data.CooldownDuration, drawList);
}
// border
if (Config.DrawBorder)
{
bool active = effectTime > 0 && Config.ChangeIconBorderWhenActive;
uint iconBorderColor = active ? Config.IconActiveBorderColor.Base : Config.BorderColor.Base;
int thickness = active ? Config.IconActiveBorderThickness : Config.BorderThickness;
drawList.AddRect(iconPos, iconPos + Config.IconSize, iconBorderColor, 0, ImDrawFlags.None, thickness);
}
}
});
});
PartyCooldown? hoveringCooldown = null;
IGameObject? character = Actor;
// labels need to be drawn separated since they have their own window for clipping
for (var i = 0; i < count; i++)
{
Vector2 iconPos = iconPositions[i];
PartyCooldown cooldown = list[i];
float cooldownTime = cooldown.CooldownTimeRemaining();
float effectTime = cooldown.EffectTimeRemaining();
PluginConfigColor? labelColor = effectTime > 0 && Config.ChangeLabelsColorWhenActive ? Config.LabelsActiveColor : null;
// time
AddDrawAction(Config.TimeLabel.StrataLevel, () =>
{
PluginConfigColor realColor = Config.TimeLabel.Color;
Config.TimeLabel.Color = labelColor ?? realColor;
Config.TimeLabel.SetText("");
if (effectTime > 0)
{
if (Config.TimeLabel.ShowEffectDuration)
{
Config.TimeLabel.SetValue(effectTime);
}
}
else if (cooldownTime > 0)
{
if (Config.TimeLabel.ShowRemainingCooldown)
{
Config.TimeLabel.SetText(Utils.DurationToString(cooldownTime, Config.TimeLabel.NumberFormat));
}
}
_timeLabel.Draw(iconPos, Config.IconSize, character);
Config.TimeLabel.Color = realColor;
});
// tooltips / interaction
if (ImGui.IsMouseHoveringRect(iconPos, iconPos + Config.IconSize))
{
hoveringCooldown = cooldown;
}
}
if (hoveringCooldown != null)
{
// tooltip
if (Config.ShowTooltips)
{
TooltipsHelper.Instance.ShowTooltipOnCursor(
hoveringCooldown.TooltipText(),
hoveringCooldown.Data.Name,
hoveringCooldown.Data.ActionId
);
}
}
}
}
}
+465
View File
@@ -0,0 +1,465 @@
using Dalamud.Game.ClientState.Objects.Types;
using HSUI.Config;
using HSUI.Enums;
using HSUI.Helpers;
using HSUI.Interface.GeneralElements;
using Dalamud.Bindings.ImGui;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
namespace HSUI.Interface.Party
{
public class PartyFramesHud : DraggableHudElement, IHudElementWithMouseOver, IHudElementWithPreview, IHudElementWithVisibilityConfig
{
private PartyFramesConfig Config => (PartyFramesConfig)_config;
public VisibilityConfig VisibilityConfig => Config.VisibilityConfig;
private PartyFramesConfigs Configs;
private Vector2 _contentMargin = new Vector2(2, 2);
private static readonly int MaxMemberCount = 9; // 8 players + chocobo
// layout
private Vector2 _origin;
private LayoutInfo _layoutInfo;
private uint _memberCount = 0;
private bool _layoutDirty = true;
private readonly List<PartyFramesBar> bars;
private LabelHud _titleLabelHud;
private bool Locked => !ConfigurationManager.Instance.IsConfigWindowOpened;
public PartyFramesHud(PartyFramesConfig config, string displayName) : base(config, displayName)
{
Configs = PartyFramesConfigs.GetConfigs();
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++)
{
PartyFramesBar bar = new PartyFramesBar("DelvUI_partyFramesBar" + i, Configs);
bar.OpenContextMenuEvent += OnOpenContextMenu;
bars.Add(bar);
}
_titleLabelHud = new LabelHud(config.ShowPartyTitleConfig);
PartyManager.Instance.MembersChangedEvent += OnMembersChanged;
UpdateBars(Vector2.Zero);
}
protected override void InternalDispose()
{
foreach (var bar in bars)
{
try { bar.Dispose(); }
catch (Exception ex) { Plugin.Logger.Error($"Error disposing PartyFramesBar: {ex.Message}"); }
}
bars.Clear();
_config.ValueChangeEvent -= OnLayoutPropertyChanged;
Configs.HealthBar.ValueChangeEvent -= OnLayoutPropertyChanged;
Configs.HealthBar.ColorsConfig.ValueChangeEvent -= OnLayoutPropertyChanged;
PartyManager.Instance.MembersChangedEvent -= OnMembersChanged;
}
private unsafe void OnOpenContextMenu(PartyFramesBar bar)
{
if (bar.Member == null || Plugin.ObjectTable.LocalPlayer == null)
{
return;
}
if (PartyManager.Instance.PartyListAddon == null || PartyManager.Instance.HudAgent == IntPtr.Zero)
{
return;
}
int addonId = PartyManager.Instance.PartyListAddon->AtkUnitBase.Id;
AgentModule.Instance()->GetAgentHUD()->OpenContextMenuFromPartyAddon(addonId, bar.Member.Index);
}
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(PartyManager sender)
{
UpdateBars(_origin);
}
public void UpdateBars(Vector2 origin)
{
uint memberCount = PartyManager.Instance.MemberCount;
uint row = 0;
uint col = 0;
for (int i = 0; i < bars.Count; i++)
{
PartyFramesBar bar = bars[i];
if (i >= memberCount)
{
bar.Visible = false;
continue;
}
// update bar
IPartyFramesMember member = PartyManager.Instance.SortedGroupMembers.ElementAt(i);
bar.Member = member;
bar.Visible = true;
// anchor and position
CalculateBarPosition(origin, Size, out var x, out var y);
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
);
// layout
if (Config.FillRowsFirst)
{
col = col + 1;
if (col >= _layoutInfo.TotalColCount)
{
col = 0;
row = row + 1;
}
}
else
{
row = row + 1;
if (row >= _layoutInfo.TotalRowCount)
{
row = 0;
col = col + 1;
}
}
}
}
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 void UpdateBarsPosition(Vector2 delta)
{
foreach (PartyFramesBar bar in bars)
{
bar.Position = bar.Position + delta;
}
}
public void StopPreview()
{
Config.Preview = false;
PartyManager.Instance?.UpdatePreview();
foreach (PartyFramesBar 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 (PartyFramesBar bar in bars)
{
bar.StopMouseover();
}
}
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)
{
Vector2 contentStartPos = origin + Config.Position;
uint count = PartyManager.Instance.MemberCount;
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 override void DrawChildren(Vector2 origin)
{
if (!_config.Enabled)
{
return;
}
// area bg
if (Config.Preview)
{
AddDrawAction(StrataLevel.LOWEST, () =>
{
ImDrawListPtr 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);
});
}
uint count = PartyManager.Instance.MemberCount;
if (count < 1)
{
return;
}
UpdateLayout(origin);
// draw bars
// check borders to determine the order in which the bars are drawn
// which is necessary for grid-like party frames
IGameObject? target = Plugin.TargetManager.SoftTarget ?? Plugin.TargetManager.Target;
int targetIndex = -1;
int enmityLeaderIndex = -1;
int enmitySecondIndex = -1;
List<int> raisedIndexes = new List<int>();
List<int> cleanseIndexes = new List<int>();
List<int> whosTalkingIndexes = new List<int>();
for (int i = 0; i < count; i++)
{
IPartyFramesMember? member = bars[i].Member;
if (member != null)
{
// whos talking
if (Configs.Icons.WhosTalking.ChangeBorders && member.WhosTalkingState != WhosTalkingState.None)
{
whosTalkingIndexes.Add(i);
continue;
}
// target
if (target != null && member.ObjectId == target.GameObjectId)
{
targetIndex = i;
continue;
}
// cleanse
bool cleanseCheck = true;
if (Configs.Trackers.Cleanse.CleanseJobsOnly)
{
cleanseCheck = Utils.IsOnCleanseJob();
}
if (Configs.Trackers.Cleanse.Enabled && Configs.Trackers.Cleanse.ChangeBorderCleanseColor && member.HasDispellableDebuff && cleanseCheck)
{
cleanseIndexes.Add(i);
continue;
}
// raise
if (Configs.Trackers.Raise.Enabled && Configs.Trackers.Raise.ChangeBorderColorWhenRaised && member.RaiseTime.HasValue)
{
raisedIndexes.Add(i);
continue;
}
// enmity
if (Configs.HealthBar.ColorsConfig.ShowEnmityBorderColors)
{
if (member.EnmityLevel == EnmityLevel.Leader)
{
enmityLeaderIndex = i;
continue;
}
else if (Configs.HealthBar.ColorsConfig.ShowSecondEnmity && member.EnmityLevel == EnmityLevel.Second &&
(count > 4 || !Configs.HealthBar.ColorsConfig.HideSecondEnmityInLightParties))
{
enmitySecondIndex = i;
continue;
}
}
}
// no special border
AddDrawActions(bars[i].GetBarDrawActions(origin));
}
// special colors for borders
// 2nd enmity
if (enmitySecondIndex >= 0)
{
AddDrawActions(bars[enmitySecondIndex].GetBarDrawActions(origin, Configs.HealthBar.ColorsConfig.EnmitySecondBordercolor));
}
// 1st enmity
if (enmityLeaderIndex >= 0)
{
AddDrawActions(bars[enmityLeaderIndex].GetBarDrawActions(origin, Configs.HealthBar.ColorsConfig.EnmityLeaderBordercolor));
}
// raise
foreach (int index in raisedIndexes)
{
AddDrawActions(bars[index].GetBarDrawActions(origin, Configs.Trackers.Raise.BorderColor));
}
// target
if (targetIndex >= 0)
{
AddDrawActions(bars[targetIndex].GetBarDrawActions(origin, Configs.HealthBar.ColorsConfig.TargetBordercolor));
}
// cleanseable debuff
foreach (int index in cleanseIndexes)
{
AddDrawActions(bars[index].GetBarDrawActions(origin, Configs.Trackers.Cleanse.BorderColor));
}
// whos talking
foreach (int index in whosTalkingIndexes)
{
IPartyFramesMember? member = bars[index].Member;
if (member != null)
{
AddDrawActions(bars[index].GetBarDrawActions(origin, Configs.Icons.WhosTalking.ColorForState(member.WhosTalkingState)));
}
else
{
AddDrawActions(bars[index].GetBarDrawActions(origin));
}
}
// extra elements
foreach (PartyFramesBar bar in bars)
{
AddDrawActions(bar.GetElementsDrawActions(origin));
}
AddDrawAction(Config.ShowPartyTitleConfig.StrataLevel, () =>
{
Config.ShowPartyTitleConfig.SetText(PartyManager.Instance.PartyTitle);
_titleLabelHud.Draw(origin + Config.Position);
});
}
}
#region utils
public struct PartyFramesConfigs
{
public PartyFramesHealthBarsConfig HealthBar;
public PartyFramesManaBarConfig ManaBar;
public PartyFramesCastbarConfig CastBar;
public PartyFramesIconsConfig Icons;
public PartyFramesBuffsConfig Buffs;
public PartyFramesDebuffsConfig Debuffs;
public PartyFramesTrackersConfig Trackers;
public PartyFramesCooldownListConfig CooldownList;
public PartyFramesConfigs(
PartyFramesHealthBarsConfig healthBar,
PartyFramesManaBarConfig manaBar,
PartyFramesCastbarConfig castBar,
PartyFramesIconsConfig icons,
PartyFramesBuffsConfig buffs,
PartyFramesDebuffsConfig debuffs,
PartyFramesTrackersConfig trackers,
PartyFramesCooldownListConfig cooldownList)
{
HealthBar = healthBar;
ManaBar = manaBar;
CastBar = castBar;
Icons = icons;
Buffs = buffs;
Debuffs = debuffs;
Trackers = trackers;
CooldownList = cooldownList;
}
public static PartyFramesConfigs GetConfigs()
{
return new PartyFramesConfigs(
ConfigurationManager.Instance.GetConfigObject<PartyFramesHealthBarsConfig>(),
ConfigurationManager.Instance.GetConfigObject<PartyFramesManaBarConfig>(),
ConfigurationManager.Instance.GetConfigObject<PartyFramesCastbarConfig>(),
ConfigurationManager.Instance.GetConfigObject<PartyFramesIconsConfig>(),
ConfigurationManager.Instance.GetConfigObject<PartyFramesBuffsConfig>(),
ConfigurationManager.Instance.GetConfigObject<PartyFramesDebuffsConfig>(),
ConfigurationManager.Instance.GetConfigObject<PartyFramesTrackersConfig>(),
ConfigurationManager.Instance.GetConfigObject<PartyFramesCooldownListConfig>()
);
}
}
#endregion
}
+111
View File
@@ -0,0 +1,111 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Statuses;
using HSUI.Config;
using HSUI.Helpers;
using System;
using System.Collections.Generic;
namespace HSUI.Interface.Party
{
public class InvulnStatus
{
public readonly uint InvulnIcon;
public readonly float InvulnTime;
public readonly uint InvulnId;
public InvulnStatus(uint invulnIcon, float invulnTime, uint invulnId)
{
InvulnIcon = invulnIcon;
InvulnTime = invulnTime;
InvulnId = invulnId;
}
}
public class PartyFramesInvulnTracker : IDisposable
{
private PartyFramesInvulnTrackerConfig _config = null!;
public PartyFramesInvulnTracker()
{
ConfigurationManager.Instance.ResetEvent += OnConfigReset;
OnConfigReset(ConfigurationManager.Instance);
}
~PartyFramesInvulnTracker()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected void Dispose(bool disposing)
{
if (!disposing)
{
return;
}
ConfigurationManager.Instance.ResetEvent -= OnConfigReset;
}
public void OnConfigReset(ConfigurationManager sender)
{
_config = ConfigurationManager.Instance.GetConfigObject<PartyFramesTrackersConfig>().Invuln;
}
public void Update(List<IPartyFramesMember> partyMembers)
{
if (!_config.Enabled)
{
return;
}
foreach (var member in partyMembers)
{
if (member.Character == null || member.ObjectId == 0)
{
member.InvulnStatus = null;
continue;
}
if (member.Character is not IBattleChara battleChara || member.HP <= 0)
{
member.InvulnStatus = null;
continue;
}
// check invuln buff
IStatus? tankInvuln = Utils.GetTankInvulnerabilityID(battleChara);
if (tankInvuln == null)
{
member.InvulnStatus = null;
continue;
}
// apply invuln data based on buff
member.InvulnStatus = new InvulnStatus(InvulnMap[tankInvuln.StatusId], tankInvuln.RemainingTime, tankInvuln.StatusId);
}
}
#region invuln ids
//these need to be mapped instead
private static Dictionary<uint, uint> InvulnMap = new Dictionary<uint, uint>()
{
{ 810, 003077 }, // LIVING DEAD
{ 3255, 003077}, // UNDEAD REBIRTH
{ 811, 003077 }, // WALKING DEAD
{ 1302, 002502 }, // HALLOWED GROUND
{ 82, 002502 }, // HALLOWED GROUND
{ 409, 000266 }, // HOLMGANG
{ 1836, 003416 } // SUPERBOLIDE
};
#endregion
}
}
+235
View File
@@ -0,0 +1,235 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Party;
using HSUI.Helpers;
using Dalamud.Bindings.ImGui;
using System;
using FFXIVClientStructs.FFXIV.Client.UI.Info;
namespace HSUI.Interface.Party
{
public enum EnmityLevel : byte
{
Leader = 1,
Second = 2,
Last = 255
}
public enum PartyMemberStatus : byte
{
None,
ViewingCutscene,
Offline,
Dead
}
public unsafe class PartyFramesMember : IPartyFramesMember
{
protected IPartyMember? _partyMember = null;
private string _name = "";
private uint _jobId = 0;
private uint _objectID = 0;
public uint ObjectId => _partyMember != null ? _partyMember.EntityId : _objectID;
public ICharacter? Character { get; private set; }
public CrossRealmMember? CrossCharacter { get; private set; }
public int Index { get; set; }
public int Order { get; set; }
public string Name => _partyMember != null ? _partyMember.Name.ToString() : (Character != null ? Character.Name.ToString() : _name);
public uint Level => _partyMember != null ? _partyMember.Level : (Character != null ? Character.Level : (uint)0);
public uint JobId => _partyMember != null ? _partyMember.ClassJob.RowId : (Character != null ? Character.ClassJob.RowId : _jobId);
public uint HP => _partyMember != null ? _partyMember.CurrentHP : (Character != null ? Character.CurrentHp : (uint)0);
public uint MaxHP => _partyMember != null ? _partyMember.MaxHP : (Character != null ? Character.MaxHp : (uint)0);
public uint MP => _partyMember != null ? _partyMember.CurrentMP : JobsHelper.CurrentPrimaryResource(Character);
public uint MaxMP => _partyMember != null ? _partyMember.MaxMP : JobsHelper.MaxPrimaryResource(Character);
public float Shield => Utils.ActorShieldValue(Character);
public EnmityLevel EnmityLevel { get; private set; } = EnmityLevel.Last;
public PartyMemberStatus Status { get; private set; } = PartyMemberStatus.None;
public ReadyCheckStatus ReadyCheckStatus { get; private set; } = ReadyCheckStatus.None;
public bool IsPartyLeader { get; private set; } = false;
public bool IsChocobo { get; private set; } = false;
public float? RaiseTime { get; set; }
public InvulnStatus? InvulnStatus { get; set; }
public bool HasDispellableDebuff { get; set; } = false;
public WhosTalkingState WhosTalkingState => WhosTalkingHelper.Instance?.GetUserState(Name) ?? WhosTalkingState.None;
public PartyFramesMember(IPartyMember partyMember, int index, int order, EnmityLevel enmityLevel, PartyMemberStatus status, ReadyCheckStatus readyCheckStatus, bool isPartyLeader, bool isChocobo = false)
{
_partyMember = partyMember;
Index = index;
Order = order;
EnmityLevel = enmityLevel;
Status = status;
ReadyCheckStatus = readyCheckStatus;
IsPartyLeader = isPartyLeader;
IsChocobo = isChocobo;
var gameObject = partyMember.GameObject;
if (gameObject is ICharacter character)
{
Character = character;
}
}
public PartyFramesMember(ICharacter character, int index, int order, EnmityLevel enmityLevel, PartyMemberStatus status, ReadyCheckStatus readyCheckStatus, bool isPartyLeader, bool isChocobo = false)
{
Index = index;
Order = order;
EnmityLevel = enmityLevel;
Status = status;
ReadyCheckStatus = readyCheckStatus;
IsPartyLeader = isPartyLeader;
IsChocobo = isChocobo;
_objectID = (uint)character.GameObjectId;
Character = character;
}
public PartyFramesMember(uint objectId, int index, int order, EnmityLevel enmityLevel, PartyMemberStatus status, ReadyCheckStatus readyCheckStatus, bool isPartyLeader, bool isChocobo = false)
{
Index = index;
Order = order;
EnmityLevel = enmityLevel;
Status = status;
ReadyCheckStatus = readyCheckStatus;
IsPartyLeader = isPartyLeader;
IsChocobo = isChocobo;
_objectID = objectId;
var gameObject = Plugin.ObjectTable.SearchById(ObjectId);
Character = gameObject is ICharacter ? (ICharacter)gameObject : null;
}
public PartyFramesMember(CrossRealmMember member, int index, int order, PartyMemberStatus status, ReadyCheckStatus readyCheckStatus, bool isPartyLeader, bool isChocobo = false)
{
Index = index;
Order = order;
Status = status;
ReadyCheckStatus = readyCheckStatus;
IsPartyLeader = isPartyLeader;
IsChocobo = isChocobo;
_objectID = (uint)member.EntityId;
CrossCharacter = member;
_name = member.NameString;
_jobId = member.ClassJobId;
}
public void Update(EnmityLevel enmityLevel, PartyMemberStatus status, ReadyCheckStatus readyCheckStatus, bool isPartyLeader, uint jobId = 0)
{
EnmityLevel = enmityLevel;
Status = status;
ReadyCheckStatus = readyCheckStatus;
IsPartyLeader = isPartyLeader;
if (ObjectId == 0)
{
Character = null;
return;
}
var gameObject = Plugin.ObjectTable.SearchById(ObjectId);
Character = gameObject is ICharacter ? (ICharacter)gameObject : null;
if (jobId > 0)
{
_jobId = jobId;
}
else if (Character != null)
{
_jobId = Character.ClassJob.RowId;
}
if (status == PartyMemberStatus.None && Character != null && MaxHP > 0 && HP <= 0)
{
Status = PartyMemberStatus.Dead;
}
}
}
public class FakePartyFramesMember : IPartyFramesMember
{
public static readonly Random RNG = new Random((int)ImGui.GetTime());
public uint ObjectId => 0xE0000000;
public ICharacter? Character => null;
public int Index { get; set; }
public int Order { get; set; }
public string Name { get; private set; }
public uint Level { get; private set; }
public uint JobId { get; private set; }
public uint HP { get; private set; }
public uint MaxHP { get; private set; }
public uint MP { get; private set; }
public uint MaxMP { get; private set; }
public float Shield { get; private set; }
public EnmityLevel EnmityLevel { get; private set; }
public PartyMemberStatus Status { get; private set; }
public ReadyCheckStatus ReadyCheckStatus { get; private set; }
public bool IsPartyLeader { get; }
public bool IsChocobo { get; }
public float? RaiseTime { get; set; }
public InvulnStatus? InvulnStatus { get; set; }
public bool HasDispellableDebuff { get; set; }
public WhosTalkingState WhosTalkingState { get; set; }
public FakePartyFramesMember(int order)
{
Name = RNG.Next(0, 2) == 1 ? "Fake Name" : "FakeLonger MockedName";
Index = order;
Order = order + 1;
Level = (uint)RNG.Next(1, 80);
JobId = (uint)RNG.Next(19, 41);
MaxHP = (uint)RNG.Next(90000, 150000);
HP = order == 2 || order == 3 ? 0 : (uint)(MaxHP * RNG.Next(50, 100) / 100f);
MaxMP = 10000;
MP = order == 2 || order == 3 ? 0 : (uint)(MaxMP * RNG.Next(100) / 100f);
Shield = order == 2 || order == 3 ? 0 : RNG.Next(30) / 100f;
EnmityLevel = order <= 1 ? (EnmityLevel)order + 1 : EnmityLevel.Last;
Status = order < 3 ? PartyMemberStatus.None : (order == 3 ? PartyMemberStatus.Dead : (PartyMemberStatus)RNG.Next(0, 3));
ReadyCheckStatus = (ReadyCheckStatus)RNG.Next(0, 3);
IsPartyLeader = order == 0;
IsChocobo = RNG.Next(0, 8) == 1;
HasDispellableDebuff = RNG.Next(0, 2) == 1;
RaiseTime = order == 2 ? RNG.Next(0, 60) : null;
InvulnStatus = order == 0 ? new InvulnStatus(3077, RNG.Next(0, 10), 810) : null;
WhosTalkingState = (WhosTalkingState)RNG.Next(0, 4);
}
public void Update(EnmityLevel enmityLevel, PartyMemberStatus status, ReadyCheckStatus readyCheckStatus, bool isPartyLeader, uint jobId = 0)
{
}
}
public interface IPartyFramesMember
{
public uint ObjectId { get; }
public ICharacter? Character { get; }
public int Index { get; }
public int Order { get; }
public string Name { get; }
public uint Level { get; }
public uint JobId { get; }
public uint HP { get; }
public uint MaxHP { get; }
public uint MP { get; }
public uint MaxMP { get; }
public float Shield { get; }
public EnmityLevel EnmityLevel { get; }
public PartyMemberStatus Status { get; }
public ReadyCheckStatus ReadyCheckStatus { get; }
public bool IsPartyLeader { get; }
public bool IsChocobo { get; }
public float? RaiseTime { get; set; }
public InvulnStatus? InvulnStatus { get; set; }
public bool HasDispellableDebuff { get; set; }
public WhosTalkingState WhosTalkingState { get; }
public void Update(EnmityLevel enmityLevel, PartyMemberStatus status, ReadyCheckStatus readyCheckStatus, bool isPartyLeader, uint jobId = 0);
}
}
+185
View File
@@ -0,0 +1,185 @@
using Dalamud.Game.ClientState.Objects.Types;
using HSUI.Config;
using HSUI.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
namespace HSUI.Interface.Party
{
public class PartyFramesRaiseTracker : IDisposable
{
private PartyFramesRaiseTrackerConfig _config = null!;
public PartyFramesRaiseTracker()
{
ConfigurationManager.Instance.ResetEvent += OnConfigReset;
OnConfigReset(ConfigurationManager.Instance);
}
~PartyFramesRaiseTracker()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected void Dispose(bool disposing)
{
if (!disposing)
{
return;
}
ConfigurationManager.Instance.ResetEvent -= OnConfigReset;
}
public void OnConfigReset(ConfigurationManager sender)
{
_config = sender.GetConfigObject<PartyFramesTrackersConfig>().Raise;
}
public void Update(List<IPartyFramesMember> partyMembers)
{
if (!_config.Enabled)
{
return;
}
Dictionary<uint, IPartyFramesMember> deadAndNotRaised = new Dictionary<uint, IPartyFramesMember>();
float? limitBreakTime = null;
Dictionary<uint, float> raiseTimeMap = new Dictionary<uint, float>();
foreach (var member in partyMembers)
{
if (member.Character == null || member.ObjectId == 0)
{
member.RaiseTime = null;
continue;
}
if (member.HP > 0)
{
member.RaiseTime = null;
}
if (member.Character is not IBattleChara battleChara)
{
continue;
}
// check raise casts
if (Utils.IsActorCasting(battleChara))
{
var remaining = Math.Max(0, battleChara.TotalCastTime - battleChara.CurrentCastTime);
// check limit break
if (IsRaiseLimitBreakAction(battleChara.CastActionId) &&
(limitBreakTime.HasValue && limitBreakTime.Value > remaining))
{
limitBreakTime = remaining;
}
// check regular raise
else if (IsRaiseAction(battleChara.CastActionId))
{
if (raiseTimeMap.TryGetValue((uint)battleChara.CastTargetObjectId, out float raiseTime))
{
if (raiseTime > remaining)
{
raiseTimeMap[(uint)battleChara.CastTargetObjectId] = remaining;
}
}
else
{
raiseTimeMap.Add((uint)battleChara.CastTargetObjectId, remaining);
}
}
}
// check raise buff
if (member.HP <= 0)
{
bool hasBuff = false;
var statusList = Utils.StatusListForBattleChara(battleChara);
foreach (var status in statusList)
{
if (status == null || (status.StatusId != 148 && status.StatusId != 1140))
{
continue;
}
// apply raise data based on buff
member.RaiseTime = status.RemainingTime;
hasBuff = true;
break;
}
if (!hasBuff)
{
deadAndNotRaised.Add(member.ObjectId, member);
}
}
}
// apply raise data based on casts
foreach (var memberId in deadAndNotRaised.Keys)
{
var member = deadAndNotRaised[memberId];
if (raiseTimeMap.TryGetValue(memberId, out float raiseTime))
{
if (limitBreakTime.HasValue && limitBreakTime.Value < raiseTime)
{
member.RaiseTime = limitBreakTime;
}
else
{
member.RaiseTime = raiseTime;
}
}
else
{
member.RaiseTime = limitBreakTime; // its fine if this is null here
}
}
}
#region raise ids
private static bool IsRaiseLimitBreakAction(uint actionId)
{
return LimitBreakIds.Contains(actionId);
}
private static bool IsRaiseAction(uint actionId)
{
return RaiseIds.Contains(actionId);
}
private static List<uint> RaiseIds = new List<uint>()
{
173, // ACN, SMN, SCH
125, // CNH, WHM
3603, // AST
18317, // BLU
22345, // Lost Sacrifice, Bozja
20730, // Lost Arise, Bozja
12996, // Raise L, Eureka
24287 // SGE
};
private static List<uint> LimitBreakIds = new List<uint>()
{
208, // WHM
4247, // SCH
4248, // AST
24859 // SGE
};
#endregion
}
}
+805
View File
@@ -0,0 +1,805 @@
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Memory;
using HSUI.Config;
using HSUI.Helpers;
using FFXIVClientStructs.FFXIV.Client.Game.Group;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Info;
using Lumina.Excel.Sheets;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.UI.Arrays;
using FFXIVClientStructs.FFXIV.Component.GUI;
using static FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager;
using DalamudPartyMember = Dalamud.Game.ClientState.Party.IPartyMember;
using StructsPartyMember = FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember;
namespace HSUI.Interface.Party
{
public delegate void PartyMembersChangedEventHandler(PartyManager sender);
public unsafe class PartyManager : IDisposable
{
#region Singleton
public static PartyManager Instance { get; private set; } = null!;
private PartyFramesConfig _config = null!;
private PartyFramesIconsConfig _iconsConfig = null!;
private PartyManager()
{
_readyCheckHelper = new PartyReadyCheckHelper();
_raiseTracker = new PartyFramesRaiseTracker();
_invulnTracker = new PartyFramesInvulnTracker();
_cleanseTracker = new PartyFramesCleanseTracker();
ConfigurationManager.Instance.ResetEvent += OnConfigReset;
OnConfigReset(ConfigurationManager.Instance);
UpdatePreview();
// find offline string for active language
if (Plugin.DataManager.GetExcelSheet<Addon>().TryGetRow(9836, out Addon row))
{
_offlineString = row.Text.ToString();
}
}
public static void Initialize()
{
Instance = new PartyManager();
}
~PartyManager()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected void Dispose(bool disposing)
{
if (!disposing)
{
return;
}
_readyCheckHelper.Dispose();
_raiseTracker.Dispose();
_invulnTracker.Dispose();
_cleanseTracker.Dispose();
_config.ValueChangeEvent -= OnConfigPropertyChanged;
Instance = null!;
}
private void OnConfigReset(ConfigurationManager sender)
{
if (_config != null)
{
_config.ValueChangeEvent -= OnConfigPropertyChanged;
}
_config = sender.GetConfigObject<PartyFramesConfig>();
_config.ValueChangeEvent += OnConfigPropertyChanged;
_iconsConfig = ConfigurationManager.Instance.GetConfigObject<PartyFramesIconsConfig>();
}
#endregion Singleton
public AddonPartyList* PartyListAddon { get; private set; } = null;
public IntPtr HudAgent { get; private set; } = IntPtr.Zero;
private RaptureAtkModule* _raptureAtkModule = null;
private const int PartyListInfoOffset = 0x0D40;
private const int PartyListMemberRawInfoSize = 0x28;
private const int PartyMembersInfoIndex = 12; // TODO: Should be reworked to use PartyMemberListStringArray.Instance()
private List<IPartyFramesMember> _groupMembers = new List<IPartyFramesMember>();
public IReadOnlyCollection<IPartyFramesMember> GroupMembers => _groupMembers.AsReadOnly();
private List<IPartyFramesMember> _sortedGroupMembers = new List<IPartyFramesMember>();
public IReadOnlyCollection<IPartyFramesMember> SortedGroupMembers => _sortedGroupMembers.AsReadOnly();
public uint MemberCount => (uint)_groupMembers.Count;
private string? _partyTitle = null;
public string PartyTitle => _partyTitle ?? "";
private int _groupMemberCount => GroupManager.Instance()->MainGroup.MemberCount;
private int _realMemberCount => PartyListAddon != null ? PartyListAddon->MemberCount : Plugin.PartyList.Length;
private int _realMemberAndChocoboCount => PartyListAddon != null ? PartyListAddon->MemberCount + Math.Max(1, (int)PartyListAddon->ChocoboCount) : Plugin.PartyList.Length;
private Dictionary<string, InternalMemberData> _prevDataMap = new();
private bool _wasRealGroup = false;
private bool _wasCrossWorld = false;
private InfoProxyCrossRealm* _crossRealmInfo => InfoProxyCrossRealm.Instance();
private Group _mainGroup => GroupManager.Instance()->MainGroup;
private string _offlineString = "offline";
private PartyReadyCheckHelper _readyCheckHelper;
private PartyFramesRaiseTracker _raiseTracker;
private PartyFramesInvulnTracker _invulnTracker;
private PartyFramesCleanseTracker _cleanseTracker;
public event PartyMembersChangedEventHandler? MembersChangedEvent;
public bool Previewing => _config.Preview;
public bool IsSoloParty()
{
if (!_config.ShowWhenSolo) { return false; }
return _groupMembers.Count <= 1 ||
(_groupMembers.Count == 2 && _config.ShowChocobo &&
_groupMembers[1].Character is IBattleNpc npc && npc.BattleNpcKind == BattleNpcSubKind.Chocobo);
}
public void Update()
{
// find party list hud agent
PartyListAddon = (AddonPartyList*)Plugin.GameGui.GetAddonByName("_PartyList", 1).Address;
HudAgent = Plugin.GameGui.FindAgentInterface(PartyListAddon);
if (PartyListAddon == null || HudAgent == IntPtr.Zero)
{
if (_groupMembers.Count > 0)
{
_groupMembers.Clear();
MembersChangedEvent?.Invoke(this);
}
return;
}
_raptureAtkModule = RaptureAtkModule.Instance();
// no need to update on preview mode
if (_config.Preview)
{
return;
}
InternalUpdate();
}
private void InternalUpdate()
{
IPlayerCharacter? player = Plugin.ObjectTable.LocalPlayer;
if (player is null || player is not IPlayerCharacter)
{
return;
}
bool isCrossWorld = IsCrossWorldParty();
// ready check update
if (_iconsConfig.ReadyCheckStatus.Enabled)
{
_readyCheckHelper.Update(_iconsConfig.ReadyCheckStatus.Duration);
}
try
{
// title
_partyTitle = GetPartyListTitle();
// solo
if (_realMemberCount <= 1 && PartyListAddon->TrustCount == 0)
{
if (_config.ShowWhenSolo)
{
UpdateSoloParty(player);
}
else if (_groupMembers.Count > 0)
{
_groupMembers.Clear();
MembersChangedEvent?.Invoke(this);
}
_wasRealGroup = false;
}
else
{
// player maps
Dictionary<string, InternalMemberData> dataMap = GetMembersDataMap(player, isCrossWorld);
bool partyChanged = _prevDataMap.Count != dataMap.Count;
if (!partyChanged)
{
foreach (string key in dataMap.Keys)
{
InternalMemberData newData = dataMap[key];
if (!_prevDataMap.TryGetValue(key, out InternalMemberData? oldData) ||
oldData == null ||
newData.Order != oldData.Order)
{
partyChanged = true;
break;
}
}
}
_prevDataMap = dataMap;
// trust
if (PartyListAddon->TrustCount > 0)
{
UpdateTrustParty(player, dataMap, partyChanged);
}
// cross world party/alliance
else if (isCrossWorld)
{
UpdateCrossWorldParty(player, dataMap, partyChanged);
}
// regular party
else
{
UpdateRegularParty(player, dataMap, partyChanged);
}
_wasRealGroup = true;
}
UpdateTrackers();
}
catch (Exception e)
{
Plugin.Logger.Warning(e.Message);
}
_wasCrossWorld = isCrossWorld;
}
private Dictionary<string, InternalMemberData> GetMembersDataMap(IPlayerCharacter player, bool isCrossWorld)
{
Dictionary<string, InternalMemberData> dataMap = new Dictionary<string, InternalMemberData>();
if (_raptureAtkModule == null || _raptureAtkModule->AtkModule.AtkArrayDataHolder.StringArrayCount <= PartyMembersInfoIndex)
{
return dataMap;
}
// raw info
int allianceNum = FindAlliance(player);
int count = isCrossWorld ? _crossRealmInfo->CrossRealmGroups[allianceNum].GroupMemberCount : _realMemberCount + PartyListAddon->TrustCount;
for (int i = 0; i < count; i++)
{
InternalMemberData data = new InternalMemberData();
data.Index = i;
if (!isCrossWorld)
{
PartyListMemberRawInfo* info = (PartyListMemberRawInfo*)(HudAgent + (PartyListInfoOffset + PartyListMemberRawInfoSize * i));
data.ObjectId = info->ObjectId;
data.ContentId = info->ContentId;
data.Name = info->Name;
data.Order = info->Order;
}
else
{
CrossRealmMember member = _crossRealmInfo->CrossRealmGroups[allianceNum].GroupMembers[i];
data.ObjectId = member.EntityId;
data.ContentId = (long)member.ContentId;
data.Name = member.NameString;
data.Order = i;
}
if (!dataMap.ContainsKey(data.Name))
{
dataMap.Add(data.Name, data);
}
}
// status string
var stringArrayData = _raptureAtkModule->AtkModule.AtkArrayDataHolder.StringArrays[PartyMembersInfoIndex];
for (int i = 0; i < count; i++)
{
int index = i * 5;
if (stringArrayData->AtkArrayData.Size <= index + 3 ||
stringArrayData->StringArray[index] == null ||
stringArrayData->StringArray[index + 3] == null) { break; }
IntPtr ptr = new IntPtr(stringArrayData->StringArray[index]);
string name = MemoryHelper.ReadSeStringNullTerminated(ptr).ToString();
ptr = new IntPtr(stringArrayData->StringArray[index + 3]);
string a = MemoryHelper.ReadSeStringNullTerminated(ptr).ToString();
if (dataMap.TryGetValue(name, out InternalMemberData? data) && data != null)
{
data.Status = MemoryHelper.ReadSeStringNullTerminated(ptr).ToString();
}
}
return dataMap;
}
private bool IsCrossWorldParty()
{
return _crossRealmInfo->IsCrossRealm && _crossRealmInfo->GroupCount > 0 && _mainGroup.MemberCount == 0;
}
private ReadyCheckStatus GetReadyCheckStatus(ulong contentId)
{
return _readyCheckHelper.GetStatusForContentId(contentId);
}
private void UpdateTrustParty(IPlayerCharacter player, Dictionary<string, InternalMemberData> dataMap, bool forced)
{
bool softUpdate = true;
if (_groupMembers.Count != dataMap.Count || forced)
{
_groupMembers.Clear();
softUpdate = false;
}
if (softUpdate)
{
foreach (IPartyFramesMember member in _groupMembers)
{
if (member.ObjectId == player.GameObjectId)
{
member.Update(EnmityForIndex(member.Index), PartyMemberStatus.None, ReadyCheckStatus.None, true, player.ClassJob.RowId);
}
else
{
member.Update(EnmityForTrustMemberIndex(member.Index - 1), PartyMemberStatus.None, ReadyCheckStatus.None, false, 0);
}
}
}
else
{
string[] keys = dataMap.Keys.ToArray();
for (int i = 0; i < keys.Length; i++)
{
InternalMemberData data = dataMap[keys[i]];
if (keys[i] == player.Name.ToString())
{
PartyFramesMember playerMember = new PartyFramesMember(player, data.Index, data.Order, EnmityForIndex(i), PartyMemberStatus.None, ReadyCheckStatus.None, true);
_groupMembers.Add(playerMember);
}
else
{
ICharacter? trustChara = Utils.GetGameObjectByName(keys[i]) as ICharacter;
if (trustChara != null)
{
_groupMembers.Add(new PartyFramesMember(trustChara, data.Index, data.Order, EnmityForTrustMemberIndex(i), PartyMemberStatus.None, ReadyCheckStatus.None, false));
}
}
}
}
if (!softUpdate)
{
SortGroupMembers(player);
MembersChangedEvent?.Invoke(this);
}
}
private void UpdateSoloParty(IPlayerCharacter player)
{
ICharacter? chocobo = null;
if (_config.ShowChocobo)
{
var gameObject = Utils.GetBattleChocobo(player);
if (gameObject != null && gameObject is ICharacter)
{
chocobo = (ICharacter)gameObject;
}
}
bool needsUpdate =
_groupMembers.Count == 0 ||
(_groupMembers.Count != 2 && _config.ShowChocobo && chocobo != null) ||
(_groupMembers.Count > 1 && !_config.ShowChocobo) ||
(_groupMembers.Count > 1 && chocobo == null) ||
(_groupMembers.Count == 2 && _config.ShowChocobo && _groupMembers[1].ObjectId != chocobo?.EntityId);
EnmityLevel playerEnmity = PartyListAddon->EnmityLeaderIndex == 0 ? EnmityLevel.Leader : EnmityLevel.Last;
// for some reason chocobos never get a proper enmity value even though they have aggro
// if the player enmity is set to first, but the "leader index" is invalid
// we can pretty much deduce that the chocobo is the one with aggro
// this might fail on some cases when there are other players not in party hitting the same thing
// but the edge case is so minor we should be fine
EnmityLevel chocoboEnmity = PartyListAddon->EnmityLeaderIndex == -1 && PartyListAddon->PartyMembers[0].EmnityByte == 1 ? EnmityLevel.Leader : EnmityLevel.Last;
if (needsUpdate)
{
_groupMembers.Clear();
_groupMembers.Add(new PartyFramesMember(player, 0, 0, playerEnmity, PartyMemberStatus.None, ReadyCheckStatus.None, true));
if (chocobo != null)
{
_groupMembers.Add(new PartyFramesMember(chocobo, 1, 1, chocoboEnmity, PartyMemberStatus.None, ReadyCheckStatus.None, false));
}
SortGroupMembers(player);
MembersChangedEvent?.Invoke(this);
}
else
{
for (int i = 0; i < _groupMembers.Count; i++)
{
_groupMembers[i].Update(i == 0 ? playerEnmity : chocoboEnmity, PartyMemberStatus.None, ReadyCheckStatus.None, i == 0, i == 0 ? player.ClassJob.RowId : 0);
}
}
}
private void UpdateCrossWorldParty(IPlayerCharacter player, Dictionary<string, InternalMemberData> dataMap, bool forced)
{
bool softUpdate = true;
int allianceNum = FindAlliance(player);
int count = _crossRealmInfo->CrossRealmGroups[allianceNum].GroupMemberCount;
if (!_wasCrossWorld || count != _groupMembers.Count || forced)
{
_groupMembers.Clear();
softUpdate = false;
}
// create new members array with cross world data
for (int i = 0; i < count; i++)
{
CrossRealmMember member = _crossRealmInfo->CrossRealmGroups[allianceNum].GroupMembers[i];
string memberName = member.NameString;
if (!dataMap.TryGetValue(memberName, out InternalMemberData? data) || data == null)
{
continue;
}
bool isPlayer = member.EntityId == player.EntityId;
bool isLeader = member.IsPartyLeader;
PartyMemberStatus status = data.Status != null ? StatusForCrossWorldMember(data.Status) : PartyMemberStatus.None;
ReadyCheckStatus readyCheckStatus = GetReadyCheckStatus(member.ContentId);
if (softUpdate)
{
IPartyFramesMember groupMember = _groupMembers.ElementAt(i);
groupMember.Update(EnmityLevel.Last, status, readyCheckStatus, isLeader, member.ClassJobId);
}
else
{
PartyFramesMember partyMember = isPlayer ?
new PartyFramesMember(player, i, data.Order, EnmityLevel.Last, status, readyCheckStatus, isLeader) :
new PartyFramesMember(member, i, data.Order, status, readyCheckStatus, isLeader);
_groupMembers.Add(partyMember);
}
}
if (!softUpdate)
{
SortGroupMembers(player);
MembersChangedEvent?.Invoke(this);
}
}
private int FindAlliance(IPlayerCharacter player)
{
for (int i = 0; i < _crossRealmInfo->CrossRealmGroups.Length; i++)
{
for (int j = 0; j < _crossRealmInfo->CrossRealmGroups[i].GroupMemberCount; j++)
{
CrossRealmMember member = _crossRealmInfo->CrossRealmGroups[i].GroupMembers[j];
if (member.EntityId == player.EntityId)
{
return i;
}
}
}
return 0;
}
private void UpdateRegularParty(IPlayerCharacter player, Dictionary<string, InternalMemberData> dataMap, bool forced)
{
bool softUpdate = true;
if (!_wasRealGroup || _realMemberCount != _groupMembers.Count || forced)
{
_groupMembers.Clear();
softUpdate = false;
}
string[] keys = dataMap.Keys.ToArray();
for (int i = 0; i < keys.Length; i++)
{
if (!dataMap.TryGetValue(keys[i], out InternalMemberData? data) || data == null)
{
continue;
}
bool isPlayer = data.ObjectId == player.GameObjectId;
bool isLeader = IsPartyLeader(data.Order);
EnmityLevel enmity = EnmityForIndex(data.Index);
PartyMemberStatus status = data.Status != null ? StatusForMember(data.Status, data.Name) : PartyMemberStatus.None;
ReadyCheckStatus readyCheckStatus = GetReadyCheckStatus((ulong)data.ContentId);
if (softUpdate)
{
IPartyFramesMember groupMember = _groupMembers.ElementAt(i);
groupMember.Update(enmity, status, readyCheckStatus, isLeader);
}
else
{
PartyFramesMember partyMember;
var member = GetDalamudPartyMember(data.Name);
if (member.HasValue && member.Value.Item1 is DalamudPartyMember dalamudPartyMember)
{
partyMember = new PartyFramesMember(dalamudPartyMember, i, data.Order, enmity, status, readyCheckStatus, isLeader);
}
else
{
partyMember = new PartyFramesMember(data.ObjectId, i, data.Order, enmity, status, readyCheckStatus, isLeader);
}
_groupMembers.Add(partyMember);
}
}
// player's chocobo (always last)
if (_config.ShowChocobo)
{
IGameObject? companion = Utils.GetBattleChocobo(player);
if (softUpdate && _groupMembers.FirstOrDefault(o => o.IsChocobo) is PartyFramesMember chocoboMember)
{
if (companion is ICharacter)
{
chocoboMember.Update(EnmityLevel.Last, PartyMemberStatus.None, ReadyCheckStatus.None, false);
}
else
{
_groupMembers.Remove(chocoboMember);
}
}
else if (companion is ICharacter companionCharacter)
{
_groupMembers.Add(new PartyFramesMember(companionCharacter, _groupMemberCount, 10, EnmityLevel.Last, PartyMemberStatus.None, ReadyCheckStatus.None, false, true));
}
}
if (!softUpdate)
{
SortGroupMembers(player);
MembersChangedEvent?.Invoke(this);
}
}
private void SortGroupMembers(IPlayerCharacter? player = null)
{
_sortedGroupMembers.Clear();
_sortedGroupMembers.AddRange(_groupMembers);
_sortedGroupMembers.Sort((a, b) =>
{
if (a.Order == b.Order)
{
if (a.ObjectId == player?.GameObjectId)
{
return 1;
}
else if (b.ObjectId == player?.GameObjectId)
{
return -1;
}
return a.Name.CompareTo(b.Name);
}
if (a.Order < b.Order)
{
return -1;
}
return 1;
});
}
private (DalamudPartyMember?, int)? GetDalamudPartyMember(string name)
{
for (int i = 0; i < Plugin.PartyList.Length; i++)
{
DalamudPartyMember? member = Plugin.PartyList[i];
if (member != null && member.Name.ToString() == name)
{
return (member, i);
}
}
return null;
}
private void UpdateTrackers()
{
_raiseTracker.Update(_groupMembers);
_invulnTracker.Update(_groupMembers);
_cleanseTracker.Update(_groupMembers);
}
#region utils
private bool IsPartyLeader(int index)
{
if (PartyListAddon == null)
{
return false;
}
// we use the icon Y coordinate in the party list to know the index (lmao)
uint partyLeadIndex = (uint)PartyListAddon->LeaderMarkResNode->ChildNode->Y / 40;
return index == partyLeadIndex;
}
private PartyMemberStatus StatusForCrossWorldMember(string statusStr)
{
// offline status
if (statusStr.Contains(_offlineString, StringComparison.InvariantCultureIgnoreCase))
{
return PartyMemberStatus.Offline;
}
return PartyMemberStatus.None;
}
private PartyMemberStatus StatusForMember(string statusStr, string name)
{
// offline status
if (statusStr.Contains(_offlineString, StringComparison.InvariantCultureIgnoreCase))
{
return PartyMemberStatus.Offline;
}
// viewing cutscene status
for (int i = 0; i < _mainGroup.MemberCount; i++)
{
if (_mainGroup.PartyMembers[i].NameString == name)
{
if ((_mainGroup.PartyMembers[i].Flags & 0x10) != 0)
{
return PartyMemberStatus.ViewingCutscene;
}
break;
}
}
return PartyMemberStatus.None;
}
private EnmityLevel EnmityForIndex(int index)
{
if (PartyListAddon == null || index < 0 || index > 7)
{
return EnmityLevel.Last;
}
EnmityLevel enmityLevel = (EnmityLevel)PartyListAddon->PartyMembers[index].EmnityByte;
if (enmityLevel == EnmityLevel.Leader && PartyListAddon->EnmityLeaderIndex != index)
{
enmityLevel = EnmityLevel.Last;
}
return enmityLevel;
}
private EnmityLevel EnmityForTrustMemberIndex(int index)
{
if (PartyListAddon == null || index < 0 || index > 6)
{
return EnmityLevel.Last;
}
return (EnmityLevel)PartyListAddon->TrustMembers[index].EmnityByte;
}
private static unsafe string? GetPartyListTitle()
{
AgentModule* agentModule = AgentModule.Instance();
if (agentModule == null) { return ""; }
AgentHUD* agentHUD = agentModule->GetAgentHUD();
if (agentHUD == null) { return ""; }
Lumina.Excel.ExcelSheet<Addon> sheet = Plugin.DataManager.GetExcelSheet<Addon>();
if (sheet.TryGetRow(agentHUD->PartyTitleAddonId, out Addon row))
{
return row.Text.ToString();
}
return null;
}
#endregion
#region events
private void OnConfigPropertyChanged(object sender, OnChangeBaseArgs args)
{
if (args.PropertyName == "Preview")
{
UpdatePreview();
}
}
public void UpdatePreview()
{
_iconsConfig.Sign.Preview = _config.Preview;
if (!_config.Preview)
{
_groupMembers.Clear();
MembersChangedEvent?.Invoke(this);
return;
}
// fill list with fake members for UI testing
_groupMembers.Clear();
if (_config.Preview)
{
int count = FakePartyFramesMember.RNG.Next(4, 9);
for (int i = 0; i < count; i++)
{
_groupMembers.Add(new FakePartyFramesMember(i));
}
}
SortGroupMembers();
MembersChangedEvent?.Invoke(this);
}
#endregion
}
internal class InternalMemberData
{
internal uint ObjectId = 0;
internal long ContentId = 0;
internal string Name = "";
internal uint JobId = 0;
internal int Order = 0;
internal int Index = 0;
internal string? Status = null;
public InternalMemberData()
{
}
}
[StructLayout(LayoutKind.Explicit, Size = 28)]
public unsafe struct PartyListMemberRawInfo
{
[FieldOffset(0x00)] public byte* NamePtr;
[FieldOffset(0x08)] public long ContentId;
[FieldOffset(0x10)] public uint ObjectId;
// some kind of type
// 1 = player
// 2 = party member?
// 3 = unknown
// 4 = chocobo
// 5 = summon?
[FieldOffset(0x14)] public byte Type;
[FieldOffset(0x18)] public byte Order;
public string Name => Marshal.PtrToStringUTF8(new IntPtr(NamePtr)) ?? "";
}
}
+171
View File
@@ -0,0 +1,171 @@
using Dalamud.Game.ClientState.Objects.SubKinds;
using HSUI.Helpers;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using System.Collections.Generic;
namespace HSUI.Interface.Party
{
public static class PartyOrderHelper
{
private enum PartySortingSetting
{
Tank_Healer_DPS = 0,
Tank_DPS_Healer = 1,
Healer_Tank_DPS = 2,
Healer_DPS_Tank = 3,
DPS_Tank_Healer = 4,
DPS_Healer_Tank = 5,
Count = 6
}
private class PartyRoles
{
internal int Tank;
internal int Healer;
internal int DPS;
internal int Other;
public PartyRoles()
{
Tank = 0;
Healer = 0;
DPS = 0;
Other = 0;
}
public PartyRoles(int tank, int healer, int dps, int other)
{
Tank = tank;
Healer = healer;
DPS = dps;
Other = other;
}
}
// calcualates the position for the player if they select the
// option to always appear as the first of their current role
// in the party frames
public static int? GetRoleFirstOrder(List<IPartyFramesMember> members)
{
IPlayerCharacter? player = Plugin.ObjectTable.LocalPlayer;
if (player == null) { return null; }
JobRoles role = JobsHelper.RoleForJob(player.ClassJob.RowId);
PartySortingSetting? setting = GetPartySortingSetting(role);
if (!setting.HasValue) { return null; }
PartyRoles rolesCount = GetPartyCountByRole(members);
PartyRoles roleWeights = GetRoleWeights(role, setting.Value);
return rolesCount.Tank * roleWeights.Tank +
rolesCount.Healer * roleWeights.Healer +
rolesCount.DPS * roleWeights.DPS +
rolesCount.Other * roleWeights.Other;
}
private static unsafe PartySortingSetting? GetPartySortingSetting(JobRoles role)
{
ConfigModule* config = ConfigModule.Instance();
if (config == null) { return null; }
ConfigOption option;
switch (role)
{
case JobRoles.Tank: option = ConfigOption.PartyListSortTypeTank; break;
case JobRoles.Healer: option = ConfigOption.PartyListSortTypeHealer; break;
case JobRoles.DPSMelee:
case JobRoles.DPSRanged:
case JobRoles.DPSCaster: option = ConfigOption.PartyListSortTypeDps; break;
default: option = ConfigOption.PartyListSortTypeOther; break;
}
Framework* framework = Framework.Instance();
if (framework == null || framework->SystemConfig.SystemConfigBase.UiConfig.ConfigCount <= (int)option) {
return PartySortingSetting.Tank_Healer_DPS;
}
uint value = framework->SystemConfig.SystemConfigBase.UiConfig.ConfigEntry[(int)option].Value.UInt;
if (value < 0 || value > (int)PartySortingSetting.Count) { return null; }
return (PartySortingSetting)value;
}
private static unsafe PartyRoles GetPartyCountByRole(List<IPartyFramesMember> members)
{
PartyRoles rolesCount = new PartyRoles();
IPlayerCharacter? player = Plugin.ObjectTable.LocalPlayer;
if (player == null) { return rolesCount; }
foreach (IPartyFramesMember member in members)
{
if (member.ObjectId == player.GameObjectId) { continue; }
JobRoles role = JobsHelper.RoleForJob(member.JobId);
switch (role)
{
case JobRoles.Tank: rolesCount.Tank++; break;
case JobRoles.Healer: rolesCount.Healer++; break;
case JobRoles.DPSMelee:
case JobRoles.DPSRanged:
case JobRoles.DPSCaster: rolesCount.DPS++; break;
default: rolesCount.Other++; break;
}
}
return rolesCount;
}
private static unsafe PartyRoles GetRoleWeights(JobRoles role, PartySortingSetting setting)
{
if (role == JobRoles.Crafter || role == JobRoles.Gatherer || role == JobRoles.Unknown)
{
return new PartyRoles(1, 1, 1, 0);
}
JobRoles mapRole = role == JobRoles.DPSRanged || role == JobRoles.DPSCaster ? JobRoles.DPSMelee : role;
return RoleWeights[mapRole][setting];
}
private static Dictionary<JobRoles, Dictionary<PartySortingSetting, PartyRoles>> RoleWeights = new Dictionary<JobRoles, Dictionary<PartySortingSetting, PartyRoles>>()
{
[JobRoles.Tank] = new Dictionary<PartySortingSetting, PartyRoles>()
{
[PartySortingSetting.Tank_Healer_DPS] = new PartyRoles(),
[PartySortingSetting.Tank_DPS_Healer] = new PartyRoles(),
[PartySortingSetting.Healer_Tank_DPS] = new PartyRoles(0, 1, 0, 0),
[PartySortingSetting.Healer_DPS_Tank] = new PartyRoles(0, 1, 1, 0),
[PartySortingSetting.DPS_Tank_Healer] = new PartyRoles(0, 0, 1, 0),
[PartySortingSetting.DPS_Healer_Tank] = new PartyRoles(0, 1, 1, 0)
},
[JobRoles.Healer] = new Dictionary<PartySortingSetting, PartyRoles>()
{
[PartySortingSetting.Tank_Healer_DPS] = new PartyRoles(1, 0, 0, 0),
[PartySortingSetting.Tank_DPS_Healer] = new PartyRoles(1, 0, 1, 0),
[PartySortingSetting.Healer_Tank_DPS] = new PartyRoles(),
[PartySortingSetting.Healer_DPS_Tank] = new PartyRoles(),
[PartySortingSetting.DPS_Tank_Healer] = new PartyRoles(1, 0, 1, 0),
[PartySortingSetting.DPS_Healer_Tank] = new PartyRoles(0, 0, 1, 0)
},
[JobRoles.DPSMelee] = new Dictionary<PartySortingSetting, PartyRoles>()
{
[PartySortingSetting.Tank_Healer_DPS] = new PartyRoles(1, 1, 0, 0),
[PartySortingSetting.Tank_DPS_Healer] = new PartyRoles(1, 0, 0, 0),
[PartySortingSetting.Healer_Tank_DPS] = new PartyRoles(1, 1, 0, 0),
[PartySortingSetting.Healer_DPS_Tank] = new PartyRoles(0, 1, 0, 0),
[PartySortingSetting.DPS_Tank_Healer] = new PartyRoles(),
[PartySortingSetting.DPS_Healer_Tank] = new PartyRoles()
}
};
}
}
+156
View File
@@ -0,0 +1,156 @@
using Dalamud.Hooking;
using Dalamud.Logging;
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Info;
using Dalamud.Bindings.ImGui;
using System;
namespace HSUI.Interface.Party
{
public enum ReadyCheckStatus
{
Ready = 0,
NotReady = 1,
None = 2
}
public class PartyReadyCheckHelper : IDisposable
{
private delegate void ReadyCheckDelegate(IntPtr ptr);
private Hook<AgentReadyCheck.Delegates.InitiateReadyCheck>? _onReadyCheckStartHook;
private Hook<AgentReadyCheck.Delegates.EndReadyCheck>? _onReadyCheckEndHook;
private delegate void ActorControlDelegate(uint entityId, uint type, uint buffID, uint direct, uint actionId, uint sourceId, uint arg7, uint arg8, uint arg9, uint arg10, ulong targetId, byte arg12);
private Hook<ActorControlDelegate>? _actorControlHook;
private bool _readyCheckOngoing = false;
private double _lastReadyCheckEndTime = -1;
public unsafe PartyReadyCheckHelper()
{
try
{
_onReadyCheckStartHook = Plugin.GameInteropProvider.HookFromAddress<AgentReadyCheck.Delegates.InitiateReadyCheck>(
AgentReadyCheck.MemberFunctionPointers.InitiateReadyCheck,
OnReadyCheckStart
);
_onReadyCheckStartHook?.Enable();
_onReadyCheckEndHook = Plugin.GameInteropProvider.HookFromAddress<AgentReadyCheck.Delegates.EndReadyCheck>(
AgentReadyCheck.MemberFunctionPointers.EndReadyCheck,
OnReadycheckEnd
);
_onReadyCheckEndHook?.Enable();
_actorControlHook = Plugin.GameInteropProvider.HookFromSignature<ActorControlDelegate>(
"E8 ?? ?? ?? ?? 0F B7 0B 83 E9 64",
OnActorControl
);
_actorControlHook?.Enable();
}
catch (Exception e)
{
Plugin.Logger.Error("Error initiating ready check sigs!!!\n" + e.Message);
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposing)
{
return;
}
_onReadyCheckStartHook?.Disable();
_onReadyCheckStartHook?.Dispose();
_onReadyCheckEndHook?.Disable();
_onReadyCheckEndHook?.Dispose();
_actorControlHook?.Disable();
_actorControlHook?.Dispose();
}
private unsafe void OnReadyCheckStart(AgentReadyCheck *ptr)
{
_onReadyCheckStartHook?.Original(ptr);
_readyCheckOngoing = true;
_lastReadyCheckEndTime = -1;
}
private unsafe void OnReadycheckEnd(AgentReadyCheck *ptr)
{
_onReadyCheckEndHook?.Original(ptr);
_lastReadyCheckEndTime = ImGui.GetTime();
}
private void OnActorControl(uint entityId, uint type, uint buffID, uint direct, uint actionId, uint sourceId, uint arg7, uint arg8, uint arg9, uint arg10, ulong targetId, byte arg12)
{
_actorControlHook?.Original(entityId, type, buffID, direct, actionId, sourceId, arg7, arg8, arg9, arg10, targetId, arg12);
// I'm not exactly sure what id == 503 means, but its always triggered when the fight starts
// which is all I care about
if (type == 503)
{
_readyCheckOngoing = false;
}
}
public void Update(double maxDuration)
{
if (_readyCheckOngoing &&
_lastReadyCheckEndTime != -1 &&
ImGui.GetTime() - _lastReadyCheckEndTime >= maxDuration)
{
_readyCheckOngoing = false;
}
}
public unsafe ReadyCheckStatus GetStatusForContentId(ulong contentId)
{
if (!_readyCheckOngoing)
{
return ReadyCheckStatus.None;
}
try
{
for (int i = 0; i < 8; i++)
{
ReadyCheckEntry entry = AgentReadyCheck.Instance()->ReadyCheckEntries[i];
if (entry.ContentId == contentId)
{
return ParseStatus(entry);
}
}
}
catch { }
return ReadyCheckStatus.None;
}
private ReadyCheckStatus ParseStatus(ReadyCheckEntry entry)
{
if (entry.Status == FFXIVClientStructs.FFXIV.Client.UI.Agent.ReadyCheckStatus.Ready)
{
return ReadyCheckStatus.Ready;
}
else if (entry.Status == FFXIVClientStructs.FFXIV.Client.UI.Agent.ReadyCheckStatus.NotReady ||
entry.Status == FFXIVClientStructs.FFXIV.Client.UI.Agent.ReadyCheckStatus.MemberNotPresent)
{
return ReadyCheckStatus.NotReady;
}
return ReadyCheckStatus.None;
}
}
}