diff --git a/Glamourer.Api b/Glamourer.Api
index 1c51730..54c1944 160000
--- a/Glamourer.Api
+++ b/Glamourer.Api
@@ -1 +1 @@
-Subproject commit 1c517301c9fd0818e2c02e72304d6de121b9d703
+Subproject commit 54c1944dc7db704733b4788520e494761bb0b58e
diff --git a/Glamourer/Automation/ApplicationType.cs b/Glamourer/Automation/ApplicationType.cs
index 58f296e..f72c93f 100644
--- a/Glamourer/Automation/ApplicationType.cs
+++ b/Glamourer/Automation/ApplicationType.cs
@@ -38,7 +38,7 @@ public static class ApplicationTypeExtensions
var customizeFlags = type.HasFlag(ApplicationType.Customizations) ? CustomizeFlagExtensions.All : 0;
var parameterFlags = type.HasFlag(ApplicationType.Customizations) ? CustomizeParameterExtensions.All : 0;
var crestFlags = type.HasFlag(ApplicationType.GearCustomization) ? CrestExtensions.AllRelevant : 0;
- var metaFlags = (type.HasFlag(ApplicationType.Armor) ? MetaFlag.HatState | MetaFlag.VisorState : 0)
+ var metaFlags = (type.HasFlag(ApplicationType.Armor) ? MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.EarState : 0)
| (type.HasFlag(ApplicationType.Weapons) ? MetaFlag.WeaponState : 0)
| (type.HasFlag(ApplicationType.Customizations) ? MetaFlag.Wetness : 0);
var bonusFlags = type.HasFlag(ApplicationType.Armor) ? BonusExtensions.All : 0;
diff --git a/Glamourer/Designs/ApplicationCollection.cs b/Glamourer/Designs/ApplicationCollection.cs
index 8beeb78..c03d4b4 100644
--- a/Glamourer/Designs/ApplicationCollection.cs
+++ b/Glamourer/Designs/ApplicationCollection.cs
@@ -19,13 +19,13 @@ public record struct ApplicationCollection(
public static readonly ApplicationCollection None = new(0, 0, CustomizeFlag.BodyType, 0, 0, 0);
public static readonly ApplicationCollection Equipment = new(EquipFlagExtensions.All, BonusExtensions.All,
- CustomizeFlag.BodyType, CrestExtensions.AllRelevant, 0, MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState);
+ CustomizeFlag.BodyType, CrestExtensions.AllRelevant, 0, MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState | MetaFlag.EarState);
public static readonly ApplicationCollection Customizations = new(0, 0, CustomizeFlagExtensions.AllRelevant, 0,
CustomizeParameterExtensions.All, MetaFlag.Wetness);
public static readonly ApplicationCollection Default = new(EquipFlagExtensions.All, BonusExtensions.All,
- CustomizeFlagExtensions.AllRelevant, CrestExtensions.AllRelevant, 0, MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState);
+ CustomizeFlagExtensions.AllRelevant, CrestExtensions.AllRelevant, 0, MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState | MetaFlag.EarState);
public static ApplicationCollection FromKeys()
=> (ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift) switch
@@ -47,7 +47,7 @@ public record struct ApplicationCollection(
Equip = 0;
BonusItem = 0;
Crest = 0;
- Meta &= ~(MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState);
+ Meta &= ~(MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState | MetaFlag.EarState);
}
public void RemoveCustomize()
diff --git a/Glamourer/Designs/DesignBase.cs b/Glamourer/Designs/DesignBase.cs
index b21c433..f87d75a 100644
--- a/Glamourer/Designs/DesignBase.cs
+++ b/Glamourer/Designs/DesignBase.cs
@@ -40,7 +40,8 @@ public class DesignBase
}
/// Used when importing .cma or .chara files.
- internal DesignBase(CustomizeService customize, in DesignData designData, EquipFlag equipFlags, CustomizeFlag customizeFlags, BonusItemFlag bonusFlags)
+ internal DesignBase(CustomizeService customize, in DesignData designData, EquipFlag equipFlags, CustomizeFlag customizeFlags,
+ BonusItemFlag bonusFlags)
{
_designData = designData;
ApplyCustomize = customizeFlags & CustomizeFlagExtensions.AllRelevant;
@@ -254,9 +255,10 @@ public class DesignBase
ret[slot.ToString()] = Serialize(item.Id, stains, crest, DoApplyEquip(slot), DoApplyStain(slot), DoApplyCrest(crestSlot));
}
- ret["Hat"] = new QuadBool(_designData.IsHatVisible(), DoApplyMeta(MetaIndex.HatState)).ToJObject("Show", "Apply");
- ret["Visor"] = new QuadBool(_designData.IsVisorToggled(), DoApplyMeta(MetaIndex.VisorState)).ToJObject("IsToggled", "Apply");
- ret["Weapon"] = new QuadBool(_designData.IsWeaponVisible(), DoApplyMeta(MetaIndex.WeaponState)).ToJObject("Show", "Apply");
+ ret["Hat"] = new QuadBool(_designData.IsHatVisible(), DoApplyMeta(MetaIndex.HatState)).ToJObject("Show", "Apply");
+ ret["VieraEars"] = new QuadBool(_designData.AreEarsVisible(), DoApplyMeta(MetaIndex.EarState)).ToJObject("Show", "Apply");
+ ret["Visor"] = new QuadBool(_designData.IsVisorToggled(), DoApplyMeta(MetaIndex.VisorState)).ToJObject("IsToggled", "Apply");
+ ret["Weapon"] = new QuadBool(_designData.IsWeaponVisible(), DoApplyMeta(MetaIndex.WeaponState)).ToJObject("Show", "Apply");
}
else
{
@@ -603,6 +605,10 @@ public class DesignBase
metaValue = QuadBool.FromJObject(equip["Visor"], "IsToggled", "Apply", QuadBool.NullFalse);
design.SetApplyMeta(MetaIndex.VisorState, metaValue.Enabled);
design._designData.SetVisor(metaValue.ForcedValue);
+
+ metaValue = QuadBool.FromJObject(equip["VieraEars"], "Show", "Apply", QuadBool.NullTrue);
+ design.SetApplyMeta(MetaIndex.EarState, metaValue.Enabled);
+ design._designData.SetEarsVisible(metaValue.ForcedValue);
return;
void PrintWarning(string msg)
diff --git a/Glamourer/Designs/DesignData.cs b/Glamourer/Designs/DesignData.cs
index bba0ccb..c7ca8e5 100644
--- a/Glamourer/Designs/DesignData.cs
+++ b/Glamourer/Designs/DesignData.cs
@@ -287,6 +287,7 @@ public unsafe struct DesignData
MetaIndex.HatState => IsHatVisible(),
MetaIndex.VisorState => IsVisorToggled(),
MetaIndex.WeaponState => IsWeaponVisible(),
+ MetaIndex.EarState => AreEarsVisible(),
_ => false,
};
@@ -297,6 +298,7 @@ public unsafe struct DesignData
MetaIndex.HatState => SetHatVisible(value),
MetaIndex.VisorState => SetVisor(value),
MetaIndex.WeaponState => SetWeaponVisible(value),
+ MetaIndex.EarState => SetEarsVisible(value),
_ => false,
};
@@ -340,6 +342,9 @@ public unsafe struct DesignData
public readonly bool IsWeaponVisible()
=> (_states & 0x08) == 0x08;
+ public readonly bool AreEarsVisible()
+ => (_states & 0x10) == 0x00;
+
public bool SetWeaponVisible(bool value)
{
if (value == IsWeaponVisible())
@@ -349,6 +354,15 @@ public unsafe struct DesignData
return true;
}
+ public bool SetEarsVisible(bool value)
+ {
+ if (value == AreEarsVisible())
+ return false;
+
+ _states = (byte)(value ? _states & ~0x10 : _states | 0x10);
+ return true;
+ }
+
public void SetDefaultEquipment(ItemManager items)
{
foreach (var slot in EquipSlotExtensions.EqdpSlots)
@@ -386,6 +400,7 @@ public unsafe struct DesignData
SetHatVisible(true);
SetWeaponVisible(true);
+ SetEarsVisible(true);
SetVisor(false);
fixed (uint* ptr = _itemIds)
{
diff --git a/Glamourer/Designs/MetaIndex.cs b/Glamourer/Designs/MetaIndex.cs
index 3fc4655..1842ae3 100644
--- a/Glamourer/Designs/MetaIndex.cs
+++ b/Glamourer/Designs/MetaIndex.cs
@@ -10,14 +10,15 @@ public enum MetaIndex
VisorState = StateIndex.MetaVisorState,
WeaponState = StateIndex.MetaWeaponState,
ModelId = StateIndex.MetaModelId,
+ EarState = StateIndex.MetaEarState,
}
public static class MetaExtensions
{
public static readonly IReadOnlyList AllRelevant =
- [MetaIndex.Wetness, MetaIndex.HatState, MetaIndex.VisorState, MetaIndex.WeaponState];
+ [MetaIndex.Wetness, MetaIndex.HatState, MetaIndex.VisorState, MetaIndex.WeaponState, MetaIndex.EarState];
- public const MetaFlag All = MetaFlag.Wetness | MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState;
+ public const MetaFlag All = MetaFlag.Wetness | MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState | MetaFlag.EarState;
public static MetaFlag ToFlag(this MetaIndex index)
=> index switch
@@ -26,6 +27,7 @@ public static class MetaExtensions
MetaIndex.HatState => MetaFlag.HatState,
MetaIndex.VisorState => MetaFlag.VisorState,
MetaIndex.WeaponState => MetaFlag.WeaponState,
+ MetaIndex.EarState => MetaFlag.EarState,
_ => (MetaFlag)byte.MaxValue,
};
@@ -36,7 +38,8 @@ public static class MetaExtensions
MetaFlag.HatState => MetaIndex.HatState,
MetaFlag.VisorState => MetaIndex.VisorState,
MetaFlag.WeaponState => MetaIndex.WeaponState,
- _ => (MetaIndex)byte.MaxValue,
+ MetaFlag.EarState => MetaIndex.EarState,
+ _ => (MetaIndex)byte.MaxValue,
};
public static IEnumerable ToIndices(this MetaFlag index)
@@ -49,6 +52,8 @@ public static class MetaExtensions
yield return MetaIndex.VisorState;
if (index.HasFlag(MetaFlag.WeaponState))
yield return MetaIndex.WeaponState;
+ if (index.HasFlag(MetaFlag.EarState))
+ yield return MetaIndex.EarState;
}
public static string ToName(this MetaIndex index)
@@ -58,6 +63,7 @@ public static class MetaExtensions
MetaIndex.VisorState => "Visor Toggled",
MetaIndex.WeaponState => "Weapon Visible",
MetaIndex.Wetness => "Force Wetness",
+ MetaIndex.EarState => "Ears Visible",
_ => "Unknown Meta",
};
@@ -68,6 +74,7 @@ public static class MetaExtensions
MetaIndex.VisorState => "Toggle the visor state of the characters head gear.",
MetaIndex.WeaponState => "Hide or show the characters weapons when not drawn.",
MetaIndex.Wetness => "Force the character to be wet or not.",
+ MetaIndex.EarState => "Hide or show the characters ears through the head gear. (Viera only)",
_ => string.Empty,
};
}
diff --git a/Glamourer/Events/PenumbraReloaded.cs b/Glamourer/Events/PenumbraReloaded.cs
index 20b58f9..0975670 100644
--- a/Glamourer/Events/PenumbraReloaded.cs
+++ b/Glamourer/Events/PenumbraReloaded.cs
@@ -15,5 +15,8 @@ public sealed class PenumbraReloaded()
///
VisorService = 0,
+
+ ///
+ VieraEarService = 0,
}
}
diff --git a/Glamourer/Events/VieraEarStateChanged.cs b/Glamourer/Events/VieraEarStateChanged.cs
new file mode 100644
index 0000000..65730b8
--- /dev/null
+++ b/Glamourer/Events/VieraEarStateChanged.cs
@@ -0,0 +1,22 @@
+using OtterGui.Classes;
+using Penumbra.GameData.Interop;
+
+namespace Glamourer.Events;
+
+///
+/// Triggered when the state of viera ear visibility for any draw object is changed.
+///
+/// - Parameter is the model with a changed viera ear visibility state.
+/// - Parameter is the new state.
+/// - Parameter is whether to call the original function.
+///
+///
+public sealed class VieraEarStateChanged()
+ : EventWrapperRef2(nameof(VieraEarStateChanged))
+{
+ public enum Priority
+ {
+ ///
+ StateListener = 0,
+ }
+}
diff --git a/Glamourer/Events/VisorStateChanged.cs b/Glamourer/Events/VisorStateChanged.cs
index d2d3a6c..03b7336 100644
--- a/Glamourer/Events/VisorStateChanged.cs
+++ b/Glamourer/Events/VisorStateChanged.cs
@@ -19,4 +19,4 @@ public sealed class VisorStateChanged()
///
StateListener = 0,
}
-}
+}
\ No newline at end of file
diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs
index dcd3a12..224154b 100644
--- a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs
+++ b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs
@@ -305,6 +305,12 @@ public class ActorPanel
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(MetaIndex.WeaponState, _stateManager, _state!));
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromState(CrestFlag.OffHand, _stateManager, _state!));
}
+
+ ImGui.SameLine();
+ using (_ = ImRaii.Group())
+ {
+ EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(MetaIndex.EarState, _stateManager, _state!));
+ }
}
private void DrawMonsterPanel()
diff --git a/Glamourer/Gui/Tabs/DebugTab/ActiveStatePanel.cs b/Glamourer/Gui/Tabs/DebugTab/ActiveStatePanel.cs
index e0ec4bd..35642a7 100644
--- a/Glamourer/Gui/Tabs/DebugTab/ActiveStatePanel.cs
+++ b/Glamourer/Gui/Tabs/DebugTab/ActiveStatePanel.cs
@@ -87,6 +87,9 @@ public class ActiveStatePanel(StateManager _stateManager, ActorObjectManager _ob
PrintRow("Visor Toggled", state.BaseData.IsVisorToggled(), state.ModelData.IsVisorToggled(),
state.Sources[MetaIndex.VisorState]);
ImGui.TableNextRow();
+ PrintRow("Viera Ears Visible", state.BaseData.AreEarsVisible(), state.ModelData.AreEarsVisible(),
+ state.Sources[MetaIndex.EarState]);
+ ImGui.TableNextRow();
PrintRow("Weapon Visible", state.BaseData.IsWeaponVisible(), state.ModelData.IsWeaponVisible(),
state.Sources[MetaIndex.WeaponState]);
ImGui.TableNextRow();
diff --git a/Glamourer/Gui/Tabs/DebugTab/DesignManagerPanel.cs b/Glamourer/Gui/Tabs/DebugTab/DesignManagerPanel.cs
index 38f0077..7c60dda 100644
--- a/Glamourer/Gui/Tabs/DebugTab/DesignManagerPanel.cs
+++ b/Glamourer/Gui/Tabs/DebugTab/DesignManagerPanel.cs
@@ -114,6 +114,7 @@ public class DesignManagerPanel(DesignManager _designManager, DesignFileSystem _
ImGuiUtil.DrawTableColumn(index.ToName());
ImGuiUtil.DrawTableColumn(design.DesignData.GetMeta(index).ToString());
ImGuiUtil.DrawTableColumn(design.DoApplyMeta(index) ? "Apply" : "Keep");
+ ImGui.TableNextRow();
}
ImGuiUtil.DrawTableColumn("Model ID");
diff --git a/Glamourer/Gui/Tabs/DebugTab/ModelEvaluationPanel.cs b/Glamourer/Gui/Tabs/DebugTab/ModelEvaluationPanel.cs
index d1b42e8..185e19b 100644
--- a/Glamourer/Gui/Tabs/DebugTab/ModelEvaluationPanel.cs
+++ b/Glamourer/Gui/Tabs/DebugTab/ModelEvaluationPanel.cs
@@ -17,6 +17,7 @@ namespace Glamourer.Gui.Tabs.DebugTab;
public unsafe class ModelEvaluationPanel(
ActorObjectManager _objectManager,
VisorService _visorService,
+ VieraEarService _vieraEarService,
UpdateSlotService _updateSlotService,
ChangeCustomizeService _changeCustomizeService,
CrestService _crestService,
@@ -84,6 +85,7 @@ public unsafe class ModelEvaluationPanel(
ImGuiUtil.CopyOnClickSelectable(offhand.ToString());
DrawVisor(actor, model);
+ DrawVieraEars(actor, model);
DrawHatState(actor, model);
DrawWeaponState(actor, model);
DrawWetness(actor, model);
@@ -135,6 +137,26 @@ public unsafe class ModelEvaluationPanel(
_visorService.SetVisorState(model, !VisorService.GetVisorState(model));
}
+ private void DrawVieraEars(Actor actor, Model model)
+ {
+ using var id = ImRaii.PushId("Viera Ears");
+ ImGuiUtil.DrawTableColumn("Viera Ears");
+ ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.ShowVieraEars.ToString() : "No Character");
+ ImGuiUtil.DrawTableColumn(model.IsHuman ? model.VieraEarsVisible.ToString() : "No Human");
+ ImGui.TableNextColumn();
+ if (!model.IsHuman)
+ return;
+
+ if (ImGui.SmallButton("Set True"))
+ _vieraEarService.SetVieraEarState(model, true);
+ ImGui.SameLine();
+ if (ImGui.SmallButton("Set False"))
+ _vieraEarService.SetVieraEarState(model, false);
+ ImGui.SameLine();
+ if (ImGui.SmallButton("Toggle"))
+ _vieraEarService.SetVieraEarState(model, !model.VieraEarsVisible);
+ }
+
private void DrawHatState(Actor actor, Model model)
{
using var id = ImRaii.PushId("HatState");
diff --git a/Glamourer/Interop/VieraEarService.cs b/Glamourer/Interop/VieraEarService.cs
new file mode 100644
index 0000000..1e5c4eb
--- /dev/null
+++ b/Glamourer/Interop/VieraEarService.cs
@@ -0,0 +1,82 @@
+using Dalamud.Hooking;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.Game.Character;
+using Glamourer.Events;
+using Penumbra.GameData.Interop;
+
+namespace Glamourer.Interop;
+
+public unsafe class VieraEarService : IDisposable
+{
+ private readonly PenumbraReloaded _penumbra;
+ private readonly IGameInteropProvider _interop;
+ public readonly VieraEarStateChanged Event;
+
+ public VieraEarService(VieraEarStateChanged visorStateChanged, IGameInteropProvider interop, PenumbraReloaded penumbra)
+ {
+ _interop = interop;
+ _penumbra = penumbra;
+ Event = visorStateChanged;
+ _setupVieraEarHook = Create();
+ _penumbra.Subscribe(Restore, PenumbraReloaded.Priority.VieraEarService);
+ }
+
+ public void Dispose()
+ {
+ _setupVieraEarHook.Dispose();
+ _penumbra.Unsubscribe(Restore);
+ }
+
+ /// Obtain the current state of viera ears for the given draw object (true: toggled).
+ public static unsafe bool GetVieraEarState(Model characterBase)
+ => characterBase is { IsCharacterBase: true, VieraEarsVisible: true };
+
+ /// Manually set the state of the Visor for the given draw object.
+ /// The draw object.
+ /// The desired state (true: toggled).
+ /// Whether the state was changed.
+ public bool SetVieraEarState(Model human, bool on)
+ {
+ if (!human.IsHuman)
+ return false;
+
+ var oldState = GetVieraEarState(human);
+ Glamourer.Log.Verbose($"[SetVieraEarState] Invoked manually on 0x{human.Address:X} switching from {oldState} to {on}.");
+ if (oldState == on)
+ return false;
+
+ human.VieraEarsVisible = on;
+ return true;
+ }
+
+ private delegate void UpdateVieraEarDelegateInternal(DrawDataContainer* drawData, byte on);
+
+ private Hook _setupVieraEarHook;
+
+ private void SetupVieraEarDetour(DrawDataContainer* drawData, byte value)
+ {
+ Actor actor = drawData->OwnerObject;
+ var originalOn = value != 0;
+ var on = originalOn;
+ // Invoke an event that can change the requested value
+ Event.Invoke(actor, ref on);
+
+ Glamourer.Log.Verbose(
+ $"[SetVieraEarState] Invoked from game on 0x{actor.Address:X} switching to {on} (original {originalOn} from {value}).");
+
+ _setupVieraEarHook.Original(drawData, on ? (byte)1 : (byte)0);
+ }
+
+ private unsafe Hook Create()
+ {
+ var hook = _interop.HookFromSignature("E8 ?? ?? ?? ?? 48 8D 8F ?? ?? ?? ?? 4C 8D 4C 24", SetupVieraEarDetour);
+ hook.Enable();
+ return hook;
+ }
+
+ private void Restore()
+ {
+ _setupVieraEarHook.Dispose();
+ _setupVieraEarHook = Create();
+ }
+}
diff --git a/Glamourer/Interop/VisorService.cs b/Glamourer/Interop/VisorService.cs
index 9763682..25823b6 100644
--- a/Glamourer/Interop/VisorService.cs
+++ b/Glamourer/Interop/VisorService.cs
@@ -9,9 +9,9 @@ namespace Glamourer.Interop;
public class VisorService : IDisposable
{
- private readonly PenumbraReloaded _penumbra;
- private readonly IGameInteropProvider _interop;
- public readonly VisorStateChanged Event;
+ private readonly PenumbraReloaded _penumbra;
+ private readonly IGameInteropProvider _interop;
+ public readonly VisorStateChanged Event;
public VisorService(VisorStateChanged visorStateChanged, IGameInteropProvider interop, PenumbraReloaded penumbra)
{
diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs
index 0754313..6cfb4b6 100644
--- a/Glamourer/Services/ServiceManager.cs
+++ b/Glamourer/Services/ServiceManager.cs
@@ -71,6 +71,7 @@ public static class StaticServiceManager
private static ServiceManager AddEvents(this ServiceManager services)
=> services.AddSingleton()
+ .AddSingleton()
.AddSingleton()
.AddSingleton()
.AddSingleton()
@@ -96,6 +97,7 @@ public static class StaticServiceManager
private static ServiceManager AddInterop(this ServiceManager services)
=> services.AddSingleton()
+ .AddSingleton()
.AddSingleton()
.AddSingleton()
.AddSingleton()
diff --git a/Glamourer/State/StateApplier.cs b/Glamourer/State/StateApplier.cs
index 93a3450..c346ab1 100644
--- a/Glamourer/State/StateApplier.cs
+++ b/Glamourer/State/StateApplier.cs
@@ -262,6 +262,14 @@ public class StateApplier(
_visor.SetVisorState(actor.Model, value);
return;
}
+ case MetaIndex.EarState:
+ foreach (var actor in data.Objects.Where(a => a.Model.IsHuman))
+ {
+ var model = actor.Model;
+ model.VieraEarsVisible = value;
+ }
+
+ return;
}
}
@@ -402,6 +410,7 @@ public class StateApplier(
ChangeMetaState(actors, MetaIndex.HatState, state.ModelData.IsHatVisible());
ChangeMetaState(actors, MetaIndex.WeaponState, state.ModelData.IsWeaponVisible());
ChangeMetaState(actors, MetaIndex.VisorState, state.ModelData.IsVisorToggled());
+ ChangeMetaState(actors, MetaIndex.EarState, state.ModelData.AreEarsVisible());
ChangeCrests(actors, state.ModelData.CrestVisibility);
ChangeParameters(actors, state.OnlyChangedParameters(), state.ModelData.Parameters);
ChangeMaterialValues(actors, state.Materials);
diff --git a/Glamourer/State/StateIndex.cs b/Glamourer/State/StateIndex.cs
index e3f1863..dff05a3 100644
--- a/Glamourer/State/StateIndex.cs
+++ b/Glamourer/State/StateIndex.cs
@@ -189,8 +189,9 @@ public readonly record struct StateIndex(int Value) : IEqualityOperators MetaFlag.HatState,
MetaVisorState => MetaFlag.VisorState,
MetaWeaponState => MetaFlag.WeaponState,
+ MetaEarState => MetaFlag.EarState,
MetaModelId => true,
CrestHead => CrestFlag.Head,
diff --git a/Glamourer/State/StateListener.cs b/Glamourer/State/StateListener.cs
index 90520b2..4b70718 100644
--- a/Glamourer/State/StateListener.cs
+++ b/Glamourer/State/StateListener.cs
@@ -9,6 +9,7 @@ using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Glamourer.GameData;
using Penumbra.GameData.DataContainers;
@@ -39,6 +40,7 @@ public class StateListener : IDisposable
private readonly WeaponLoading _weaponLoading;
private readonly HeadGearVisibilityChanged _headGearVisibility;
private readonly VisorStateChanged _visorState;
+ private readonly VieraEarStateChanged _vieraEarState;
private readonly WeaponVisibilityChanged _weaponVisibility;
private readonly StateFinalized _stateFinalized;
private readonly AutoDesignApplier _autoDesignApplier;
@@ -62,7 +64,7 @@ public class StateListener : IDisposable
WeaponVisibilityChanged weaponVisibility, HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier,
FunModule funModule, HumanModelList humans, StateApplier applier, MovedEquipment movedEquipment, ActorObjectManager objects,
GPoseService gPose, ChangeCustomizeService changeCustomizeService, CustomizeService customizations, ICondition condition,
- CrestService crestService, BonusSlotUpdating bonusSlotUpdating, StateFinalized stateFinalized)
+ CrestService crestService, BonusSlotUpdating bonusSlotUpdating, StateFinalized stateFinalized, VieraEarStateChanged vieraEarState)
{
_manager = manager;
_items = items;
@@ -88,6 +90,7 @@ public class StateListener : IDisposable
_crestService = crestService;
_bonusSlotUpdating = bonusSlotUpdating;
_stateFinalized = stateFinalized;
+ _vieraEarState = vieraEarState;
Subscribe();
}
@@ -266,7 +269,7 @@ public class StateListener : IDisposable
private void OnGearsetDataLoaded(Actor actor, Model model)
{
- if (!actor.Valid || (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart))
+ if (!actor.Valid || _condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart)
return;
// ensure actor and state are valid.
@@ -710,6 +713,44 @@ public class StateListener : IDisposable
}
}
+ /// Handle visor state changes made by the game.
+ private void OnVieraEarChange(Actor actor, ref bool value)
+ {
+ // Value is inverted compared to our own handling.
+
+ // Skip updates when in customize update.
+ if (ChangeCustomizeService.InUpdate.InMethod)
+ return;
+
+ if (!actor.IsCharacter)
+ return;
+
+ if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart)
+ return;
+
+ if (!actor.Identifier(_actors, out var identifier))
+ return;
+
+ if (!_manager.TryGetValue(identifier, out var state))
+ return;
+
+ // Update visor base state.
+ if (state.BaseData.SetEarsVisible(!value))
+ {
+ // if base state changed, either overwrite the actual value if we have fixed values,
+ // or overwrite the stored model state with the new one.
+ if (state.Sources[MetaIndex.EarState].IsFixed())
+ value = !state.ModelData.AreEarsVisible();
+ else
+ _manager.ChangeMetaState(state, MetaIndex.EarState, !value, ApplySettings.Game);
+ }
+ else
+ {
+ // if base state did not change, overwrite the value with the model state one.
+ value = !state.ModelData.AreEarsVisible();
+ }
+ }
+
/// Handle Hat Visibility changes. These act on the game object.
private void OnHeadGearVisibilityChange(Actor actor, ref bool value)
{
@@ -802,6 +843,7 @@ public class StateListener : IDisposable
_movedEquipment.Subscribe(OnMovedEquipment, MovedEquipment.Priority.StateListener);
_weaponLoading.Subscribe(OnWeaponLoading, WeaponLoading.Priority.StateListener);
_visorState.Subscribe(OnVisorChange, VisorStateChanged.Priority.StateListener);
+ _vieraEarState.Subscribe(OnVieraEarChange, VieraEarStateChanged.Priority.StateListener);
_headGearVisibility.Subscribe(OnHeadGearVisibilityChange, HeadGearVisibilityChanged.Priority.StateListener);
_weaponVisibility.Subscribe(OnWeaponVisibilityChange, WeaponVisibilityChanged.Priority.StateListener);
_changeCustomizeService.Subscribe(OnCustomizeChange, ChangeCustomizeService.Priority.StateListener);
@@ -820,6 +862,7 @@ public class StateListener : IDisposable
_movedEquipment.Unsubscribe(OnMovedEquipment);
_weaponLoading.Unsubscribe(OnWeaponLoading);
_visorState.Unsubscribe(OnVisorChange);
+ _vieraEarState.Unsubscribe(OnVieraEarChange);
_headGearVisibility.Unsubscribe(OnHeadGearVisibilityChange);
_weaponVisibility.Unsubscribe(OnWeaponVisibilityChange);
_changeCustomizeService.Unsubscribe(OnCustomizeChange);
diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs
index 98b12aa..e8926d6 100644
--- a/Glamourer/State/StateManager.cs
+++ b/Glamourer/State/StateManager.cs
@@ -158,6 +158,7 @@ public sealed class StateManager(
// Visor state is a flag on the game object, but we can see the actual state on the draw object.
ret.SetVisor(VisorService.GetVisorState(model));
+ ret.SetEarsVisible(model.VieraEarsVisible);
foreach (var slot in CrestExtensions.AllRelevantSet)
ret.SetCrest(slot, CrestService.GetModelCrest(actor, slot));
@@ -186,7 +187,7 @@ public sealed class StateManager(
off = actor.GetOffhand();
FistWeaponHack(ref ret, ref main, ref off);
ret.SetVisor(actor.AsCharacter->DrawData.IsVisorToggled);
-
+ ret.SetEarsVisible(actor.ShowVieraEars);
foreach (var slot in CrestExtensions.AllRelevantSet)
ret.SetCrest(slot, actor.GetCrest(slot));
diff --git a/Penumbra.GameData b/Penumbra.GameData
index 65c5bf3..17f2f49 160000
--- a/Penumbra.GameData
+++ b/Penumbra.GameData
@@ -1 +1 @@
-Subproject commit 65c5bf3f46569a54b0057c9015ab839b4e2a4350
+Subproject commit 17f2f496664b0d69ebd7fcdabe7bc8e3e20b6463