Glamourer/Glamourer/Automation/AutoDesignApplier.cs
Diorik 87016419c5 Add "Reset Design" command
A new command to reapply automation while resetting the random design regardless of settings.
2025-02-04 00:46:12 -06:00

372 lines
15 KiB
C#

using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using Glamourer.Designs;
using Glamourer.Designs.Links;
using Glamourer.Events;
using Glamourer.Interop;
using Glamourer.Interop.Material;
using Glamourer.State;
using Penumbra.GameData.Actors;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs;
using ObjectManager = Glamourer.Interop.ObjectManager;
namespace Glamourer.Automation;
public sealed class AutoDesignApplier : IDisposable
{
private readonly Configuration _config;
private readonly AutoDesignManager _manager;
private readonly StateManager _state;
private readonly JobService _jobs;
private readonly EquippedGearset _equippedGearset;
private readonly ActorManager _actors;
private readonly AutomationChanged _event;
private readonly ObjectManager _objects;
private readonly WeaponLoading _weapons;
private readonly HumanModelList _humans;
private readonly DesignMerger _designMerger;
private readonly IClientState _clientState;
private readonly JobChangeState _jobChangeState;
public AutoDesignApplier(Configuration config, AutoDesignManager manager, StateManager state, JobService jobs, ActorManager actors,
AutomationChanged @event, ObjectManager objects, WeaponLoading weapons, HumanModelList humans, IClientState clientState,
EquippedGearset equippedGearset, DesignMerger designMerger, JobChangeState jobChangeState)
{
_config = config;
_manager = manager;
_state = state;
_jobs = jobs;
_actors = actors;
_event = @event;
_objects = objects;
_weapons = weapons;
_humans = humans;
_clientState = clientState;
_equippedGearset = equippedGearset;
_designMerger = designMerger;
_jobChangeState = jobChangeState;
_jobs.JobChanged += OnJobChange;
_event.Subscribe(OnAutomationChange, AutomationChanged.Priority.AutoDesignApplier);
_weapons.Subscribe(OnWeaponLoading, WeaponLoading.Priority.AutoDesignApplier);
_equippedGearset.Subscribe(OnEquippedGearset, EquippedGearset.Priority.AutoDesignApplier);
}
public void OnEnableAutoDesignsChanged(bool value)
{
if (value)
return;
foreach (var state in _state.Values)
state.Sources.RemoveFixedDesignSources();
}
public void Dispose()
{
_weapons.Unsubscribe(OnWeaponLoading);
_event.Unsubscribe(OnAutomationChange);
_equippedGearset.Unsubscribe(OnEquippedGearset);
_jobs.JobChanged -= OnJobChange;
}
private void OnWeaponLoading(Actor actor, EquipSlot slot, ref CharacterWeapon weapon)
{
if (!_jobChangeState.HasState || !_config.EnableAutoDesigns)
return;
var id = actor.GetIdentifier(_actors);
if (id == _jobChangeState.Identifier)
{
var state = _jobChangeState.State!;
var current = state.BaseData.Item(slot);
switch (slot)
{
case EquipSlot.MainHand:
{
if (_jobChangeState.TryGetValue(current.Type, actor.Job, false, out var data))
{
Glamourer.Log.Verbose(
$"Changing Mainhand from {state.ModelData.Weapon(EquipSlot.MainHand)} | {state.BaseData.Weapon(EquipSlot.MainHand)} to {data.Item1} for 0x{actor.Address:X}.");
_state.ChangeItem(state, EquipSlot.MainHand, data.Item1, new ApplySettings(Source: data.Item2));
weapon = state.ModelData.Weapon(EquipSlot.MainHand);
}
break;
}
case EquipSlot.OffHand when current.Type == state.BaseData.MainhandType.Offhand():
{
if (_jobChangeState.TryGetValue(current.Type, actor.Job, false, out var data))
{
Glamourer.Log.Verbose(
$"Changing Offhand from {state.ModelData.Weapon(EquipSlot.OffHand)} | {state.BaseData.Weapon(EquipSlot.OffHand)} to {data.Item1} for 0x{actor.Address:X}.");
_state.ChangeItem(state, EquipSlot.OffHand, data.Item1, new ApplySettings(Source: data.Item2));
weapon = state.ModelData.Weapon(EquipSlot.OffHand);
}
_jobChangeState.Reset();
break;
}
}
}
else
{
_jobChangeState.Reset();
}
}
private void OnAutomationChange(AutomationChanged.Type type, AutoDesignSet? set, object? bonusData)
{
if (!_config.EnableAutoDesigns || set == null)
return;
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, _, _) = ((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:
case AutomationChanged.Type.ChangedData:
ApplyNew(set);
break;
}
return;
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, _config.RespectManualOnAutomationUpdate, false, true, out var forcedRedraw);
foreach (var actor in data.Objects)
_state.ReapplyAutomationState(actor, forcedRedraw, false, StateSource.Fixed);
}
}
else if (_objects.TryGetValueAllWorld(id, out data) || _objects.TryGetValueNonOwned(id, out data))
{
foreach (var actor in data.Objects)
{
var specificId = actor.GetIdentifier(_actors);
if (_state.GetOrCreate(specificId, actor, out var state))
{
Reduce(actor, state, newSet, _config.RespectManualOnAutomationUpdate, false, true, out var forcedRedraw);
_state.ReapplyAutomationState(actor, forcedRedraw, false, StateSource.Fixed);
}
}
}
else if (_state.TryGetValue(id, out var state))
{
state.Sources.RemoveFixedDesignSources();
}
}
}
void RemoveOld(ActorIdentifier[]? identifiers)
{
if (identifiers == null)
return;
foreach (var id in identifiers)
{
if (id.Type is IdentifierType.Player && id.HomeWorld == WorldId.AnyWorld)
foreach (var state in _state.Where(kvp => kvp.Key.PlayerName == id.PlayerName).Select(kvp => kvp.Value))
state.Sources.RemoveFixedDesignSources();
else if (_state.TryGetValue(id, out var state))
state.Sources.RemoveFixedDesignSources();
}
}
}
private void OnJobChange(Actor actor, Job oldJob, Job newJob)
{
if (!_config.EnableAutoDesigns || !actor.Identifier(_actors, out var id))
return;
if (!GetPlayerSet(id, out var set))
{
if (_state.TryGetValue(id, out var s))
s.LastJob = newJob.Id;
return;
}
if (!_state.GetOrCreate(actor, 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, true, out var forcedRedraw);
_state.ReapplyState(actor, forcedRedraw, StateSource.Fixed);
}
public void ReapplyAutomation(Actor actor, ActorIdentifier identifier, ActorState state, bool reset, bool forcedNew, out bool forcedRedraw)
{
forcedRedraw = false;
if (!_config.EnableAutoDesigns)
return;
if (reset)
_state.ResetState(state, StateSource.Game);
if (GetPlayerSet(identifier, out var set))
Reduce(actor, state, set, false, false, forcedNew, out forcedRedraw);
}
public bool Reduce(Actor actor, ActorIdentifier identifier, [NotNullWhen(true)] out ActorState? state)
{
AutoDesignSet set;
if (!_state.TryGetValue(identifier, out state))
{
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, StateSource.Game);
return true;
}
var respectManual = !state.UpdateTerritory(_clientState.TerritoryType) || !_config.RevertManualChangesOnZoneChange;
if (!respectManual)
_state.ResetState(state, StateSource.Game);
Reduce(actor, state, set, respectManual, false, false, out _);
return true;
}
private unsafe void Reduce(Actor actor, ActorState state, AutoDesignSet set, bool respectManual, bool fromJobChange, bool newApplication,
out bool forcedRedraw)
{
if (set.BaseState is AutoDesignSet.Base.Game)
{
_state.ResetStateFixed(state, respectManual);
}
else if (!respectManual)
{
state.Sources.RemoveFixedDesignSources();
for (var i = 0; i < state.Materials.Values.Count; ++i)
{
var (key, value) = state.Materials.Values[i];
if (value.Source is StateSource.Fixed)
state.Materials.UpdateValue(key, new MaterialValueState(value.Game, value.Model, value.DrawData, StateSource.Manual),
out _);
}
}
forcedRedraw = false;
if (!_humans.IsHuman((uint)actor.AsCharacter->ModelContainer.ModelCharaId))
return;
if (actor.IsTransformed)
return;
var mergedDesign = _designMerger.Merge(
set.Designs.Where(d => d.IsActive(actor))
.SelectMany(d => d.Design.AllLinks(newApplication).Select(l => (l.Design, l.Flags & d.Type, d.Jobs.Flags))),
state.ModelData.Customize, state.BaseData, true, _config.AlwaysApplyAssociatedMods);
if (set.ResetTemporarySettings)
mergedDesign.ResetTemporarySettings = true;
_state.ApplyDesign(state, mergedDesign, new ApplySettings(0, StateSource.Fixed, respectManual, fromJobChange, false, false, false));
forcedRedraw = mergedDesign.ForcedRedraw;
}
/// <summary> Get world-specific first and all-world afterward. </summary>
private bool GetPlayerSet(ActorIdentifier identifier, [NotNullWhen(true)] out AutoDesignSet? set)
{
if (!_config.EnableAutoDesigns)
{
set = null;
return false;
}
switch (identifier.Type)
{
case IdentifierType.Player:
if (_manager.EnabledSets.TryGetValue(identifier, out set))
return true;
identifier = _actors.CreatePlayer(identifier.PlayerName, WorldId.AnyWorld);
return _manager.EnabledSets.TryGetValue(identifier, out set);
case IdentifierType.Retainer:
case IdentifierType.Npc:
return _manager.EnabledSets.TryGetValue(identifier, out set);
case IdentifierType.Owned:
if (_manager.EnabledSets.TryGetValue(identifier, out set))
return true;
identifier = _actors.CreateOwned(identifier.PlayerName, WorldId.AnyWorld, identifier.Kind, identifier.DataId);
if (_manager.EnabledSets.TryGetValue(identifier, out set))
return true;
identifier = _actors.CreateNpc(identifier.Kind, identifier.DataId);
return _manager.EnabledSets.TryGetValue(identifier, out set);
default:
set = null;
return false;
}
}
internal static int NewGearsetId = -1;
private void OnEquippedGearset(string name, int id, int prior, byte _, byte job)
{
if (!_config.EnableAutoDesigns)
return;
var (player, data) = _objects.PlayerData;
if (!player.IsValid)
return;
if (!GetPlayerSet(player, out var set) || !_state.TryGetValue(player, out var state))
return;
var respectManual = prior == id;
NewGearsetId = id;
Reduce(data.Objects[0], state, set, respectManual, job != state.LastJob, prior == id, out var forcedRedraw);
NewGearsetId = -1;
foreach (var actor in data.Objects)
_state.ReapplyState(actor, forcedRedraw, StateSource.Fixed);
}
public static unsafe bool CheckGearset(short check)
{
if (NewGearsetId != -1)
return check == NewGearsetId;
var module = RaptureGearsetModule.Instance();
if (module == null)
return false;
return check == module->CurrentGearsetIndex;
}
}