mirror of
https://github.com/Ottermandias/Glamourer.git
synced 2025-12-12 18:27:24 +01:00
485 lines
20 KiB
C#
485 lines
20 KiB
C#
using System;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using Dalamud.Plugin.Services;
|
|
using Glamourer.Customization;
|
|
using Glamourer.Designs;
|
|
using Glamourer.Events;
|
|
using Glamourer.Interop;
|
|
using Glamourer.Interop.Structs;
|
|
using Glamourer.Services;
|
|
using Glamourer.State;
|
|
using Glamourer.Structs;
|
|
using Glamourer.Unlocks;
|
|
using OtterGui.Classes;
|
|
using Penumbra.GameData.Actors;
|
|
using Penumbra.GameData.Data;
|
|
using Penumbra.GameData.Enums;
|
|
using Penumbra.GameData.Structs;
|
|
|
|
namespace Glamourer.Automation;
|
|
|
|
public class AutoDesignApplier : IDisposable
|
|
{
|
|
private readonly Configuration _config;
|
|
private readonly AutoDesignManager _manager;
|
|
private readonly StateManager _state;
|
|
private readonly JobService _jobs;
|
|
private readonly ActorService _actors;
|
|
private readonly CustomizationService _customizations;
|
|
private readonly CustomizeUnlockManager _customizeUnlocks;
|
|
private readonly ItemUnlockManager _itemUnlocks;
|
|
private readonly AutomationChanged _event;
|
|
private readonly ObjectManager _objects;
|
|
private readonly WeaponLoading _weapons;
|
|
private readonly HumanModelList _humans;
|
|
private readonly IClientState _clientState;
|
|
|
|
private ActorState? _jobChangeState;
|
|
private EquipItem _jobChangeMainhand;
|
|
private EquipItem _jobChangeOffhand;
|
|
|
|
public AutoDesignApplier(Configuration config, AutoDesignManager manager, StateManager state, JobService jobs,
|
|
CustomizationService customizations, ActorService actors, ItemUnlockManager itemUnlocks, CustomizeUnlockManager customizeUnlocks,
|
|
AutomationChanged @event, ObjectManager objects, WeaponLoading weapons, HumanModelList humans, IClientState clientState)
|
|
{
|
|
_config = config;
|
|
_manager = manager;
|
|
_state = state;
|
|
_jobs = jobs;
|
|
_customizations = customizations;
|
|
_actors = actors;
|
|
_itemUnlocks = itemUnlocks;
|
|
_customizeUnlocks = customizeUnlocks;
|
|
_event = @event;
|
|
_objects = objects;
|
|
_weapons = weapons;
|
|
_humans = humans;
|
|
_clientState = clientState;
|
|
_jobs.JobChanged += OnJobChange;
|
|
_event.Subscribe(OnAutomationChange, AutomationChanged.Priority.AutoDesignApplier);
|
|
_weapons.Subscribe(OnWeaponLoading, WeaponLoading.Priority.AutoDesignApplier);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_weapons.Unsubscribe(OnWeaponLoading);
|
|
_event.Unsubscribe(OnAutomationChange);
|
|
_jobs.JobChanged -= OnJobChange;
|
|
}
|
|
|
|
private void OnWeaponLoading(Actor actor, EquipSlot slot, Ref<CharacterWeapon> weapon)
|
|
{
|
|
if (_jobChangeState == null || !_config.EnableAutoDesigns)
|
|
return;
|
|
|
|
var id = actor.GetIdentifier(_actors.AwaitedService);
|
|
if (id == _jobChangeState.Identifier)
|
|
{
|
|
var current = _jobChangeState.BaseData.Item(slot);
|
|
if (slot is EquipSlot.MainHand)
|
|
{
|
|
if (current.Type == _jobChangeMainhand.Type)
|
|
{
|
|
_state.ChangeItem(_jobChangeState, EquipSlot.MainHand, _jobChangeMainhand, StateChanged.Source.Fixed);
|
|
weapon.Value = _jobChangeState.ModelData.Weapon(EquipSlot.MainHand);
|
|
}
|
|
}
|
|
else if (slot is EquipSlot.OffHand)
|
|
{
|
|
if (current.Type == _jobChangeOffhand.Type)
|
|
{
|
|
_state.ChangeItem(_jobChangeState, EquipSlot.OffHand, _jobChangeOffhand, StateChanged.Source.Fixed);
|
|
weapon.Value = _jobChangeState.ModelData.Weapon(EquipSlot.OffHand);
|
|
}
|
|
|
|
_jobChangeState = null;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_jobChangeState = null;
|
|
}
|
|
}
|
|
|
|
private void OnAutomationChange(AutomationChanged.Type type, AutoDesignSet? set, object? bonusData)
|
|
{
|
|
if (!_config.EnableAutoDesigns || set == null)
|
|
return;
|
|
|
|
void RemoveOld(ActorIdentifier[]? identifiers)
|
|
{
|
|
if (identifiers == null)
|
|
return;
|
|
|
|
foreach (var id in identifiers)
|
|
{
|
|
if (_state.TryGetValue(id, out var state))
|
|
state.RemoveFixedDesignSources();
|
|
}
|
|
}
|
|
|
|
void ApplyNew(AutoDesignSet? newSet)
|
|
{
|
|
if (newSet is not { Enabled: true })
|
|
return;
|
|
|
|
_objects.Update();
|
|
foreach (var id in newSet.Identifiers)
|
|
{
|
|
if (_objects.TryGetValue(id, out var data))
|
|
{
|
|
if (_state.GetOrCreate(id, data.Objects[0], out var state))
|
|
{
|
|
Reduce(data.Objects[0], state, newSet, false, false);
|
|
foreach (var actor in data.Objects)
|
|
_state.ReapplyState(actor);
|
|
}
|
|
}
|
|
else if (_objects.TryGetValueAllWorld(id, out data) || _objects.TryGetValueNonOwned(id, out data))
|
|
{
|
|
foreach (var actor in data.Objects)
|
|
{
|
|
var specificId = actor.GetIdentifier(_actors.AwaitedService);
|
|
if (_state.GetOrCreate(specificId, actor, out var state))
|
|
{
|
|
Reduce(actor, state, newSet, false, false);
|
|
_state.ReapplyState(actor);
|
|
}
|
|
}
|
|
}
|
|
else if (_state.TryGetValue(id, out var state))
|
|
{
|
|
state.RemoveFixedDesignSources();
|
|
}
|
|
}
|
|
}
|
|
|
|
switch (type)
|
|
{
|
|
case AutomationChanged.Type.ToggleSet when !set.Enabled:
|
|
case AutomationChanged.Type.DeletedDesign when set.Enabled:
|
|
// The automation set was disabled or deleted, no other for those identifiers can be enabled, remove existing Fixed Locks.
|
|
RemoveOld(set.Identifiers);
|
|
break;
|
|
case AutomationChanged.Type.ChangeIdentifier when set.Enabled:
|
|
// Remove fixed state from the old identifiers assigned and the old enabled set, if any.
|
|
var (oldIds, _, oldSet) = ((ActorIdentifier[], ActorIdentifier, AutoDesignSet?))bonusData!;
|
|
RemoveOld(oldIds);
|
|
ApplyNew(set); // Does not need to disable oldSet because same identifiers.
|
|
break;
|
|
case AutomationChanged.Type.ToggleSet: // Does not need to disable old states because same identifiers.
|
|
case AutomationChanged.Type.ChangedBase:
|
|
case AutomationChanged.Type.AddedDesign:
|
|
case AutomationChanged.Type.MovedDesign:
|
|
case AutomationChanged.Type.ChangedDesign:
|
|
case AutomationChanged.Type.ChangedConditions:
|
|
case AutomationChanged.Type.ChangedType:
|
|
ApplyNew(set);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void OnJobChange(Actor actor, Job oldJob, Job newJob)
|
|
{
|
|
if (!_config.EnableAutoDesigns || !actor.Identifier(_actors.AwaitedService, out var id))
|
|
return;
|
|
|
|
if (!GetPlayerSet(id, out var set))
|
|
{
|
|
if (_state.TryGetValue(id, out var s))
|
|
s.LastJob = (byte)newJob.Id;
|
|
return;
|
|
}
|
|
|
|
if (!_state.TryGetValue(id, out var state))
|
|
return;
|
|
|
|
if (oldJob.Id == newJob.Id && state.LastJob == newJob.Id)
|
|
return;
|
|
|
|
var respectManual = state.LastJob == newJob.Id;
|
|
state.LastJob = actor.Job;
|
|
Reduce(actor, state, set, respectManual, true);
|
|
_state.ReapplyState(actor);
|
|
}
|
|
|
|
public void ReapplyAutomation(Actor actor, ActorIdentifier identifier, ActorState state)
|
|
{
|
|
if (!_config.EnableAutoDesigns)
|
|
return;
|
|
|
|
if (!GetPlayerSet(identifier, out var set))
|
|
return;
|
|
|
|
Reduce(actor, state, set, false, false);
|
|
}
|
|
|
|
public bool Reduce(Actor actor, ActorIdentifier identifier, [NotNullWhen(true)] out ActorState? state)
|
|
{
|
|
AutoDesignSet set;
|
|
if (!_state.TryGetValue(identifier, out state))
|
|
{
|
|
if (!_config.EnableAutoDesigns)
|
|
return false;
|
|
|
|
if (!GetPlayerSet(identifier, out set!))
|
|
return false;
|
|
|
|
if (!_state.GetOrCreate(identifier, actor, out state))
|
|
return false;
|
|
}
|
|
else if (!GetPlayerSet(identifier, out set!))
|
|
{
|
|
if (state.UpdateTerritory(_clientState.TerritoryType) && _config.RevertManualChangesOnZoneChange)
|
|
_state.ResetState(state, StateChanged.Source.Game);
|
|
return true;
|
|
}
|
|
|
|
var respectManual = !state.UpdateTerritory(_clientState.TerritoryType) || !_config.RevertManualChangesOnZoneChange;
|
|
if (!respectManual)
|
|
_state.ResetState(state, StateChanged.Source.Game);
|
|
Reduce(actor, state, set, respectManual, false);
|
|
return true;
|
|
}
|
|
|
|
private unsafe void Reduce(Actor actor, ActorState state, AutoDesignSet set, bool respectManual, bool fromJobChange)
|
|
{
|
|
EquipFlag totalEquipFlags = 0;
|
|
CustomizeFlag totalCustomizeFlags = 0;
|
|
byte totalMetaFlags = 0;
|
|
if (set.BaseState == AutoDesignSet.Base.Game)
|
|
_state.ResetStateFixed(state);
|
|
else if (!respectManual)
|
|
state.RemoveFixedDesignSources();
|
|
|
|
if (!_humans.IsHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId))
|
|
return;
|
|
|
|
foreach (var design in set.Designs)
|
|
{
|
|
if (!design.IsActive(actor))
|
|
continue;
|
|
|
|
if (design.ApplicationType is 0)
|
|
continue;
|
|
|
|
ref var data = ref design.GetDesignData(state);
|
|
var source = design.Revert ? StateChanged.Source.Game : StateChanged.Source.Fixed;
|
|
|
|
if (!data.IsHuman)
|
|
continue;
|
|
|
|
var (equipFlags, customizeFlags, applyHat, applyVisor, applyWeapon, applyWet) = design.ApplyWhat();
|
|
ReduceMeta(state, data, applyHat, applyVisor, applyWeapon, applyWet, ref totalMetaFlags, respectManual, source);
|
|
ReduceCustomize(state, data, customizeFlags, ref totalCustomizeFlags, respectManual, source);
|
|
ReduceEquip(state, data, equipFlags, ref totalEquipFlags, respectManual, source, fromJobChange);
|
|
}
|
|
|
|
if (totalCustomizeFlags != 0)
|
|
state.ModelData.ModelId = 0;
|
|
}
|
|
|
|
/// <summary> Get world-specific first and all-world afterwards. </summary>
|
|
private bool GetPlayerSet(ActorIdentifier identifier, [NotNullWhen(true)] out AutoDesignSet? set)
|
|
{
|
|
switch (identifier.Type)
|
|
{
|
|
case IdentifierType.Player:
|
|
if (_manager.EnabledSets.TryGetValue(identifier, out set))
|
|
return true;
|
|
|
|
identifier = _actors.AwaitedService.CreatePlayer(identifier.PlayerName, ushort.MaxValue);
|
|
return _manager.EnabledSets.TryGetValue(identifier, out set);
|
|
case IdentifierType.Retainer:
|
|
case IdentifierType.Npc:
|
|
return _manager.EnabledSets.TryGetValue(identifier, out set);
|
|
case IdentifierType.Owned:
|
|
identifier = _actors.AwaitedService.CreateNpc(identifier.Kind, identifier.DataId);
|
|
return _manager.EnabledSets.TryGetValue(identifier, out set);
|
|
default:
|
|
set = null;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void ReduceEquip(ActorState state, in DesignData design, EquipFlag equipFlags, ref EquipFlag totalEquipFlags, bool respectManual,
|
|
StateChanged.Source source, bool fromJobChange)
|
|
{
|
|
equipFlags &= ~totalEquipFlags;
|
|
if (equipFlags == 0)
|
|
return;
|
|
|
|
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
|
{
|
|
var flag = slot.ToFlag();
|
|
if (equipFlags.HasFlag(flag))
|
|
{
|
|
var item = design.Item(slot);
|
|
if (!_config.UnlockedItemMode || _itemUnlocks.IsUnlocked(item.Id, out _))
|
|
{
|
|
if (!respectManual || state[slot, false] is not StateChanged.Source.Manual)
|
|
_state.ChangeItem(state, slot, item, source);
|
|
totalEquipFlags |= flag;
|
|
}
|
|
}
|
|
|
|
var stainFlag = slot.ToStainFlag();
|
|
if (equipFlags.HasFlag(stainFlag))
|
|
{
|
|
if (!respectManual || state[slot, true] is not StateChanged.Source.Manual)
|
|
_state.ChangeStain(state, slot, design.Stain(slot), source);
|
|
totalEquipFlags |= stainFlag;
|
|
}
|
|
}
|
|
|
|
if (equipFlags.HasFlag(EquipFlag.Mainhand))
|
|
{
|
|
var item = design.Item(EquipSlot.MainHand);
|
|
var checkUnlock = !_config.UnlockedItemMode || _itemUnlocks.IsUnlocked(item.Id, out _);
|
|
var checkState = !respectManual || state[EquipSlot.MainHand, false] is not StateChanged.Source.Manual;
|
|
if (checkUnlock && checkState)
|
|
{
|
|
if (state.ModelData.Item(EquipSlot.MainHand).Type == item.Type)
|
|
{
|
|
_state.ChangeItem(state, EquipSlot.MainHand, item, source);
|
|
totalEquipFlags |= EquipFlag.Mainhand;
|
|
}
|
|
else if (fromJobChange)
|
|
{
|
|
_jobChangeMainhand = item;
|
|
_jobChangeState = state;
|
|
totalEquipFlags |= EquipFlag.Mainhand;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (equipFlags.HasFlag(EquipFlag.Offhand))
|
|
{
|
|
var item = design.Item(EquipSlot.OffHand);
|
|
var checkUnlock = !_config.UnlockedItemMode || _itemUnlocks.IsUnlocked(item.Id, out _);
|
|
var checkState = !respectManual || state[EquipSlot.OffHand, false] is not StateChanged.Source.Manual;
|
|
if (checkUnlock && checkState)
|
|
{
|
|
if (state.ModelData.Item(EquipSlot.OffHand).Type == item.Type)
|
|
{
|
|
_state.ChangeItem(state, EquipSlot.OffHand, item, source);
|
|
totalEquipFlags |= EquipFlag.Mainhand;
|
|
}
|
|
else if (fromJobChange)
|
|
{
|
|
_jobChangeOffhand = item;
|
|
_jobChangeState = state;
|
|
totalEquipFlags |= EquipFlag.Mainhand;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (equipFlags.HasFlag(EquipFlag.MainhandStain))
|
|
{
|
|
if (!respectManual || state[EquipSlot.MainHand, true] is not StateChanged.Source.Manual)
|
|
_state.ChangeStain(state, EquipSlot.MainHand, design.Stain(EquipSlot.MainHand), source);
|
|
totalEquipFlags |= EquipFlag.MainhandStain;
|
|
}
|
|
|
|
if (equipFlags.HasFlag(EquipFlag.OffhandStain))
|
|
{
|
|
if (!respectManual || state[EquipSlot.OffHand, true] is not StateChanged.Source.Manual)
|
|
_state.ChangeStain(state, EquipSlot.OffHand, design.Stain(EquipSlot.OffHand), source);
|
|
totalEquipFlags |= EquipFlag.OffhandStain;
|
|
}
|
|
}
|
|
|
|
private void ReduceCustomize(ActorState state, in DesignData design, CustomizeFlag customizeFlags, ref CustomizeFlag totalCustomizeFlags,
|
|
bool respectManual, StateChanged.Source source)
|
|
{
|
|
customizeFlags &= ~totalCustomizeFlags;
|
|
if (customizeFlags == 0)
|
|
return;
|
|
|
|
var customize = state.ModelData.Customize;
|
|
CustomizeFlag fixFlags = 0;
|
|
|
|
// Skip anything not human.
|
|
if (!state.ModelData.IsHuman || !design.IsHuman)
|
|
return;
|
|
|
|
if (customizeFlags.HasFlag(CustomizeFlag.Clan))
|
|
{
|
|
if (!respectManual || state[CustomizeIndex.Clan] is not StateChanged.Source.Manual)
|
|
fixFlags |= _customizations.ChangeClan(ref customize, design.Customize.Clan);
|
|
customizeFlags &= ~(CustomizeFlag.Clan | CustomizeFlag.Race);
|
|
totalCustomizeFlags |= CustomizeFlag.Clan | CustomizeFlag.Race;
|
|
}
|
|
|
|
if (customizeFlags.HasFlag(CustomizeFlag.Gender))
|
|
{
|
|
if (!respectManual || state[CustomizeIndex.Gender] is not StateChanged.Source.Manual)
|
|
fixFlags |= _customizations.ChangeGender(ref customize, design.Customize.Gender);
|
|
customizeFlags &= ~CustomizeFlag.Gender;
|
|
totalCustomizeFlags |= CustomizeFlag.Gender;
|
|
}
|
|
|
|
if (fixFlags != 0)
|
|
_state.ChangeCustomize(state, customize, fixFlags, source);
|
|
|
|
if (customizeFlags.HasFlag(CustomizeFlag.Face))
|
|
{
|
|
if (!respectManual || state[CustomizeIndex.Face] is not StateChanged.Source.Manual)
|
|
_state.ChangeCustomize(state, CustomizeIndex.Face, design.Customize.Face, source);
|
|
customizeFlags &= ~CustomizeFlag.Face;
|
|
totalCustomizeFlags |= CustomizeFlag.Face;
|
|
}
|
|
|
|
var set = _customizations.AwaitedService.GetList(state.ModelData.Customize.Clan, state.ModelData.Customize.Gender);
|
|
var face = state.ModelData.Customize.Face;
|
|
foreach (var index in Enum.GetValues<CustomizeIndex>())
|
|
{
|
|
var flag = index.ToFlag();
|
|
if (!customizeFlags.HasFlag(flag))
|
|
continue;
|
|
|
|
var value = design.Customize[index];
|
|
if (CustomizationService.IsCustomizationValid(set, face, index, value, out var data))
|
|
{
|
|
if (data.HasValue && _config.UnlockedItemMode && !_customizeUnlocks.IsUnlocked(data.Value, out _))
|
|
continue;
|
|
|
|
if (!respectManual || state[index] is not StateChanged.Source.Manual)
|
|
_state.ChangeCustomize(state, index, value, source);
|
|
totalCustomizeFlags |= flag;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ReduceMeta(ActorState state, in DesignData design, bool applyHat, bool applyVisor, bool applyWeapon, bool applyWet,
|
|
ref byte totalMetaFlags, bool respectManual, StateChanged.Source source)
|
|
{
|
|
if (applyHat && (totalMetaFlags & 0x01) == 0)
|
|
{
|
|
if (!respectManual || state[ActorState.MetaIndex.HatState] is not StateChanged.Source.Manual)
|
|
_state.ChangeHatState(state, design.IsHatVisible(), source);
|
|
totalMetaFlags |= 0x01;
|
|
}
|
|
|
|
if (applyVisor && (totalMetaFlags & 0x02) == 0)
|
|
{
|
|
if (!respectManual || state[ActorState.MetaIndex.VisorState] is not StateChanged.Source.Manual)
|
|
_state.ChangeVisorState(state, design.IsVisorToggled(), source);
|
|
totalMetaFlags |= 0x02;
|
|
}
|
|
|
|
if (applyWeapon && (totalMetaFlags & 0x04) == 0)
|
|
{
|
|
if (!respectManual || state[ActorState.MetaIndex.WeaponState] is not StateChanged.Source.Manual)
|
|
_state.ChangeWeaponState(state, design.IsWeaponVisible(), source);
|
|
totalMetaFlags |= 0x04;
|
|
}
|
|
|
|
if (applyWet && (totalMetaFlags & 0x08) == 0)
|
|
{
|
|
if (!respectManual || state[ActorState.MetaIndex.Wetness] is not StateChanged.Source.Manual)
|
|
_state.ChangeWetness(state, design.IsWet(), source);
|
|
totalMetaFlags |= 0x08;
|
|
}
|
|
}
|
|
}
|