mirror of
https://github.com/Ottermandias/Glamourer.git
synced 2025-12-15 05:04:16 +01:00
437 lines
21 KiB
C#
437 lines
21 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
using Glamourer.Customization;
|
|
using Glamourer.Designs;
|
|
using Glamourer.Events;
|
|
using Glamourer.Interop;
|
|
using Glamourer.Interop.Structs;
|
|
using Glamourer.Services;
|
|
using Penumbra.GameData.Actors;
|
|
using Penumbra.GameData.Data;
|
|
using Penumbra.GameData.Enums;
|
|
using Penumbra.GameData.Structs;
|
|
|
|
namespace Glamourer.State;
|
|
|
|
public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
|
|
{
|
|
private readonly ActorService _actors;
|
|
private readonly ItemManager _items;
|
|
private readonly HumanModelList _humans;
|
|
private readonly StateChanged _event;
|
|
private readonly StateApplier _applier;
|
|
private readonly StateEditor _editor;
|
|
|
|
private readonly Dictionary<ActorIdentifier, ActorState> _states = new();
|
|
|
|
public StateManager(ActorService actors, ItemManager items, StateChanged @event, StateApplier applier, StateEditor editor,
|
|
HumanModelList humans)
|
|
{
|
|
_actors = actors;
|
|
_items = items;
|
|
_event = @event;
|
|
_applier = applier;
|
|
_editor = editor;
|
|
_humans = humans;
|
|
}
|
|
|
|
public IEnumerator<KeyValuePair<ActorIdentifier, ActorState>> GetEnumerator()
|
|
=> _states.GetEnumerator();
|
|
|
|
IEnumerator IEnumerable.GetEnumerator()
|
|
=> GetEnumerator();
|
|
|
|
public int Count
|
|
=> _states.Count;
|
|
|
|
public bool ContainsKey(ActorIdentifier key)
|
|
=> _states.ContainsKey(key);
|
|
|
|
public bool TryGetValue(ActorIdentifier key, out ActorState value)
|
|
=> _states.TryGetValue(key, out value!);
|
|
|
|
public ActorState this[ActorIdentifier key]
|
|
=> _states[key];
|
|
|
|
public IEnumerable<ActorIdentifier> Keys
|
|
=> _states.Keys;
|
|
|
|
public IEnumerable<ActorState> Values
|
|
=> _states.Values;
|
|
|
|
/// <inheritdoc cref="GetOrCreate(ActorIdentifier, Actor, out ActorState?)"/>
|
|
public bool GetOrCreate(Actor actor, [NotNullWhen(true)] out ActorState? state)
|
|
=> GetOrCreate(actor.GetIdentifier(_actors.AwaitedService), actor, out state);
|
|
|
|
/// <summary> Try to obtain or create a new state for an existing actor. Returns false if no state could be created. </summary>
|
|
public unsafe bool GetOrCreate(ActorIdentifier identifier, Actor actor, [NotNullWhen(true)] out ActorState? state)
|
|
{
|
|
if (TryGetValue(identifier, out state))
|
|
return true;
|
|
|
|
try
|
|
{
|
|
// Initial Creation, use the actors data for the base data,
|
|
// and the draw objects data for the model data (where possible).
|
|
state = new ActorState(identifier)
|
|
{
|
|
ModelData = FromActor(actor, true),
|
|
BaseData = FromActor(actor, false),
|
|
LastJob = (byte)(actor.IsCharacter ? actor.AsCharacter->CharacterData.ClassJob : 0),
|
|
};
|
|
// state.Identifier is owned.
|
|
_states.Add(state.Identifier, state);
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Glamourer.Log.Error($"Could not create new actor data for {identifier}:\n{ex}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create DesignData from a given actor.
|
|
/// This uses the draw object if available and where possible,
|
|
/// and the game object where necessary.
|
|
/// </summary>
|
|
public unsafe DesignData FromActor(Actor actor, bool useModel)
|
|
{
|
|
var ret = new DesignData();
|
|
// If the given actor is not a character, just return a default character.
|
|
if (!actor.IsCharacter)
|
|
{
|
|
ret.SetDefaultEquipment(_items);
|
|
return ret;
|
|
}
|
|
|
|
var model = actor.Model;
|
|
|
|
// Model ID is only unambiguously contained in the game object.
|
|
// The draw object only has the object type.
|
|
// TODO reverse search model data to get model id from model.
|
|
if (!_humans.IsHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId))
|
|
{
|
|
ret.LoadNonHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId, *(Customize*)&actor.AsCharacter->DrawData.CustomizeData,
|
|
(nint)(&actor.AsCharacter->DrawData.Head));
|
|
return ret;
|
|
}
|
|
|
|
CharacterWeapon main;
|
|
CharacterWeapon off;
|
|
|
|
// Hat visibility is only unambiguously contained in the game object.
|
|
// Set it first to know where to get head slot data from.
|
|
ret.SetHatVisible(!actor.AsCharacter->DrawData.IsHatHidden);
|
|
|
|
// Use the draw object if it is a human.
|
|
if (useModel && model.IsHuman)
|
|
{
|
|
// Customize can be obtained from the draw object.
|
|
ret.Customize = model.GetCustomize();
|
|
|
|
// We can not use the head slot data from the draw object if the hat is hidden.
|
|
var head = ret.IsHatVisible() ? model.GetArmor(EquipSlot.Head) : actor.GetArmor(EquipSlot.Head);
|
|
var headItem = _items.Identify(EquipSlot.Head, head.Set, head.Variant);
|
|
ret.SetItem(EquipSlot.Head, headItem);
|
|
ret.SetStain(EquipSlot.Head, head.Stain);
|
|
|
|
// The other slots can be used from the draw object.
|
|
foreach (var slot in EquipSlotExtensions.EqdpSlots.Skip(1))
|
|
{
|
|
var armor = model.GetArmor(slot);
|
|
var item = _items.Identify(slot, armor.Set, armor.Variant);
|
|
ret.SetItem(slot, item);
|
|
ret.SetStain(slot, armor.Stain);
|
|
}
|
|
|
|
// Weapons use the draw objects of the weapons, but require the game object either way.
|
|
(_, _, main, off) = model.GetWeapons(actor);
|
|
|
|
// 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));
|
|
}
|
|
else
|
|
{
|
|
// Obtain all data from the game object.
|
|
ret.Customize = actor.GetCustomize();
|
|
|
|
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
|
{
|
|
var armor = actor.GetArmor(slot);
|
|
var item = _items.Identify(slot, armor.Set, armor.Variant);
|
|
ret.SetItem(slot, item);
|
|
ret.SetStain(slot, armor.Stain);
|
|
}
|
|
|
|
main = actor.GetMainhand();
|
|
off = actor.GetOffhand();
|
|
ret.SetVisor(actor.AsCharacter->DrawData.IsVisorToggled);
|
|
}
|
|
|
|
// Set the weapons regardless of source.
|
|
var mainItem = _items.Identify(EquipSlot.MainHand, main.Set, main.Type, (byte)main.Variant);
|
|
var offItem = _items.Identify(EquipSlot.OffHand, off.Set, off.Type, (byte)off.Variant, mainItem.Type);
|
|
ret.SetItem(EquipSlot.MainHand, mainItem);
|
|
ret.SetStain(EquipSlot.MainHand, main.Stain);
|
|
ret.SetItem(EquipSlot.OffHand, offItem);
|
|
ret.SetStain(EquipSlot.OffHand, off.Stain);
|
|
|
|
// Wetness can technically only be set in GPose or via external tools.
|
|
// It is only available in the game object.
|
|
ret.SetIsWet(actor.AsCharacter->IsGPoseWet);
|
|
|
|
// Weapon visibility could technically be inferred from the weapon draw objects,
|
|
// but since we use hat visibility from the game object we can also use weapon visibility from it.
|
|
ret.SetWeaponVisible(!actor.AsCharacter->DrawData.IsWeaponHidden);
|
|
return ret;
|
|
}
|
|
|
|
#region Change Values
|
|
|
|
/// <summary> Turn an actor human. </summary>
|
|
public void TurnHuman(ActorState state, StateChanged.Source source, uint key = 0)
|
|
=> ChangeModelId(state, 0, Customize.Default, nint.Zero, source, key);
|
|
|
|
/// <summary> Turn an actor to. </summary>
|
|
public void ChangeModelId(ActorState state, uint modelId, Customize customize, nint equipData, StateChanged.Source source,
|
|
uint key = 0)
|
|
{
|
|
if (!_editor.ChangeModelId(state, modelId, customize, equipData, source, out var old, key))
|
|
return;
|
|
|
|
var actors = _applier.ForceRedraw(state, source is StateChanged.Source.Manual or StateChanged.Source.Ipc);
|
|
Glamourer.Log.Verbose(
|
|
$"Set model id in state {state.Identifier.Incognito(null)} from {old} to {modelId}. [Affecting {actors.ToLazyString("nothing")}.]");
|
|
_event.Invoke(StateChanged.Type.Model, source, state, actors, (old, modelId));
|
|
}
|
|
|
|
/// <summary> Change a customization value. </summary>
|
|
public void ChangeCustomize(ActorState state, CustomizeIndex idx, CustomizeValue value, StateChanged.Source source, uint key = 0)
|
|
{
|
|
if (!_editor.ChangeCustomize(state, idx, value, source, out var old, key))
|
|
return;
|
|
|
|
var actors = _applier.ChangeCustomize(state, source is StateChanged.Source.Manual or StateChanged.Source.Ipc);
|
|
Glamourer.Log.Verbose(
|
|
$"Set {idx.ToDefaultName()} customizations in state {state.Identifier.Incognito(null)} from {old.Value} to {value.Value}. [Affecting {actors.ToLazyString("nothing")}.]");
|
|
_event.Invoke(StateChanged.Type.Customize, source, state, actors, (old, value, idx));
|
|
}
|
|
|
|
/// <summary> Change an entire customization array according to flags. </summary>
|
|
public void ChangeCustomize(ActorState state, in Customize customizeInput, CustomizeFlag apply, StateChanged.Source source, uint key = 0)
|
|
{
|
|
if (!_editor.ChangeHumanCustomize(state, customizeInput, apply, source, out var old, out var applied, key))
|
|
return;
|
|
|
|
var actors = _applier.ChangeCustomize(state, source is StateChanged.Source.Manual or StateChanged.Source.Ipc);
|
|
Glamourer.Log.Verbose(
|
|
$"Set {applied} customizations in state {state.Identifier.Incognito(null)} from {old} to {customizeInput}. [Affecting {actors.ToLazyString("nothing")}.]");
|
|
_event.Invoke(StateChanged.Type.EntireCustomize, source, state, actors, (old, applied));
|
|
}
|
|
|
|
/// <summary> Change a single piece of equipment without stain. </summary>
|
|
/// <remarks> Do not use this in the same frame as ChangeStain, use <see cref="ChangeEquip(ActorState,EquipSlot,EquipItem,StainId,StateChanged.Source,uint)"/> instead. </remarks>
|
|
public void ChangeItem(ActorState state, EquipSlot slot, EquipItem item, StateChanged.Source source, uint key = 0)
|
|
{
|
|
if (!_editor.ChangeItem(state, slot, item, source, out var old, key))
|
|
return;
|
|
|
|
var type = slot.ToIndex() < 10 ? StateChanged.Type.Equip : StateChanged.Type.Weapon;
|
|
var actors = type is StateChanged.Type.Equip
|
|
? _applier.ChangeArmor(state, slot, source is StateChanged.Source.Manual or StateChanged.Source.Ipc)
|
|
: _applier.ChangeWeapon(state, slot, source is StateChanged.Source.Manual or StateChanged.Source.Ipc);
|
|
Glamourer.Log.Verbose(
|
|
$"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId}). [Affecting {actors.ToLazyString("nothing")}.]");
|
|
_event.Invoke(type, source, state, actors, (old, item, slot));
|
|
}
|
|
|
|
/// <summary> Change a single piece of equipment including stain. </summary>
|
|
public void ChangeEquip(ActorState state, EquipSlot slot, EquipItem item, StainId stain, StateChanged.Source source, uint key = 0)
|
|
{
|
|
if (!_editor.ChangeEquip(state, slot, item, stain, source, out var old, out var oldStain, key))
|
|
return;
|
|
|
|
var type = slot.ToIndex() < 10 ? StateChanged.Type.Equip : StateChanged.Type.Weapon;
|
|
var actors = type is StateChanged.Type.Equip
|
|
? _applier.ChangeArmor(state, slot, source is StateChanged.Source.Manual or StateChanged.Source.Ipc)
|
|
: _applier.ChangeWeapon(state, slot, source is StateChanged.Source.Manual or StateChanged.Source.Ipc);
|
|
Glamourer.Log.Verbose(
|
|
$"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.ItemId}) to {item.Name} ({item.ItemId}) and its stain from {oldStain.Value} to {stain.Value}. [Affecting {actors.ToLazyString("nothing")}.]");
|
|
_event.Invoke(type, source, state, actors, (old, item, slot));
|
|
_event.Invoke(StateChanged.Type.Stain, source, state, actors, (oldStain, stain, slot));
|
|
}
|
|
|
|
/// <summary> Change only the stain of an equipment piece. </summary>
|
|
/// <remarks> Do not use this in the same frame as ChangeEquip, use <see cref="ChangeEquip(ActorState,EquipSlot,EquipItem,StainId,StateChanged.Source,uint)"/> instead. </remarks>
|
|
public void ChangeStain(ActorState state, EquipSlot slot, StainId stain, StateChanged.Source source, uint key = 0)
|
|
{
|
|
if (!_editor.ChangeStain(state, slot, stain, source, out var old, key))
|
|
return;
|
|
|
|
var actors = _applier.ChangeStain(state, slot, source is StateChanged.Source.Manual or StateChanged.Source.Ipc);
|
|
Glamourer.Log.Verbose(
|
|
$"Set {slot.ToName()} stain in state {state.Identifier.Incognito(null)} from {old.Value} to {stain.Value}. [Affecting {actors.ToLazyString("nothing")}.]");
|
|
_event.Invoke(StateChanged.Type.Stain, source, state, actors, (old, stain, slot));
|
|
}
|
|
|
|
/// <summary> Change hat visibility. </summary>
|
|
public void ChangeHatState(ActorState state, bool value, StateChanged.Source source, uint key = 0)
|
|
{
|
|
if (!_editor.ChangeMetaState(state, ActorState.MetaIndex.HatState, value, source, out var old, key))
|
|
return;
|
|
|
|
var actors = _applier.ChangeHatState(state, source is StateChanged.Source.Manual or StateChanged.Source.Ipc);
|
|
Glamourer.Log.Verbose(
|
|
$"Set Head Gear Visibility in state {state.Identifier.Incognito(null)} from {old} to {value}. [Affecting {actors.ToLazyString("nothing")}.]");
|
|
_event.Invoke(StateChanged.Type.Other, source, state, actors, (old, value, ActorState.MetaIndex.HatState));
|
|
}
|
|
|
|
/// <summary> Change weapon visibility. </summary>
|
|
public void ChangeWeaponState(ActorState state, bool value, StateChanged.Source source, uint key = 0)
|
|
{
|
|
if (!_editor.ChangeMetaState(state, ActorState.MetaIndex.WeaponState, value, source, out var old, key))
|
|
return;
|
|
|
|
var actors = _applier.ChangeWeaponState(state, source is StateChanged.Source.Manual or StateChanged.Source.Ipc);
|
|
Glamourer.Log.Verbose(
|
|
$"Set Weapon Visibility in state {state.Identifier.Incognito(null)} from {old} to {value}. [Affecting {actors.ToLazyString("nothing")}.]");
|
|
_event.Invoke(StateChanged.Type.Other, source, state, actors, (old, value, ActorState.MetaIndex.WeaponState));
|
|
}
|
|
|
|
/// <summary> Change visor state. </summary>
|
|
public void ChangeVisorState(ActorState state, bool value, StateChanged.Source source, uint key = 0)
|
|
{
|
|
if (!_editor.ChangeMetaState(state, ActorState.MetaIndex.VisorState, value, source, out var old, key))
|
|
return;
|
|
|
|
var actors = _applier.ChangeVisor(state, source is StateChanged.Source.Manual or StateChanged.Source.Ipc);
|
|
Glamourer.Log.Verbose(
|
|
$"Set Visor State in state {state.Identifier.Incognito(null)} from {old} to {value}. [Affecting {actors.ToLazyString("nothing")}.]");
|
|
_event.Invoke(StateChanged.Type.Other, source, state, actors, (old, value, ActorState.MetaIndex.VisorState));
|
|
}
|
|
|
|
/// <summary> Set GPose Wetness. </summary>
|
|
public void ChangeWetness(ActorState state, bool value, StateChanged.Source source, uint key = 0)
|
|
{
|
|
if (!_editor.ChangeMetaState(state, ActorState.MetaIndex.Wetness, value, source, out var old, key))
|
|
return;
|
|
|
|
var actors = _applier.ChangeWetness(state, true);
|
|
Glamourer.Log.Verbose(
|
|
$"Set Wetness in state {state.Identifier.Incognito(null)} from {old} to {value}. [Affecting {actors.ToLazyString("nothing")}.]");
|
|
_event.Invoke(StateChanged.Type.Other, state[ActorState.MetaIndex.Wetness], state, actors, (old, value, ActorState.MetaIndex.Wetness));
|
|
}
|
|
|
|
#endregion
|
|
|
|
public void ApplyDesign(DesignBase design, ActorState state, StateChanged.Source source, uint key = 0)
|
|
{
|
|
void HandleEquip(EquipSlot slot, bool applyPiece, bool applyStain)
|
|
{
|
|
var unused = (applyPiece, applyStain) switch
|
|
{
|
|
(false, false) => false,
|
|
(true, false) => _editor.ChangeItem(state, slot, design.DesignData.Item(slot), source, out _, key),
|
|
(false, true) => _editor.ChangeStain(state, slot, design.DesignData.Stain(slot), source, out _, key),
|
|
(true, true) => _editor.ChangeEquip(state, slot, design.DesignData.Item(slot), design.DesignData.Stain(slot), source, out _,
|
|
out _, key),
|
|
};
|
|
}
|
|
|
|
if (!_editor.ChangeModelId(state, design.DesignData.ModelId, design.DesignData.Customize, design.DesignData.GetEquipmentPtr(), source,
|
|
out var oldModelId, key))
|
|
return;
|
|
|
|
var redraw = oldModelId != design.DesignData.ModelId || !design.DesignData.IsHuman;
|
|
if (design.DoApplyWetness())
|
|
_editor.ChangeMetaState(state, ActorState.MetaIndex.Wetness, design.DesignData.IsWet(), source, out _, key);
|
|
|
|
if (state.ModelData.IsHuman)
|
|
{
|
|
if (design.DoApplyHatVisible())
|
|
_editor.ChangeMetaState(state, ActorState.MetaIndex.HatState, design.DesignData.IsHatVisible(), source, out _, key);
|
|
if (design.DoApplyWeaponVisible())
|
|
_editor.ChangeMetaState(state, ActorState.MetaIndex.WeaponState, design.DesignData.IsWeaponVisible(), source, out _, key);
|
|
if (design.DoApplyVisorToggle())
|
|
_editor.ChangeMetaState(state, ActorState.MetaIndex.VisorState, design.DesignData.IsVisorToggled(), source, out _, key);
|
|
|
|
_editor.ChangeHumanCustomize(state, design.DesignData.Customize, design.ApplyCustomize, source, out _, out var applied, key);
|
|
redraw |= applied.RequiresRedraw();
|
|
|
|
foreach (var slot in EquipSlotExtensions.FullSlots)
|
|
HandleEquip(slot, design.DoApplyEquip(slot), design.DoApplyStain(slot));
|
|
}
|
|
|
|
var actors = ApplyAll(state, redraw);
|
|
Glamourer.Log.Verbose(
|
|
$"Applied design to {state.Identifier.Incognito(null)}. [Affecting {actors.ToLazyString("nothing")}.]");
|
|
_event.Invoke(StateChanged.Type.Design, state[ActorState.MetaIndex.Wetness], state, actors, design);
|
|
}
|
|
|
|
private ActorData ApplyAll(ActorState state, bool redraw)
|
|
{
|
|
var actors = _applier.ChangeWetness(state, true);
|
|
if (redraw)
|
|
{
|
|
_applier.ForceRedraw(actors);
|
|
}
|
|
else
|
|
{
|
|
_applier.ChangeCustomize(actors, state.ModelData.Customize);
|
|
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
|
_applier.ChangeArmor(actors, slot, state.ModelData.Armor(slot), state.ModelData.IsHatVisible());
|
|
foreach (var slot in EquipSlotExtensions.WeaponSlots)
|
|
_applier.ChangeWeapon(actors, slot, state.ModelData.Item(slot), state.ModelData.Stain(slot));
|
|
}
|
|
|
|
if (state.ModelData.IsHuman)
|
|
{
|
|
_applier.ChangeHatState(actors, state.ModelData.IsHatVisible());
|
|
_applier.ChangeWeaponState(actors, state.ModelData.IsWeaponVisible());
|
|
_applier.ChangeVisor(actors, state.ModelData.IsVisorToggled());
|
|
}
|
|
|
|
return actors;
|
|
}
|
|
|
|
public void ResetState(ActorState state, uint key = 0)
|
|
{
|
|
if (!state.Unlock(key))
|
|
return;
|
|
|
|
var redraw = state.ModelData.ModelId != state.BaseData.ModelId
|
|
|| !state.ModelData.IsHuman
|
|
|| Customize.Compare(state.ModelData.Customize, state.BaseData.Customize).RequiresRedraw();
|
|
state.ModelData = state.BaseData;
|
|
foreach (var index in Enum.GetValues<CustomizeIndex>())
|
|
state[index] = StateChanged.Source.Game;
|
|
foreach (var slot in EquipSlotExtensions.FullSlots)
|
|
{
|
|
state[slot, true] = StateChanged.Source.Game;
|
|
state[slot, false] = StateChanged.Source.Game;
|
|
}
|
|
|
|
foreach (var type in Enum.GetValues<ActorState.MetaIndex>())
|
|
state[type] = StateChanged.Source.Game;
|
|
|
|
var actors = ApplyAll(state, redraw);
|
|
Glamourer.Log.Verbose(
|
|
$"Reset entire state of {state.Identifier.Incognito(null)} to game base. [Affecting {actors.ToLazyString("nothing")}.]");
|
|
_event.Invoke(StateChanged.Type.Design, state[ActorState.MetaIndex.Wetness], state, actors, null);
|
|
}
|
|
|
|
public void ReapplyState(Actor actor)
|
|
{
|
|
if (!GetOrCreate(actor, out var state))
|
|
return;
|
|
|
|
ApplyAll(state, !actor.Model.IsHuman || Customize.Compare(actor.Model.GetCustomize(), state.ModelData.Customize).RequiresRedraw());
|
|
}
|
|
|
|
public void DeleteState(ActorIdentifier identifier)
|
|
=> _states.Remove(identifier);
|
|
}
|