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.Penumbra; using Glamourer.Interop.Structs; using Glamourer.Services; using Glamourer.Structs; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; namespace Glamourer.State; public class StateManager : IReadOnlyDictionary { private readonly ActorService _actors; private readonly ItemManager _items; private readonly CustomizationService _customizations; private readonly VisorService _visor; private readonly StateChanged _event; private readonly PenumbraService _penumbra; private readonly Dictionary _states = new(); public StateManager(ActorService actors, ItemManager items, CustomizationService customizations, VisorService visor, StateChanged @event, PenumbraService penumbra) { _actors = actors; _items = items; _customizations = customizations; _visor = visor; _event = @event; _penumbra = penumbra; } public bool GetOrCreate(Actor actor, [NotNullWhen(true)] out ActorState? state) => GetOrCreate(actor.GetIdentifier(_actors.AwaitedService), actor, out state); public bool GetOrCreate(ActorIdentifier identifier, Actor actor, [NotNullWhen(true)] out ActorState? state) { if (TryGetValue(identifier, out state)) return true; try { var designData = FromActor(actor); state = new ActorState(identifier) { ModelData = designData, ActorData = designData }; _states.Add(identifier, state); return true; } catch (Exception ex) { Glamourer.Log.Error($"Could not create new actor data for {identifier}:\n{ex}"); return false; } } public void UpdateEquip(ActorState state, EquipSlot slot, CharacterArmor armor) { var current = state.ModelData.Item(slot); if (armor.Set.Value != current.ModelId.Value || armor.Variant != current.Variant) { var item = _items.Identify(slot, armor.Set, armor.Variant); state.ModelData.SetItem(slot, item); } state.ModelData.SetStain(slot, armor.Stain); } public void UpdateWeapon(ActorState state, EquipSlot slot, CharacterWeapon weapon) { var current = state.ModelData.Item(slot); if (weapon.Set.Value != current.ModelId.Value || weapon.Variant != current.Variant || weapon.Type.Value != current.WeaponType.Value) { var item = _items.Identify(slot, weapon.Set, weapon.Type, (byte)weapon.Variant, slot == EquipSlot.OffHand ? state.ModelData.Item(EquipSlot.MainHand).Type : FullEquipType.Unknown); state.ModelData.SetItem(slot, item); } state.ModelData.SetStain(slot, weapon.Stain); } public unsafe void Update(ActorState state, Actor actor) { if (!actor.IsCharacter) return; if (actor.AsCharacter->ModelCharaId != state.ModelData.ModelId) return; var model = actor.Model; state.ModelData.SetHatVisible(!actor.AsCharacter->DrawData.IsHatHidden); state.ModelData.SetIsWet(actor.AsCharacter->IsGPoseWet); state.ModelData.SetWeaponVisible(!actor.AsCharacter->DrawData.IsWeaponHidden); if (model.IsHuman) { var head = state.ModelData.IsHatVisible() ? model.GetArmor(EquipSlot.Head) : actor.GetArmor(EquipSlot.Head); UpdateEquip(state, EquipSlot.Head, head); foreach (var slot in EquipSlotExtensions.EqdpSlots.Skip(1)) UpdateEquip(state, slot, model.GetArmor(slot)); state.ModelData.Customize = model.GetCustomize(); var (_, _, main, off) = model.GetWeapons(actor); UpdateWeapon(state, EquipSlot.MainHand, main); UpdateWeapon(state, EquipSlot.OffHand, off); state.ModelData.SetVisor(_visor.GetVisorState(model)); } else { foreach (var slot in EquipSlotExtensions.EqdpSlots) UpdateEquip(state, slot, actor.GetArmor(slot)); state.ModelData.Customize = actor.GetCustomize(); UpdateWeapon(state, EquipSlot.MainHand, actor.GetMainhand()); UpdateWeapon(state, EquipSlot.OffHand, actor.GetOffhand()); state.ModelData.SetVisor(actor.AsCharacter->DrawData.IsVisorToggled); } } public IEnumerator> 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 Keys => _states.Keys; public IEnumerable Values => _states.Values; public unsafe DesignData FromActor(Actor actor) { var ret = new DesignData(); if (!actor.IsCharacter) { ret.SetDefaultEquipment(_items); return ret; } if (actor.AsCharacter->ModelCharaId != 0) { ret.LoadNonHuman((uint)actor.AsCharacter->ModelCharaId, *(Customize*)&actor.AsCharacter->DrawData.CustomizeData, (byte*)&actor.AsCharacter->DrawData.Head); return ret; } var model = actor.Model; CharacterWeapon main; CharacterWeapon off; ret.SetHatVisible(!actor.AsCharacter->DrawData.IsHatHidden); if (model.IsHuman) { 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); 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); } ret.Customize = model.GetCustomize(); (_, _, main, off) = model.GetWeapons(actor); ret.SetVisor(_visor.GetVisorState(model)); } else { 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); } ret.Customize = actor.GetCustomize(); main = actor.GetMainhand(); off = actor.GetOffhand(); ret.SetVisor(actor.AsCharacter->DrawData.IsVisorToggled); } 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); ret.SetIsWet(actor.AsCharacter->IsGPoseWet); ret.SetWeaponVisible(!actor.AsCharacter->DrawData.IsWeaponHidden); return ret; } /// Change a customization value. public void ChangeCustomize(ActorState state, ActorData data, CustomizeIndex idx, CustomizeValue value, StateChanged.Source source, bool force) { ref var s = ref state[idx]; if (s is StateChanged.Source.Fixed && source is StateChanged.Source.Game) return; var oldValue = state.ModelData.Customize[idx]; if (oldValue == value && !force) return; state.ModelData.Customize[idx] = value; Glamourer.Log.Excessive( $"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)); } // ///// Change whether to apply a specific customize value. //public void ChangeApplyCustomize(Design design, CustomizeIndex idx, bool value) //{ // if (!design.SetApplyCustomize(idx, value)) // return; // // design.LastEdit = DateTimeOffset.UtcNow; // _saveService.QueueSave(design); // Glamourer.Log.Debug($"Set applying of customization {idx.ToDefaultName()} to {value}."); // _event.Invoke(DesignChanged.Type.ApplyCustomize, design, idx); //} // ///// Change a non-weapon equipment piece. //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)); //} // ///// Change a weapon. //public void ChangeWeapon(Design design, EquipSlot slot, EquipItem item) //{ // var currentMain = design.DesignData.Item(EquipSlot.MainHand); // var currentOff = design.DesignData.Item(EquipSlot.OffHand); // switch (slot) // { // case EquipSlot.MainHand: // var newOff = currentOff; // if (item.Type == currentMain.Type) // { // if (_items.ValidateWeapons(item.Id, currentOff.Id, out _, out _).Length != 0) // return; // } // else // { // var newOffId = FullEquipTypeExtensions.OffhandTypes.Contains(item.Type) // ? item.Id // : ItemManager.NothingId(item.Type.Offhand()); // if (_items.ValidateWeapons(item.Id, newOffId, out _, out newOff).Length != 0) // return; // } // // design.DesignData.SetItem(EquipSlot.MainHand, item); // design.DesignData.SetItem(EquipSlot.OffHand, newOff); // design.LastEdit = DateTimeOffset.UtcNow; // _saveService.QueueSave(design); // Glamourer.Log.Debug( // $"Set {EquipSlot.MainHand.ToName()} weapon in design {design.Identifier} from {currentMain.Name} ({currentMain.Id}) to {item.Name} ({item.Id})."); // _event.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, item, newOff)); // return; // case EquipSlot.OffHand: // if (item.Type != currentOff.Type) // return; // if (_items.ValidateWeapons(currentMain.Id, item.Id, out _, out _).Length > 0) // return; // // if (!design.DesignData.SetItem(EquipSlot.OffHand, item)) // return; // // design.LastEdit = DateTimeOffset.UtcNow; // _saveService.QueueSave(design); // Glamourer.Log.Debug( // $"Set {EquipSlot.OffHand.ToName()} weapon in design {design.Identifier} from {currentOff.Name} ({currentOff.Id}) to {item.Name} ({item.Id})."); // _event.Invoke(DesignChanged.Type.Weapon, design, (currentMain, currentOff, currentMain, item)); // return; // default: return; // } //} // ///// Change whether to apply a specific equipment piece. //public void ChangeApplyEquip(Design design, EquipSlot slot, bool value) //{ // if (!design.SetApplyEquip(slot, value)) // return; // // design.LastEdit = DateTimeOffset.UtcNow; // _saveService.QueueSave(design); // Glamourer.Log.Debug($"Set applying of {slot} equipment piece to {value}."); // _event.Invoke(DesignChanged.Type.ApplyEquip, design, slot); //} // ///// Change the stain for any equipment piece. //public void ChangeStain(Design design, EquipSlot slot, StainId stain) //{ // if (_items.ValidateStain(stain, out _).Length > 0) // return; // // var oldStain = design.DesignData.Stain(slot); // if (!design.DesignData.SetStain(slot, stain)) // return; // // design.LastEdit = DateTimeOffset.UtcNow; // _saveService.QueueSave(design); // Glamourer.Log.Debug($"Set stain of {slot} equipment piece to {stain.Value}."); // _event.Invoke(DesignChanged.Type.Stain, design, (oldStain, stain, slot)); //} // ///// Change whether to apply a specific stain. //public void ChangeApplyStain(Design design, EquipSlot slot, bool value) //{ // if (!design.SetApplyStain(slot, value)) // return; // // design.LastEdit = DateTimeOffset.UtcNow; // _saveService.QueueSave(design); // Glamourer.Log.Debug($"Set applying of stain of {slot} equipment piece to {value}."); // _event.Invoke(DesignChanged.Type.ApplyStain, design, slot); //} }