This commit is contained in:
Ottermandias 2023-06-21 18:09:50 +02:00
parent 5e6f797af4
commit 803fd1b247
16 changed files with 521 additions and 132 deletions

View file

@ -72,4 +72,22 @@ public static class EquipFlagExtensions
EquipSlot.LFinger => EquipFlag.LFingerStain,
_ => 0,
};
public static EquipFlag ToBothFlags(this EquipSlot slot)
=> slot switch
{
EquipSlot.MainHand => EquipFlag.Mainhand | EquipFlag.MainhandStain,
EquipSlot.OffHand => EquipFlag.Offhand | EquipFlag.OffhandStain,
EquipSlot.Head => EquipFlag.Head | EquipFlag.HeadStain,
EquipSlot.Body => EquipFlag.Body | EquipFlag.BodyStain,
EquipSlot.Hands => EquipFlag.Hands | EquipFlag.HandsStain,
EquipSlot.Legs => EquipFlag.Legs | EquipFlag.LegsStain,
EquipSlot.Feet => EquipFlag.Feet | EquipFlag.FeetStain,
EquipSlot.Ears => EquipFlag.Ears | EquipFlag.EarsStain,
EquipSlot.Neck => EquipFlag.Neck | EquipFlag.NeckStain,
EquipSlot.Wrists => EquipFlag.Wrist | EquipFlag.WristStain,
EquipSlot.RFinger => EquipFlag.RFinger | EquipFlag.RFingerStain,
EquipSlot.LFinger => EquipFlag.LFinger | EquipFlag.LFingerStain,
_ => 0,
};
}

View file

@ -20,6 +20,7 @@ public class Configuration : IPluginConfiguration, ISavable
public bool Enabled { get; set; } = true;
public bool UseRestrictedGearProtection { get; set; } = true;
public bool OpenFoldersByDefault { get; set; } = false;
public bool AutoRedrawEquipOnChanges { get; set; } = false;
public MainWindow.TabType SelectedTab { get; set; } = MainWindow.TabType.Settings;
public DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift);

View file

@ -37,7 +37,7 @@ public class Design : ISavable
/// <summary> Unconditionally apply a design to a designdata. </summary>
/// <returns>Whether a redraw is required for the changes to take effect.</returns>
public bool ApplyDesign(ref DesignData data)
public (bool, CustomizeFlag, EquipFlag) ApplyDesign(ref DesignData data)
{
var modelChanged = data.ModelId != DesignData.ModelId;
data.ModelId = DesignData.ModelId;
@ -52,13 +52,16 @@ public class Design : ISavable
customizeFlags |= index.ToFlag();
}
EquipFlag equipFlags = 0;
foreach (var slot in EquipSlotExtensions.EqdpSlots.Append(EquipSlot.MainHand).Append(EquipSlot.OffHand))
{
if (DoApplyEquip(slot))
data.SetItem(slot, DesignData.Item(slot));
if (data.SetItem(slot, DesignData.Item(slot)))
equipFlags |= slot.ToFlag();
if (DoApplyStain(slot))
data.SetStain(slot, DesignData.Stain(slot));
if (data.SetStain(slot, DesignData.Stain(slot)))
equipFlags |= slot.ToStainFlag();
}
if (DoApplyHatVisible())
@ -72,7 +75,7 @@ public class Design : ISavable
if (DoApplyWetness())
data.SetIsWet(DesignData.IsWet());
return modelChanged || customizeFlags.RequiresRedraw();
return (modelChanged, customizeFlags, equipFlags);
}
#endregion

View file

@ -63,6 +63,24 @@ public unsafe struct DesignData
// @formatter:on
};
public readonly CharacterArmor Armor(EquipSlot slot)
{
fixed (byte* ptr = _equipmentBytes)
{
var armorPtr = (CharacterArmor*)ptr;
return armorPtr[slot.ToIndex()];
}
}
public readonly CharacterWeapon Weapon(EquipSlot slot)
{
fixed (byte* ptr = _equipmentBytes)
{
var armorPtr = (CharacterArmor*)ptr;
return armorPtr[slot is EquipSlot.MainHand ? 10 : 11].ToWeapon(_secondaryMainhand);
}
}
public bool SetItem(EquipSlot slot, EquipItem item)
{
var index = slot.ToIndex();

View file

@ -3,11 +3,13 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Interface;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Interop;
using Glamourer.Interop.Penumbra;
using Glamourer.Interop.Structs;
@ -854,11 +856,81 @@ public unsafe class DebugTab : ITab
}
}
public void DrawState(ActorData data, ActorState state)
{
using var table = ImRaii.Table("##state", 7, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
ImGuiUtil.DrawTableColumn("Name");
ImGuiUtil.DrawTableColumn(state.Identifier.ToString());
ImGui.TableNextColumn();
if (ImGui.Button("Reset"))
_state.ResetState(state);
ImGui.TableNextRow();
static void PrintRow<T>(string label, T actor, T model, StateChanged.Source source) where T : notnull
{
ImGuiUtil.DrawTableColumn(label);
ImGuiUtil.DrawTableColumn(actor.ToString()!);
ImGuiUtil.DrawTableColumn(model.ToString()!);
ImGuiUtil.DrawTableColumn(source.ToString());
}
static string ItemString(in DesignData data, EquipSlot slot)
{
var item = data.Item(slot);
return $"{item.Name} ({item.ModelId.Value}{(item.WeaponType != 0 ? $"-{item.WeaponType.Value}" : string.Empty)}-{item.Variant})";
}
PrintRow("Model ID", state.BaseData.ModelId, state.ModelData.ModelId, state[ActorState.MetaFlag.ModelId]);
ImGui.TableNextRow();
PrintRow("Wetness", state.BaseData.IsWet(), state.ModelData.IsWet(), state[ActorState.MetaFlag.Wetness]);
ImGui.TableNextRow();
if (state.BaseData.ModelId == 0 && state.ModelData.ModelId == 0)
{
PrintRow("Hat Visible", state.BaseData.IsHatVisible(), state.ModelData.IsHatVisible(), state[ActorState.MetaFlag.HatState]);
ImGui.TableNextRow();
PrintRow("Visor Toggled", state.BaseData.IsVisorToggled(), state.ModelData.IsVisorToggled(),
state[ActorState.MetaFlag.VisorState]);
ImGui.TableNextRow();
PrintRow("Weapon Visible", state.BaseData.IsWeaponVisible(), state.ModelData.IsWeaponVisible(),
state[ActorState.MetaFlag.WeaponState]);
ImGui.TableNextRow();
foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand))
{
PrintRow(slot.ToName(), ItemString(state.BaseData, slot), ItemString(state.ModelData, slot), state[slot, false]);
ImGuiUtil.DrawTableColumn(state.BaseData.Stain(slot).Value.ToString());
ImGuiUtil.DrawTableColumn(state.ModelData.Stain(slot).Value.ToString());
ImGuiUtil.DrawTableColumn(state[slot, true].ToString());
}
foreach (var type in Enum.GetValues<CustomizeIndex>())
{
PrintRow(type.ToDefaultName(), state.BaseData.Customize[type].Value, state.ModelData.Customize[type].Value, state[type]);
ImGui.TableNextRow();
}
}
else
{
ImGuiUtil.DrawTableColumn(string.Join(" ", state.BaseData.GetCustomizeBytes().Select(b => b.ToString("X2"))));
ImGuiUtil.DrawTableColumn(string.Join(" ", state.ModelData.GetCustomizeBytes().Select(b => b.ToString("X2"))));
ImGui.TableNextRow();
ImGuiUtil.DrawTableColumn(string.Join(" ", state.BaseData.GetEquipmentBytes().Select(b => b.ToString("X2"))));
ImGuiUtil.DrawTableColumn(string.Join(" ", state.ModelData.GetEquipmentBytes().Select(b => b.ToString("X2"))));
}
}
public static void DrawDesignData(in DesignData data)
{
if (data.ModelId == 0)
{
using var table = ImRaii.Table("##equip", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand))
{
var item = data.Item(slot);
@ -1011,7 +1083,7 @@ public unsafe class DebugTab : ITab
continue;
if (_state.GetOrCreate(identifier, actors.Objects[0], out var state))
DrawDesignData(state.ModelData);
DrawState(actors, state);
else
ImGui.TextUnformatted("Invalid actor.");
}
@ -1026,8 +1098,10 @@ public unsafe class DebugTab : ITab
foreach (var (identifier, state) in _state.Where(kvp => !_objectManager.ContainsKey(kvp.Key)))
{
using var t = ImRaii.TreeNode(identifier.ToString());
if (t)
DrawDesignData(state.ModelData);
if (!t)
return;
DrawState(ActorData.Invalid, state);
}
}

View file

@ -1,21 +1,44 @@
using System.Numerics;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Gui.Customization;
using Glamourer.Interop;
using Glamourer.Interop.Penumbra;
using Glamourer.State;
using Glamourer.Structs;
using ImGuiNET;
using OtterGui.Raii;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.GameData.Enums;
namespace Glamourer.Gui.Tabs.DesignTab;
public class DesignPanel
{
private readonly ObjectManager _objects;
private readonly DesignFileSystemSelector _selector;
private readonly DesignManager _manager;
private readonly CustomizationDrawer _customizationDrawer;
private readonly StateManager _state;
private readonly PenumbraService _penumbra;
private readonly UpdateSlotService _updateSlot;
private readonly WeaponService _weaponService;
private readonly ChangeCustomizeService _changeCustomizeService;
public DesignPanel(DesignFileSystemSelector selector, CustomizationDrawer customizationDrawer, DesignManager manager)
public DesignPanel(DesignFileSystemSelector selector, CustomizationDrawer customizationDrawer, DesignManager manager, ObjectManager objects,
StateManager state, PenumbraService penumbra, ChangeCustomizeService changeCustomizeService, WeaponService weaponService,
UpdateSlotService updateSlot)
{
_selector = selector;
_customizationDrawer = customizationDrawer;
_manager = manager;
_objects = objects;
_state = state;
_penumbra = penumbra;
_changeCustomizeService = changeCustomizeService;
_weaponService = weaponService;
_updateSlot = updateSlot;
}
public void Draw()
@ -28,6 +51,14 @@ public class DesignPanel
if (!child)
return;
if (ImGui.Button("TEST"))
{
var (id, data) = _objects.PlayerData;
if (data.Valid && _state.GetOrCreate(id, data.Objects[0], out var state))
_state.ApplyDesign(design, state);
}
_customizationDrawer.Draw(design.DesignData.Customize, design.WriteProtected());
}
}

View file

@ -2,6 +2,7 @@
using System.Runtime.CompilerServices;
using Dalamud.Interface;
using Glamourer.Gui.Tabs.DesignTab;
using Glamourer.Interop.Penumbra;
using Glamourer.State;
using ImGuiNET;
using OtterGui;
@ -15,12 +16,14 @@ public class SettingsTab : ITab
private readonly Configuration _config;
private readonly DesignFileSystemSelector _selector;
private readonly StateListener _stateListener;
private readonly PenumbraAutoRedraw _autoRedraw;
public SettingsTab(Configuration config, DesignFileSystemSelector selector, StateListener stateListener)
public SettingsTab(Configuration config, DesignFileSystemSelector selector, StateListener stateListener, PenumbraAutoRedraw autoRedraw)
{
_config = config;
_selector = selector;
_stateListener = stateListener;
_autoRedraw = autoRedraw;
}
public ReadOnlySpan<byte> Label
@ -31,10 +34,14 @@ public class SettingsTab : ITab
using var child = ImRaii.Child("MainWindowChild");
if (!child)
return;
Checkbox("Enabled", "Enable main functionality of keeping and applying state.", _stateListener.Enabled, _stateListener.Enable);
Checkbox("Restricted Gear Protection",
"Use gender- and race-appropriate models when detecting certain items not available for a characters current gender and race.",
_config.UseRestrictedGearProtection, v => _config.UseRestrictedGearProtection = v);
Checkbox("Auto-Reload Gear",
"Automatically reload equipment pieces on your own character when changing any mod options in Penumbra in their associated collection.",
_config.AutoRedrawEquipOnChanges, _autoRedraw.SetState);
if (Widget.DoubleModifierSelector("Design Deletion Modifier",
"A modifier you need to hold while clicking the Delete Design button for it to take effect.", 100 * ImGuiHelpers.GlobalScale,
_config.DeleteDesignModifier, v => _config.DeleteDesignModifier = v))

View file

@ -0,0 +1,67 @@
using System;
using Glamourer.State;
using Penumbra.Api.Enums;
namespace Glamourer.Interop.Penumbra;
public class PenumbraAutoRedraw : IDisposable
{
private readonly Configuration _config;
private readonly PenumbraService _penumbra;
private readonly StateManager _state;
private readonly ObjectManager _objects;
private bool _enabled;
public PenumbraAutoRedraw(PenumbraService penumbra, Configuration config, StateManager state, ObjectManager objects)
{
_penumbra = penumbra;
_config = config;
_state = state;
_objects = objects;
if (_config.AutoRedrawEquipOnChanges)
Enable();
}
public void SetState(bool value)
{
if (value == _config.AutoRedrawEquipOnChanges)
return;
_config.AutoRedrawEquipOnChanges = value;
_config.Save();
if (value)
Enable();
else
Disable();
}
public void Enable()
{
if (_enabled)
return;
_penumbra.ModSettingChanged += OnModSettingChange;
_enabled = true;
}
public void Disable()
{
if (!_enabled)
return;
_penumbra.ModSettingChanged -= OnModSettingChange;
_enabled = false;
}
public void Dispose()
{
Disable();
}
private void OnModSettingChange(ModSettingChange type, string name, string mod, bool inherited)
{
var playerName = _penumbra.GetCurrentPlayerCollection();
if (playerName == name)
_state.ReapplyState(_objects.Player);
}
}

View file

@ -18,9 +18,11 @@ public unsafe class PenumbraService : IDisposable
private readonly EventSubscriber<MouseButton, ChangedItemType, uint> _clickSubscriber;
private readonly EventSubscriber<nint, string, nint, nint, nint> _creatingCharacterBase;
private readonly EventSubscriber<nint, string, nint> _createdCharacterBase;
private readonly EventSubscriber<ModSettingChange, string, string, bool> _modSettingChanged;
private ActionSubscriber<int, RedrawType> _redrawSubscriber;
private FuncSubscriber<nint, (nint, string)> _drawObjectInfo;
private FuncSubscriber<int, int> _cutsceneParent;
private FuncSubscriber<int, (bool, bool, string)> _objectCollection;
private readonly EventSubscriber _initializedEvent;
private readonly EventSubscriber _disposedEvent;
@ -35,6 +37,7 @@ public unsafe class PenumbraService : IDisposable
_clickSubscriber = Ipc.ChangedItemClick.Subscriber(pi);
_createdCharacterBase = Ipc.CreatedCharacterBase.Subscriber(pi);
_creatingCharacterBase = Ipc.CreatingCharacterBase.Subscriber(pi);
_modSettingChanged = Ipc.ModSettingChanged.Subscriber(pi);
Reattach();
}
@ -63,6 +66,22 @@ public unsafe class PenumbraService : IDisposable
remove => _createdCharacterBase.Event -= value;
}
public event Action<ModSettingChange, string, string, bool> ModSettingChanged
{
add => _modSettingChanged.Event += value;
remove => _modSettingChanged.Event -= value;
}
/// <summary> Obtain the name of the collection currently assigned to the player. </summary>
public string GetCurrentPlayerCollection()
{
if (!Available)
return string.Empty;
var (valid, _, name) = _objectCollection.Invoke(0);
return valid ? name : string.Empty;
}
/// <summary> Obtain the game object corresponding to a draw object. </summary>
public Actor GameObjectFromDrawObject(Model drawObject)
=> Available ? _drawObjectInfo.Invoke(drawObject.Address).Item1 : Actor.Null;
@ -103,9 +122,11 @@ public unsafe class PenumbraService : IDisposable
_clickSubscriber.Enable();
_creatingCharacterBase.Enable();
_createdCharacterBase.Enable();
_modSettingChanged.Enable();
_drawObjectInfo = Ipc.GetDrawObjectInfo.Subscriber(_pluginInterface);
_cutsceneParent = Ipc.GetCutsceneParentIndex.Subscriber(_pluginInterface);
_redrawSubscriber = Ipc.RedrawObjectByIndex.Subscriber(_pluginInterface);
_objectCollection = Ipc.GetCollectionForObject.Subscriber(_pluginInterface);
Available = true;
Glamourer.Log.Debug("Glamourer attached to Penumbra.");
}
@ -122,6 +143,7 @@ public unsafe class PenumbraService : IDisposable
_clickSubscriber.Disable();
_creatingCharacterBase.Disable();
_createdCharacterBase.Disable();
_modSettingChanged.Disable();
if (Available)
{
Available = false;
@ -138,5 +160,6 @@ public unsafe class PenumbraService : IDisposable
_createdCharacterBase.Dispose();
_initializedEvent.Dispose();
_disposedEvent.Dispose();
_modSettingChanged.Dispose();
}
}

View file

@ -1,4 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using OtterGui.Log;
namespace Glamourer.Interop.Structs;
@ -26,4 +28,12 @@ public readonly struct ActorData
Objects = new List<Actor>(0);
Label = string.Empty;
}
public LazyString ToLazyString(string invalid)
{
var objects = Objects;
return Valid
? new LazyString(() => string.Join(", ", objects.Select(o => o.ToString())))
: new LazyString(() => invalid);
}
}

View file

@ -5,6 +5,7 @@ using Glamourer.Events;
using Glamourer.Interop.Structs;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace Glamourer.Interop;
@ -31,24 +32,18 @@ public unsafe class UpdateSlotService : IDisposable
{
if (!drawObject.IsCharacterBase)
return;
FlagSlotForUpdateInterop(drawObject, slot, data);
}
public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor data)
{
if (!drawObject.IsCharacterBase)
return;
public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor armor, StainId stain)
=> UpdateSlot(drawObject, slot, armor.With(stain));
FlagSlotForUpdateInterop(drawObject, slot, data.With(drawObject.GetArmor(slot).Stain));
}
public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor armor)
=> UpdateArmor(drawObject, slot, armor, drawObject.GetArmor(slot).Stain);
public void UpdateStain(Model drawObject, EquipSlot slot, StainId stain)
{
if (!drawObject.IsHuman)
return;
FlagSlotForUpdateInterop(drawObject, slot, drawObject.GetArmor(slot).With(stain));
}
=> UpdateArmor(drawObject, slot, drawObject.GetArmor(slot), stain);
private ulong FlagSlotForUpdateDetour(nint drawObject, uint slotIdx, CharacterArmor* data)
{

View file

@ -69,7 +69,8 @@ public static class ServiceManager
.AddSingleton<UpdateSlotService>()
.AddSingleton<WeaponService>()
.AddSingleton<PenumbraService>()
.AddSingleton<ObjectManager>();
.AddSingleton<ObjectManager>()
.AddSingleton<PenumbraAutoRedraw>();
private static IServiceCollection AddDesigns(this IServiceCollection services)
=> services.AddSingleton<DesignManager>()
@ -77,6 +78,7 @@ public static class ServiceManager
private static IServiceCollection AddState(this IServiceCollection services)
=> services.AddSingleton<StateManager>()
.AddSingleton<StateEditor>()
.AddSingleton<StateListener>();
private static IServiceCollection AddUi(this IServiceCollection services)

View file

@ -17,14 +17,20 @@ public class ActorState
HatState,
VisorState,
WeaponState,
ModelId,
}
public ActorIdentifier Identifier { get; internal init; }
public DesignData ActorData;
/// <summary> This should always represent the unmodified state of the draw object. </summary>
public DesignData BaseData;
/// <summary> This should be the desired state of the draw object. </summary>
public DesignData ModelData;
/// <summary> This contains whether a change to the base data was made by the game, the user via manual input or through automatic application. </summary>
private readonly StateChanged.Source[] _sources = Enumerable
.Repeat(StateChanged.Source.Game, EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices + 4).ToArray();
.Repeat(StateChanged.Source.Game, EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices + 5).ToArray();
internal ActorState(ActorIdentifier identifier)
=> Identifier = identifier;

View file

@ -26,6 +26,7 @@ public class StateEditor
_items = items;
}
public void ChangeCustomize(ActorData data, Customize customize)
{
foreach (var actor in data.Objects)
@ -43,19 +44,15 @@ public class StateEditor
}
}
public void ChangeArmor(ActorData data, EquipSlot slot, EquipItem item)
public void ChangeArmor(ActorState state, ActorData data, EquipSlot slot)
{
var idx = slot.ToIndex();
if (idx >= 10)
return;
var armor = item.Armor();
var armor = state.ModelData.Armor(slot);
foreach (var actor in data.Objects.Where(a => a.IsCharacter))
{
var mdl = actor.Model;
var customize = mdl.IsHuman ? mdl.GetCustomize() : actor.GetCustomize();
var (_, resolvedItem) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender);
_updateSlot.UpdateArmor(actor.Model, slot, resolvedItem);
_updateSlot.UpdateSlot(actor.Model, slot, resolvedItem);
}
}

View file

@ -61,42 +61,46 @@ public class StateListener : IDisposable
Unsubscribe();
}
private enum UpdateState
{
NoChange,
Transformed,
Change,
}
private unsafe void OnCreatingCharacterBase(nint actorPtr, string _, nint modelPtr, nint customizePtr, nint equipDataPtr)
{
// TODO: Fixed Designs.
var actor = (Actor)actorPtr;
var identifier = actor.GetIdentifier(_actors.AwaitedService);
if (*(int*)modelPtr != actor.AsCharacter->ModelCharaId)
return;
var modelId = *(uint*)modelPtr;
ref var customize = ref *(Customize*)customizePtr;
if (_manager.TryGetValue(identifier, out var state))
switch (UpdateBaseData(actor, state, modelId, customizePtr, equipDataPtr))
{
ApplyCustomize(actor, state, ref customize);
ApplyEquipment(actor, state, (CharacterArmor*)equipDataPtr);
if (_config.UseRestrictedGearProtection)
ProtectRestrictedGear(equipDataPtr, customize.Race, customize.Gender);
case UpdateState.Change: break;
case UpdateState.Transformed: break;
case UpdateState.NoChange:
UpdateBaseData(actor, state, customize);
break;
}
else if (_config.UseRestrictedGearProtection && *(uint*)modelPtr == 0)
{
if (_config.UseRestrictedGearProtection && modelId == 0)
ProtectRestrictedGear(equipDataPtr, customize.Race, customize.Gender);
}
}
private void OnSlotUpdating(Model model, EquipSlot slot, Ref<CharacterArmor> armor, Ref<ulong> returnValue)
{
// TODO handle hat state
// TODO handle fixed designs
var actor = _penumbra.GameObjectFromDrawObject(model);
var customize = model.GetCustomize();
if (actor.Identifier(_actors.AwaitedService, out var identifier)
&& _manager.TryGetValue(identifier, out var state))
ApplyEquipmentPiece(actor, state, slot, ref armor.Value);
var (replaced, replacedArmor) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender);
if (replaced)
armor.Assign(replacedArmor);
if (_config.UseRestrictedGearProtection)
(_, armor.Value) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender);
}
private void OnWeaponLoading(Actor actor, EquipSlot slot, Ref<CharacterWeapon> weapon)
@ -111,7 +115,7 @@ public class StateListener : IDisposable
|| actorWeapon.Type.Value != stateItem.WeaponType
|| actorWeapon.Variant != stateItem.Variant)
{
var oldActorItem = state.ActorData.Item(slot);
var oldActorItem = state.BaseData.Item(slot);
if (oldActorItem.ModelId.Value == actorWeapon.Set.Value
&& oldActorItem.WeaponType.Value == actorWeapon.Type.Value
&& oldActorItem.Variant == actorWeapon.Variant)
@ -123,8 +127,8 @@ public class StateListener : IDisposable
else
{
var identified = _items.Identify(slot, actorWeapon.Set, actorWeapon.Type, (byte)actorWeapon.Variant,
slot == EquipSlot.OffHand ? state.ActorData.Item(EquipSlot.MainHand).Type : FullEquipType.Unknown);
state.ActorData.SetItem(slot, identified);
slot == EquipSlot.OffHand ? state.BaseData.Item(EquipSlot.MainHand).Type : FullEquipType.Unknown);
state.BaseData.SetItem(slot, identified);
if (state[slot, false] is not StateChanged.Source.Fixed)
{
state.ModelData.SetItem(slot, identified);
@ -142,7 +146,7 @@ public class StateListener : IDisposable
var stateStain = state.ModelData.Stain(slot);
if (actorWeapon.Stain.Value != stateStain.Value)
{
var oldActorStain = state.ActorData.Stain(slot);
var oldActorStain = state.BaseData.Stain(slot);
if (state[slot, true] is not StateChanged.Source.Fixed)
{
state.ModelData.SetStain(slot, actorWeapon.Stain);
@ -159,7 +163,7 @@ public class StateListener : IDisposable
private void ApplyCustomize(Actor actor, ActorState state, ref Customize customize)
{
var actorCustomize = actor.GetCustomize();
ref var oldActorCustomize = ref state.ActorData.Customize;
ref var oldActorCustomize = ref state.BaseData.Customize;
ref var stateCustomize = ref state.ModelData.Customize;
foreach (var idx in Enum.GetValues<CustomizeIndex>())
{
@ -201,57 +205,34 @@ public class StateListener : IDisposable
private void ApplyEquipmentPiece(Actor actor, ActorState state, EquipSlot slot, ref CharacterArmor armor)
{
var actorArmor = actor.GetArmor(slot);
if (armor.Value != actorArmor.Value)
var changeState = UpdateBaseData(actor, state, slot, armor);
if (changeState is UpdateState.Transformed)
return;
var stateArmor = state.ModelData.Item(slot);
if (armor.Set.Value != stateArmor.ModelId.Value || armor.Variant != stateArmor.Variant)
if (changeState is UpdateState.NoChange)
{
var oldActorArmor = state.ActorData.Item(slot);
if (oldActorArmor.ModelId.Value == actorArmor.Set.Value && oldActorArmor.Variant == actorArmor.Variant)
{
armor.Set = stateArmor.ModelId;
armor.Variant = stateArmor.Variant;
armor = state.ModelData.Armor(slot);
}
else
{
var identified = _items.Identify(slot, actorArmor.Set, actorArmor.Variant);
state.ActorData.SetItem(slot, identified);
if (state[slot, false] is not StateChanged.Source.Fixed)
var modelArmor = state.ModelData.Armor(slot);
if (armor.Value == modelArmor.Value)
return;
if (state[slot, false] is StateChanged.Source.Fixed)
{
state.ModelData.SetItem(slot, identified);
state[slot, false] = StateChanged.Source.Game;
armor.Set = modelArmor.Set;
armor.Variant = modelArmor.Variant;
}
else
{
armor.Set = stateArmor.ModelId;
armor.Variant = stateArmor.Variant;
}
}
_manager.ChangeEquip(state, slot, state.BaseData.Item(slot), StateChanged.Source.Game);
}
var stateStain = state.ModelData.Stain(slot);
if (armor.Stain.Value != stateStain.Value)
{
var oldActorStain = state.ActorData.Stain(slot);
if (oldActorStain.Value == actorArmor.Stain.Value)
{
armor.Stain = stateStain;
}
if (state[slot, true] is StateChanged.Source.Fixed)
armor.Stain = modelArmor.Stain;
else
{
state.ActorData.SetStain(slot, actorArmor.Stain);
if (state[slot, true] is not StateChanged.Source.Fixed)
{
state.ModelData.SetStain(slot, actorArmor.Stain);
state[slot, true] = StateChanged.Source.Game;
}
else
{
armor.Stain = stateStain;
}
}
_manager.ChangeStain(state, slot, state.BaseData.Stain(slot), StateChanged.Source.Game);
}
}
@ -280,4 +261,81 @@ public class StateListener : IDisposable
_slotUpdating.Unsubscribe(OnSlotUpdating);
_weaponLoading.Unsubscribe(OnWeaponLoading);
}
private UpdateState UpdateBaseData(Actor actor, ActorState state, EquipSlot slot, CharacterArmor armor)
{
var actorArmor = actor.GetArmor(slot);
// The actor armor does not correspond to the model armor, thus the actor is transformed.
if (actorArmor.Value != armor.Value)
return UpdateState.Transformed;
// TODO: Hat State.
var baseData = state.BaseData.Armor(slot);
var change = UpdateState.NoChange;
if (baseData.Stain != armor.Stain)
{
state.BaseData.SetStain(slot, armor.Stain);
change = UpdateState.Change;
}
if (baseData.Set.Value != armor.Set.Value || baseData.Variant != armor.Variant)
{
var item = _items.Identify(slot, armor.Set, armor.Variant);
state.BaseData.SetItem(slot, item);
change = UpdateState.Change;
}
return change;
}
private UpdateState UpdateBaseData(Actor actor, ActorState state, EquipSlot slot, CharacterWeapon weapon)
{
var baseData = state.BaseData.Weapon(slot);
var change = UpdateState.NoChange;
if (baseData.Stain != weapon.Stain)
{
state.BaseData.SetStain(slot, weapon.Stain);
change = UpdateState.Change;
}
if (baseData.Set.Value != weapon.Set.Value || baseData.Type.Value != weapon.Type.Value || baseData.Variant != weapon.Variant)
{
var item = _items.Identify(slot, weapon.Set, weapon.Type, (byte)weapon.Variant,
slot is EquipSlot.OffHand ? state.BaseData.Item(EquipSlot.MainHand).Type : FullEquipType.Unknown);
state.BaseData.SetItem(slot, item);
change = UpdateState.Change;
}
return change;
}
private unsafe UpdateState UpdateBaseData(Actor actor, ActorState state, uint modelId, nint customizeData, nint equipData)
{
if (modelId != (uint)actor.AsCharacter->CharacterData.ModelCharaId)
return UpdateState.Transformed;
if (modelId == state.BaseData.ModelId)
return UpdateState.NoChange;
if (modelId == 0)
state.BaseData.LoadNonHuman(modelId, *(Customize*)customizeData, (byte*)equipData);
else
state.BaseData = _manager.FromActor(actor);
return UpdateState.Change;
}
private UpdateState UpdateBaseData(Actor actor, ActorState state, Customize customize)
{
if (!actor.GetCustomize().Equals(customize))
return UpdateState.Transformed;
if (state.BaseData.Customize.Equals(customize))
return UpdateState.NoChange;
state.BaseData.Customize.Load(customize);
return UpdateState.Change;
}
}

View file

@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Security.Cryptography;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Events;
@ -11,6 +12,7 @@ using Glamourer.Interop.Penumbra;
using Glamourer.Interop.Structs;
using Glamourer.Services;
using Glamourer.Structs;
using OtterGui.Log;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -24,13 +26,15 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
private readonly CustomizationService _customizations;
private readonly VisorService _visor;
private readonly StateChanged _event;
private readonly ObjectManager _objects;
private readonly StateEditor _editor;
private readonly PenumbraService _penumbra;
private readonly Dictionary<ActorIdentifier, ActorState> _states = new();
public StateManager(ActorService actors, ItemManager items, CustomizationService customizations, VisorService visor, StateChanged @event,
PenumbraService penumbra)
PenumbraService penumbra, ObjectManager objects, StateEditor editor)
{
_actors = actors;
_items = items;
@ -38,7 +42,8 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
_visor = visor;
_event = @event;
_penumbra = penumbra;
_objects = objects;
_editor = editor;
}
public bool GetOrCreate(Actor actor, [NotNullWhen(true)] out ActorState? state)
@ -55,7 +60,7 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
state = new ActorState(identifier)
{
ModelData = designData,
ActorData = designData
BaseData = designData,
};
_states.Add(identifier, state);
return true;
@ -165,9 +170,9 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
return ret;
}
if (actor.AsCharacter->ModelCharaId != 0)
if (actor.AsCharacter->CharacterData.ModelCharaId != 0)
{
ret.LoadNonHuman((uint)actor.AsCharacter->ModelCharaId, *(Customize*)&actor.AsCharacter->DrawData.CustomizeData,
ret.LoadNonHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId, *(Customize*)&actor.AsCharacter->DrawData.CustomizeData,
(byte*)&actor.AsCharacter->DrawData.Head);
return ret;
}
@ -242,6 +247,94 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
$"Changed customize {idx.ToDefaultName()} for {state.Identifier} ({string.Join(", ", data.Objects.Select(o => $"0x{o.Address}"))}) from {oldValue.Value} to {value.Value}.");
_event.Invoke(StateChanged.Type.Customize, source, state, data, (oldValue, value, idx));
}
public void ApplyDesign(Design design, ActorState state)
{
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
switch (design.DoApplyEquip(slot), design.DoApplyStain(slot))
{
case (false, false): continue;
case (true, false):
ChangeEquip(state, slot, design.DesignData.Item(slot), StateChanged.Source.Manual);
break;
case (false, true):
ChangeStain(state, slot, design.DesignData.Stain(slot), StateChanged.Source.Manual);
break;
case (true, true):
ChangeEquip(state, slot, design.DesignData.Item(slot), design.DesignData.Stain(slot), StateChanged.Source.Manual);
break;
}
}
}
public void ResetState(ActorState state)
{
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
ChangeEquip(state, slot, state.BaseData.Item(slot), state.BaseData.Stain(slot), StateChanged.Source.Game);
_editor.ChangeArmor(state, objects, slot);
}
}
public void ReapplyState(Actor actor)
{
if (!GetOrCreate(actor, out var state))
return;
_objects.Update();
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
foreach (var slot in EquipSlotExtensions.EqdpSlots)
_editor.ChangeArmor(state, objects, slot);
}
public void ChangeEquip(ActorState state, EquipSlot slot, EquipItem item, StateChanged.Source source)
{
var old = state.ModelData.Item(slot);
state.ModelData.SetItem(slot, item);
state[slot, false] = source;
_objects.Update();
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
if (source is StateChanged.Source.Manual)
_editor.ChangeArmor(state, objects, slot);
Glamourer.Log.Verbose(
$"Set {slot.ToName()} equipment piece in state {state.Identifier} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}). [Affecting {objects.ToLazyString("nothing")}.]");
_event.Invoke(StateChanged.Type.Equip, source, state, objects, (old, item, slot));
}
public void ChangeEquip(ActorState state, EquipSlot slot, EquipItem item, StainId stain, StateChanged.Source source)
{
var old = state.ModelData.Item(slot);
var oldStain = state.ModelData.Stain(slot);
state.ModelData.SetItem(slot, item);
state.ModelData.SetStain(slot, stain);
state[slot, false] = source;
state[slot, true] = source;
_objects.Update();
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
if (source is StateChanged.Source.Manual)
_editor.ChangeArmor(state, objects, slot);
Glamourer.Log.Verbose(
$"Set {slot.ToName()} equipment piece in state {state.Identifier} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}) and its stain from {oldStain.Value} to {stain.Value}. [Affecting {objects.ToLazyString("nothing")}.]");
_event.Invoke(StateChanged.Type.Equip, source, state, objects, (old, item, slot));
_event.Invoke(StateChanged.Type.Stain, source, state, objects, (oldStain, stain, slot));
}
public void ChangeStain(ActorState state, EquipSlot slot, StainId stain, StateChanged.Source source)
{
var old = state.ModelData.Stain(slot);
state.ModelData.SetStain(slot, stain);
state[slot, true] = source;
_objects.Update();
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
if (source is StateChanged.Source.Manual)
_editor.ChangeArmor(state, objects, slot);
Glamourer.Log.Verbose(
$"Set {slot.ToName()} stain in state {state.Identifier} from {old.Value} to {stain.Value}. [Affecting {objects.ToLazyString("nothing")}.]");
_event.Invoke(StateChanged.Type.Stain, source, state, objects, (old, stain, slot));
}
//
///// <summary> Change whether to apply a specific customize value. </summary>
//public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value)
@ -255,21 +348,7 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
// _event.Invoke(DesignChanged.Type.ApplyCustomize, design, idx);
//}
//
///// <summary> Change a non-weapon equipment piece. </summary>
//public void ChangeEquip(Design design, EquipSlot slot, EquipItem item)
//{
// if (_items.ValidateItem(slot, item.Id, out item).Length > 0)
// return;
//
// var old = design.DesignData.Item(slot);
// if (!design.DesignData.SetItem(slot, item))
// return;
//
// Glamourer.Log.Debug(
// $"Set {slot.ToName()} equipment piece in design {design.Identifier} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}).");
// _saveService.QueueSave(design);
// _event.Invoke(DesignChanged.Type.Equip, design, (old, item, slot));
//}
//
///// <summary> Change a weapon. </summary>
//public void ChangeWeapon(Design design, EquipSlot slot, EquipItem item)