Add viera ear flags

This commit is contained in:
Ottermandias 2025-08-08 15:38:51 +02:00
parent 0f98fac157
commit 00d550f4fe
21 changed files with 245 additions and 21 deletions

@ -1 +1 @@
Subproject commit 1c517301c9fd0818e2c02e72304d6de121b9d703
Subproject commit 54c1944dc7db704733b4788520e494761bb0b58e

View file

@ -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;

View file

@ -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()

View file

@ -40,7 +40,8 @@ public class DesignBase
}
/// <summary> Used when importing .cma or .chara files. </summary>
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;
@ -255,6 +256,7 @@ public class DesignBase
}
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");
}
@ -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)

View file

@ -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)
{

View file

@ -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<MetaIndex> 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,6 +38,7 @@ public static class MetaExtensions
MetaFlag.HatState => MetaIndex.HatState,
MetaFlag.VisorState => MetaIndex.VisorState,
MetaFlag.WeaponState => MetaIndex.WeaponState,
MetaFlag.EarState => MetaIndex.EarState,
_ => (MetaIndex)byte.MaxValue,
};
@ -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,
};
}

View file

@ -15,5 +15,8 @@ public sealed class PenumbraReloaded()
/// <seealso cref="Interop.VisorService.Restore"/>
VisorService = 0,
/// <seealso cref="Interop.VieraEarService.Restore"/>
VieraEarService = 0,
}
}

View file

@ -0,0 +1,22 @@
using OtterGui.Classes;
using Penumbra.GameData.Interop;
namespace Glamourer.Events;
/// <summary>
/// Triggered when the state of viera ear visibility for any draw object is changed.
/// <list type="number">
/// <item>Parameter is the model with a changed viera ear visibility state. </item>
/// <item>Parameter is the new state. </item>
/// <item>Parameter is whether to call the original function. </item>
/// </list>
/// </summary>
public sealed class VieraEarStateChanged()
: EventWrapperRef2<Actor, bool, VieraEarStateChanged.Priority>(nameof(VieraEarStateChanged))
{
public enum Priority
{
/// <seealso cref="State.StateListener.OnVieraEarChange"/>
StateListener = 0,
}
}

View file

@ -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()

View file

@ -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();

View file

@ -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");

View file

@ -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");

View file

@ -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);
}
/// <summary> Obtain the current state of viera ears for the given draw object (true: toggled). </summary>
public static unsafe bool GetVieraEarState(Model characterBase)
=> characterBase is { IsCharacterBase: true, VieraEarsVisible: true };
/// <summary> Manually set the state of the Visor for the given draw object. </summary>
/// <param name="human"> The draw object. </param>
/// <param name="on"> The desired state (true: toggled). </param>
/// <returns> Whether the state was changed. </returns>
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<UpdateVieraEarDelegateInternal> _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<UpdateVieraEarDelegateInternal> Create()
{
var hook = _interop.HookFromSignature<UpdateVieraEarDelegateInternal>("E8 ?? ?? ?? ?? 48 8D 8F ?? ?? ?? ?? 4C 8D 4C 24", SetupVieraEarDetour);
hook.Enable();
return hook;
}
private void Restore()
{
_setupVieraEarHook.Dispose();
_setupVieraEarHook = Create();
}
}

View file

@ -71,6 +71,7 @@ public static class StaticServiceManager
private static ServiceManager AddEvents(this ServiceManager services)
=> services.AddSingleton<VisorStateChanged>()
.AddSingleton<VieraEarStateChanged>()
.AddSingleton<EquipSlotUpdating>()
.AddSingleton<DesignChanged>()
.AddSingleton<AutomationChanged>()
@ -96,6 +97,7 @@ public static class StaticServiceManager
private static ServiceManager AddInterop(this ServiceManager services)
=> services.AddSingleton<VisorService>()
.AddSingleton<VieraEarService>()
.AddSingleton<ChangeCustomizeService>()
.AddSingleton<MetaService>()
.AddSingleton<UpdateSlotService>()

View file

@ -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);

View file

@ -189,8 +189,9 @@ public readonly record struct StateIndex(int Value) : IEqualityOperators<StateIn
public const int MetaVisorState = MetaHatState + 1;
public const int MetaWeaponState = MetaVisorState + 1;
public const int MetaModelId = MetaWeaponState + 1;
public const int MetaEarState = MetaModelId + 1;
public const int CrestHead = MetaModelId + 1;
public const int CrestHead = MetaEarState + 1;
public const int CrestBody = CrestHead + 1;
public const int CrestOffhand = CrestBody + 1;
@ -300,6 +301,7 @@ public readonly record struct StateIndex(int Value) : IEqualityOperators<StateIn
MetaHatState => MetaFlag.HatState,
MetaVisorState => MetaFlag.VisorState,
MetaWeaponState => MetaFlag.WeaponState,
MetaEarState => MetaFlag.EarState,
MetaModelId => true,
CrestHead => CrestFlag.Head,

View file

@ -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
}
}
/// <summary> Handle visor state changes made by the game. </summary>
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();
}
}
/// <summary> Handle Hat Visibility changes. These act on the game object. </summary>
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);

View file

@ -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));

@ -1 +1 @@
Subproject commit 65c5bf3f46569a54b0057c9015ab839b4e2a4350
Subproject commit 17f2f496664b0d69ebd7fcdabe7bc8e3e20b6463