Add listening to ChangeCustomize.

This commit is contained in:
Ottermandias 2023-07-27 18:47:01 +02:00
parent 19e81b4e16
commit ccb29ef9b2
4 changed files with 120 additions and 41 deletions

View file

@ -15,6 +15,7 @@ public class GPoseService : EventWrapper<Action<bool>, GPoseService.Priority>
public enum Priority
{
/// <seealso cref="Api.GlamourerIpc.OnGPoseChanged"/>
GlamourerIpc = int.MinValue,
}

View file

@ -1,7 +1,11 @@
using Dalamud.Utility.Signatures;
using System;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization;
using Glamourer.Interop.Structs;
using Penumbra.GameData.Structs;
using OtterGui.Classes;
using CustomizeData = Penumbra.GameData.Structs.CustomizeData;
namespace Glamourer.Interop;
@ -10,15 +14,32 @@ namespace Glamourer.Interop;
/// Changes in Race, body type or Gender are probably ignored.
/// This operates on draw objects, not game objects.
/// </summary>
public unsafe class ChangeCustomizeService
public unsafe class ChangeCustomizeService : EventWrapper<Action<Model, Ref<Customize>>, ChangeCustomizeService.Priority>
{
public enum Priority
{
/// <seealso cref="State.StateListener.OnCustomizeChange"/>
StateListener = 0,
}
public ChangeCustomizeService()
=> SignatureHelper.Initialise(this);
: base("ChangeCustomize")
{
_changeCustomizeHook =
Hook<ChangeCustomizeDelegate>.FromAddress((nint)Human.MemberFunctionPointers.UpdateDrawData, ChangeCustomizeDetour);
_changeCustomizeHook.Enable();
}
public new void Dispose()
{
base.Dispose();
_changeCustomizeHook.Dispose();
}
private delegate bool ChangeCustomizeDelegate(Human* human, byte* data, byte skipEquipment);
[Signature(Sigs.ChangeCustomize)]
private readonly ChangeCustomizeDelegate _changeCustomize = null!;
[Signature(Sigs.ChangeCustomize, DetourName = nameof(ChangeCustomizeDetour))]
private readonly Hook<ChangeCustomizeDelegate> _changeCustomizeHook;
public bool UpdateCustomize(Model model, CustomizeData customize)
{
@ -26,9 +47,19 @@ public unsafe class ChangeCustomizeService
return false;
Glamourer.Log.Verbose($"[ChangeCustomize] Invoked on 0x{model.Address:X} with {customize}.");
return _changeCustomize(model.AsHuman, customize.Data, 1);
return _changeCustomizeHook.Original(model.AsHuman, customize.Data, 1);
}
public bool UpdateCustomize(Actor actor, CustomizeData customize)
=> UpdateCustomize(actor.Model, customize);
private bool ChangeCustomizeDetour(Human* human, byte* data, byte skipEquipment)
{
var customize = new Ref<Customize>(new Customize(*(CustomizeData*)data));
Invoke(this, (Model)human, customize);
fixed (byte* ptr = customize.Value.Data.Data)
{
return _changeCustomizeHook.Original(human, ptr, skipEquipment);
}
}
}

View file

@ -1,5 +1,4 @@
using System;
using Glamourer.Automation;
using Glamourer.Automation;
using Glamourer.Customization;
using Glamourer.Events;
using Glamourer.Interop;
@ -11,6 +10,7 @@ using Penumbra.GameData.Actors;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using System;
namespace Glamourer.State;
@ -27,6 +27,7 @@ public class StateListener : IDisposable
private readonly StateManager _manager;
private readonly StateApplier _applier;
private readonly ItemManager _items;
private readonly CustomizationService _customizations;
private readonly PenumbraService _penumbra;
private readonly SlotUpdating _slotUpdating;
private readonly WeaponLoading _weaponLoading;
@ -37,7 +38,8 @@ public class StateListener : IDisposable
private readonly FunModule _funModule;
private readonly HumanModelList _humans;
private readonly MovedEquipment _movedEquipment;
private readonly GPoseService _gpose;
private readonly GPoseService _gPose;
private readonly ChangeCustomizeService _changeCustomizeService;
private ActorIdentifier _creatingIdentifier = ActorIdentifier.Invalid;
private ActorState? _creatingState;
@ -52,25 +54,28 @@ public class StateListener : IDisposable
public StateListener(StateManager manager, ItemManager items, PenumbraService penumbra, ActorService actors, Configuration config,
SlotUpdating slotUpdating, WeaponLoading weaponLoading, VisorStateChanged visorState, WeaponVisibilityChanged weaponVisibility,
HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier, FunModule funModule, HumanModelList humans,
StateApplier applier, MovedEquipment movedEquipment, ObjectManager objects, GPoseService gpose)
StateApplier applier, MovedEquipment movedEquipment, ObjectManager objects, GPoseService gPose,
ChangeCustomizeService changeCustomizeService, CustomizationService customizations)
{
_manager = manager;
_items = items;
_penumbra = penumbra;
_actors = actors;
_config = config;
_slotUpdating = slotUpdating;
_weaponLoading = weaponLoading;
_visorState = visorState;
_weaponVisibility = weaponVisibility;
_headGearVisibility = headGearVisibility;
_autoDesignApplier = autoDesignApplier;
_funModule = funModule;
_humans = humans;
_applier = applier;
_movedEquipment = movedEquipment;
_objects = objects;
_gpose = gpose;
_manager = manager;
_items = items;
_penumbra = penumbra;
_actors = actors;
_config = config;
_slotUpdating = slotUpdating;
_weaponLoading = weaponLoading;
_visorState = visorState;
_weaponVisibility = weaponVisibility;
_headGearVisibility = headGearVisibility;
_autoDesignApplier = autoDesignApplier;
_funModule = funModule;
_humans = humans;
_applier = applier;
_movedEquipment = movedEquipment;
_objects = objects;
_gPose = gPose;
_changeCustomizeService = changeCustomizeService;
_customizations = customizations;
if (Enabled)
Subscribe();
@ -132,15 +137,7 @@ public class StateListener : IDisposable
case UpdateState.NoChange:
modelId = _creatingState.ModelData.ModelId;
switch (UpdateBaseData(actor, _creatingState, customize))
{
case UpdateState.Transformed: break;
case UpdateState.Change: break;
case UpdateState.NoChange:
customize = _creatingState.ModelData.Customize;
break;
}
UpdateCustomize(actor, _creatingState, ref customize, true);
foreach (var slot in EquipSlotExtensions.EqdpSlots)
HandleEquipSlot(actor, _creatingState, slot, ref ((CharacterArmor*)equipDataPtr)[slot.ToIndex()]);
@ -155,6 +152,54 @@ public class StateListener : IDisposable
ProtectRestrictedGear(equipDataPtr, customize.Race, customize.Gender);
}
private unsafe void OnCustomizeChange(Model model, Ref<Customize> customize)
{
if (!model.IsHuman)
return;
var actor = _penumbra.GameObjectFromDrawObject(model);
if (!actor.Identifier(_actors.AwaitedService, out var identifier)
|| !_manager.TryGetValue(identifier, out var state))
return;
UpdateCustomize(actor, state, ref customize.Value, false);
}
private void UpdateCustomize(Actor actor, ActorState state, ref Customize customize, bool checkTransform)
{
switch (UpdateBaseData(actor, state, customize, checkTransform))
{
case UpdateState.Transformed: break;
case UpdateState.Change:
var set = _customizations.AwaitedService.GetList(state.ModelData.Customize.Clan, state.ModelData.Customize.Gender);
var model = state.ModelData.Customize;
foreach (var index in CustomizationExtensions.AllBasic)
{
if (state[index] is not StateChanged.Source.Fixed)
{
var newValue = customize[index];
var oldValue = model[index];
if (newValue != oldValue)
{
if (set.Validate(index, newValue, out _, model.Face))
_manager.ChangeCustomize(state, index, newValue, StateChanged.Source.Game);
else
customize[index] = oldValue;
}
}
else
{
customize[index] = model[index];
}
}
break;
case UpdateState.NoChange:
customize = state.ModelData.Customize;
break;
}
}
/// <summary>
/// A draw model loads a new equipment piece.
/// Update base data, apply or update model data, and protect against restricted gear.
@ -250,7 +295,7 @@ public class StateListener : IDisposable
{
// Only allow overwriting identical weapons
var newWeapon = state.ModelData.Weapon(slot);
if (baseType is FullEquipType.Unknown || baseType == state.ModelData.Item(slot).Type || _gpose.InGPose && actor.IsGPoseOrCutscene)
if (baseType is FullEquipType.Unknown || baseType == state.ModelData.Item(slot).Type || _gPose.InGPose && actor.IsGPoseOrCutscene)
actorWeapon = newWeapon;
else if (actorWeapon.Set.Value != 0)
actorWeapon = actorWeapon.With(newWeapon.Stain);
@ -385,10 +430,10 @@ public class StateListener : IDisposable
/// only if we kept track of state of someone who went to the aesthetician,
/// or if they used other tools to change things.
/// </summary>
private UpdateState UpdateBaseData(Actor actor, ActorState state, Customize customize)
private UpdateState UpdateBaseData(Actor actor, ActorState state, Customize customize, bool checkTransform)
{
// Customize array does not agree between game object and draw object => transformation.
if (!actor.GetCustomize().Equals(customize))
if (checkTransform && !actor.GetCustomize().Equals(customize))
return UpdateState.Transformed;
// Customize array did not change to stored state.
@ -516,6 +561,7 @@ public class StateListener : IDisposable
_visorState.Subscribe(OnVisorChange, VisorStateChanged.Priority.StateListener);
_headGearVisibility.Subscribe(OnHeadGearVisibilityChange, HeadGearVisibilityChanged.Priority.StateListener);
_weaponVisibility.Subscribe(OnWeaponVisibilityChange, WeaponVisibilityChanged.Priority.StateListener);
_changeCustomizeService.Subscribe(OnCustomizeChange, ChangeCustomizeService.Priority.StateListener);
}
private void Unsubscribe()
@ -528,6 +574,7 @@ public class StateListener : IDisposable
_visorState.Unsubscribe(OnVisorChange);
_headGearVisibility.Unsubscribe(OnHeadGearVisibilityChange);
_weaponVisibility.Unsubscribe(OnWeaponVisibilityChange);
_changeCustomizeService.Unsubscribe(OnCustomizeChange);
}
private void OnCreatedCharacterBase(nint gameObject, string _, nint drawObject)

@ -1 +1 @@
Subproject commit f306da6f43e8681d3bace45cbd4fd98ef49927ca
Subproject commit 5dd2b440e69b1725fa214b005b7179f2414a4053