mirror of
https://github.com/Ottermandias/Glamourer.git
synced 2025-12-12 10:17:23 +01:00
.
This commit is contained in:
parent
63e82d19dc
commit
e57538561f
34 changed files with 2428 additions and 720 deletions
|
|
@ -4,6 +4,7 @@ using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Dalamud;
|
using Dalamud;
|
||||||
using Dalamud.Data;
|
using Dalamud.Data;
|
||||||
|
using Dalamud.Logging;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
using Lumina.Excel;
|
using Lumina.Excel;
|
||||||
|
|
@ -68,7 +69,7 @@ public partial class CustomizationOptions
|
||||||
{
|
{
|
||||||
var tmp = new TemporaryData(gameData, this);
|
var tmp = new TemporaryData(gameData, this);
|
||||||
_icons = new IconStorage(pi, gameData, _customizationSets.Length * 50);
|
_icons = new IconStorage(pi, gameData, _customizationSets.Length * 50);
|
||||||
_valid = tmp.Valid;
|
_valid = tmp.Valid;
|
||||||
SetNames(gameData, tmp);
|
SetNames(gameData, tmp);
|
||||||
foreach (var race in Clans)
|
foreach (var race in Clans)
|
||||||
{
|
{
|
||||||
|
|
@ -415,7 +416,7 @@ public partial class CustomizationOptions
|
||||||
// Obtain available hairstyles via reflection from the Hair sheet for the given subrace and gender.
|
// Obtain available hairstyles via reflection from the Hair sheet for the given subrace and gender.
|
||||||
private CustomizeData[] GetHairStyles(SubRace race, Gender gender)
|
private CustomizeData[] GetHairStyles(SubRace race, Gender gender)
|
||||||
{
|
{
|
||||||
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
|
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
|
||||||
// Unknown30 is the number of available hairstyles.
|
// Unknown30 is the number of available hairstyles.
|
||||||
var hairList = new List<CustomizeData>(row.Unknown30);
|
var hairList = new List<CustomizeData>(row.Unknown30);
|
||||||
// Hairstyles can be found starting at Unknown66.
|
// Hairstyles can be found starting at Unknown66.
|
||||||
|
|
@ -435,7 +436,8 @@ public partial class CustomizationOptions
|
||||||
}
|
}
|
||||||
else if (_options._icons.IconExists(hairRow.Icon))
|
else if (_options._icons.IconExists(hairRow.Icon))
|
||||||
{
|
{
|
||||||
hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)hairRow.FeatureID, hairRow.Icon, (ushort)hairRow.RowId));
|
hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)hairRow.FeatureID, hairRow.Icon,
|
||||||
|
(ushort)hairRow.RowId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -462,9 +464,8 @@ public partial class CustomizationOptions
|
||||||
// Get face paints from the hair sheet via reflection.
|
// Get face paints from the hair sheet via reflection.
|
||||||
private CustomizeData[] GetFacePaints(SubRace race, Gender gender)
|
private CustomizeData[] GetFacePaints(SubRace race, Gender gender)
|
||||||
{
|
{
|
||||||
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
|
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
|
||||||
var paintList = new List<CustomizeData>(row.Unknown37);
|
var paintList = new List<CustomizeData>(row.Unknown37);
|
||||||
|
|
||||||
// Number of available face paints is at Unknown37.
|
// Number of available face paints is at Unknown37.
|
||||||
for (var i = 0; i < row.Unknown37; ++i)
|
for (var i = 0; i < row.Unknown37; ++i)
|
||||||
{
|
{
|
||||||
|
|
@ -478,12 +479,14 @@ public partial class CustomizationOptions
|
||||||
|
|
||||||
var paintRow = _customizeSheet.GetRow(customizeIdx);
|
var paintRow = _customizeSheet.GetRow(customizeIdx);
|
||||||
// Facepaint Row from CustomizeSheet might not be set in case of unlockable facepaints.
|
// Facepaint Row from CustomizeSheet might not be set in case of unlockable facepaints.
|
||||||
paintList.Add(paintRow != null
|
if (paintRow != null)
|
||||||
? new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)paintRow.FeatureID, paintRow.Icon,
|
{
|
||||||
(ushort)paintRow.RowId)
|
paintList.Add(new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)paintRow.FeatureID, paintRow.Icon,
|
||||||
: new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)i, customizeIdx));
|
(ushort)paintRow.RowId));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
paintList.Add(new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)i, customizeIdx));
|
||||||
}
|
}
|
||||||
|
|
||||||
return paintList.ToArray();
|
return paintList.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,9 @@ public unsafe struct Customize
|
||||||
public Penumbra.GameData.Structs.CustomizeData Data;
|
public Penumbra.GameData.Structs.CustomizeData Data;
|
||||||
|
|
||||||
public Customize(in Penumbra.GameData.Structs.CustomizeData data)
|
public Customize(in Penumbra.GameData.Structs.CustomizeData data)
|
||||||
=> Data = data;
|
{
|
||||||
|
Data = data.Clone();
|
||||||
|
}
|
||||||
|
|
||||||
public Race Race
|
public Race Race
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ public enum CustomizeFlag : ulong
|
||||||
public static class CustomizeFlagExtensions
|
public static class CustomizeFlagExtensions
|
||||||
{
|
{
|
||||||
public const CustomizeFlag All = (CustomizeFlag)(((ulong)CustomizeFlag.FacePaintColor << 1) - 1ul);
|
public const CustomizeFlag All = (CustomizeFlag)(((ulong)CustomizeFlag.FacePaintColor << 1) - 1ul);
|
||||||
public const CustomizeFlag RedrawRequired = CustomizeFlag.Race | CustomizeFlag.Clan | CustomizeFlag.Gender | CustomizeFlag.Face;
|
public const CustomizeFlag RedrawRequired = CustomizeFlag.Race | CustomizeFlag.Clan | CustomizeFlag.Gender | CustomizeFlag.Face | CustomizeFlag.BodyType;
|
||||||
|
|
||||||
public static bool RequiresRedraw(this CustomizeFlag flags)
|
public static bool RequiresRedraw(this CustomizeFlag flags)
|
||||||
=> (flags & RedrawRequired) != 0;
|
=> (flags & RedrawRequired) != 0;
|
||||||
|
|
|
||||||
78
Glamourer/Automation/AutoDesign.cs
Normal file
78
Glamourer/Automation/AutoDesign.cs
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
using System;
|
||||||
|
using Glamourer.Customization;
|
||||||
|
using Glamourer.Designs;
|
||||||
|
using Glamourer.Interop.Structs;
|
||||||
|
using Glamourer.Structs;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace Glamourer.Automation;
|
||||||
|
|
||||||
|
public class AutoDesign
|
||||||
|
{
|
||||||
|
[Flags]
|
||||||
|
public enum Type : uint
|
||||||
|
{
|
||||||
|
Armor = 0x01,
|
||||||
|
Customizations = 0x02,
|
||||||
|
Meta = 0x04,
|
||||||
|
Weapons = 0x08,
|
||||||
|
Stains = 0x10,
|
||||||
|
Accessories = 0x20,
|
||||||
|
|
||||||
|
All = Armor | Accessories | Customizations | Meta | Weapons | Stains,
|
||||||
|
}
|
||||||
|
|
||||||
|
public Design Design;
|
||||||
|
public JobGroup Jobs;
|
||||||
|
public Type ApplicationType;
|
||||||
|
|
||||||
|
public unsafe bool IsActive(Actor actor)
|
||||||
|
=> actor.IsCharacter && Jobs.Fits(actor.AsCharacter->CharacterData.ClassJob);
|
||||||
|
|
||||||
|
public JObject Serialize()
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
["Design"] = Design.Identifier.ToString(),
|
||||||
|
["ApplicationType"] = (uint)ApplicationType,
|
||||||
|
["Conditions"] = CreateConditionObject(),
|
||||||
|
};
|
||||||
|
|
||||||
|
private JObject CreateConditionObject()
|
||||||
|
{
|
||||||
|
var ret = new JObject();
|
||||||
|
if (Jobs.Id != 0)
|
||||||
|
ret["JobGroup"] = Jobs.Id;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public (EquipFlag Equip, CustomizeFlag Customize, bool ApplyHat, bool ApplyVisor, bool ApplyWeapon, bool ApplyWet) ApplyWhat()
|
||||||
|
{
|
||||||
|
var equipFlags = (ApplicationType.HasFlag(Type.Weapons) ? WeaponFlags : 0)
|
||||||
|
| (ApplicationType.HasFlag(Type.Armor) ? ArmorFlags : 0)
|
||||||
|
| (ApplicationType.HasFlag(Type.Accessories) ? AccessoryFlags : 0)
|
||||||
|
| (ApplicationType.HasFlag(Type.Stains) ? StainFlags : 0);
|
||||||
|
var customizeFlags = ApplicationType.HasFlag(Type.Customizations) ? CustomizeFlagExtensions.All : 0;
|
||||||
|
return (equipFlags & Design.ApplyEquip, customizeFlags & Design.ApplyCustomize,
|
||||||
|
ApplicationType.HasFlag(Type.Armor) && Design.DoApplyHatVisible(),
|
||||||
|
ApplicationType.HasFlag(Type.Armor) && Design.DoApplyVisorToggle(),
|
||||||
|
ApplicationType.HasFlag(Type.Weapons) && Design.DoApplyWeaponVisible(),
|
||||||
|
ApplicationType.HasFlag(Type.Customizations) && Design.DoApplyWetness());
|
||||||
|
}
|
||||||
|
|
||||||
|
public const EquipFlag WeaponFlags = EquipFlag.Mainhand | EquipFlag.Offhand;
|
||||||
|
public const EquipFlag ArmorFlags = EquipFlag.Head | EquipFlag.Body | EquipFlag.Hands | EquipFlag.Legs | EquipFlag.Feet;
|
||||||
|
public const EquipFlag AccessoryFlags = EquipFlag.Ears | EquipFlag.Neck | EquipFlag.Wrist | EquipFlag.RFinger | EquipFlag.LFinger;
|
||||||
|
|
||||||
|
public const EquipFlag StainFlags = EquipFlag.MainhandStain
|
||||||
|
| EquipFlag.OffhandStain
|
||||||
|
| EquipFlag.HeadStain
|
||||||
|
| EquipFlag.BodyStain
|
||||||
|
| EquipFlag.HandsStain
|
||||||
|
| EquipFlag.LegsStain
|
||||||
|
| EquipFlag.FeetStain
|
||||||
|
| EquipFlag.EarsStain
|
||||||
|
| EquipFlag.NeckStain
|
||||||
|
| EquipFlag.WristStain
|
||||||
|
| EquipFlag.RFingerStain
|
||||||
|
| EquipFlag.LFingerStain;
|
||||||
|
}
|
||||||
214
Glamourer/Automation/AutoDesignApplier.cs
Normal file
214
Glamourer/Automation/AutoDesignApplier.cs
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
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 Penumbra.GameData.Actors;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
|
|
||||||
|
namespace Glamourer.Automation;
|
||||||
|
|
||||||
|
public class AutoDesignApplier : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Configuration _config;
|
||||||
|
private readonly AutoDesignManager _manager;
|
||||||
|
private readonly PhrasingService _phrasing;
|
||||||
|
private readonly StateManager _state;
|
||||||
|
private readonly JobService _jobs;
|
||||||
|
private readonly ActorService _actors;
|
||||||
|
private readonly CustomizationService _customizations;
|
||||||
|
|
||||||
|
public AutoDesignApplier(Configuration config, AutoDesignManager manager, PhrasingService phrasing, StateManager state, JobService jobs,
|
||||||
|
CustomizationService customizations, ActorService actors)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
_manager = manager;
|
||||||
|
_phrasing = phrasing;
|
||||||
|
_state = state;
|
||||||
|
_jobs = jobs;
|
||||||
|
_customizations = customizations;
|
||||||
|
_actors = actors;
|
||||||
|
_jobs.JobChanged += OnJobChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_jobs.JobChanged -= OnJobChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnJobChange(Actor actor, Job _)
|
||||||
|
{
|
||||||
|
if (!_config.EnableAutoDesigns || !actor.Identifier(_actors.AwaitedService, out var id))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_manager.EnabledSets.TryGetValue(id, out var set))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_state.GetOrCreate(id, actor, out var state))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Reduce(actor, state, set);
|
||||||
|
_state.ReapplyState(actor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reduce(Actor actor, ActorIdentifier identifier, ActorState state)
|
||||||
|
{
|
||||||
|
if (!_config.EnableAutoDesigns)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!GetPlayerSet(identifier, out var set))
|
||||||
|
return;
|
||||||
|
Reduce(actor, state, set);
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void Reduce(Actor actor, ActorState state, AutoDesignSet set)
|
||||||
|
{
|
||||||
|
EquipFlag totalEquipFlags = 0;
|
||||||
|
//var totalCustomizeFlags = _phrasing.Phrasing2 ? 0 : CustomizeFlagExtensions.RedrawRequired;
|
||||||
|
var totalCustomizeFlags = CustomizeFlagExtensions.RedrawRequired;
|
||||||
|
byte totalMetaFlags = 0;
|
||||||
|
foreach (var design in set.Designs)
|
||||||
|
{
|
||||||
|
if (!design.IsActive(actor))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (design.ApplicationType is 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (actor.AsCharacter->CharacterData.ModelCharaId != design.Design.DesignData.ModelId)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var (equipFlags, customizeFlags, applyHat, applyVisor, applyWeapon, applyWet) = design.ApplyWhat();
|
||||||
|
Reduce(state, in design.Design.DesignData, equipFlags, ref totalEquipFlags);
|
||||||
|
Reduce(state, in design.Design.DesignData, customizeFlags, ref totalCustomizeFlags);
|
||||||
|
Reduce(state, in design.Design.DesignData, applyHat, applyVisor, applyWeapon, applyWet, ref totalMetaFlags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Get world-specific first and all-world afterwards. </summary>
|
||||||
|
private bool GetPlayerSet(ActorIdentifier identifier, [NotNullWhen(true)] out AutoDesignSet? set)
|
||||||
|
{
|
||||||
|
if (identifier.Type is not IdentifierType.Player)
|
||||||
|
return _manager.EnabledSets.TryGetValue(identifier, out set);
|
||||||
|
|
||||||
|
if (_manager.EnabledSets.TryGetValue(identifier, out set))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
identifier = _actors.AwaitedService.CreatePlayer(identifier.PlayerName, ushort.MaxValue);
|
||||||
|
return _manager.EnabledSets.TryGetValue(identifier, out set);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Reduce(ActorState state, in DesignData design, EquipFlag equipFlags, ref EquipFlag totalEquipFlags)
|
||||||
|
{
|
||||||
|
equipFlags &= ~totalEquipFlags;
|
||||||
|
if (equipFlags == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// TODO add item conditions
|
||||||
|
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
||||||
|
{
|
||||||
|
var flag = slot.ToFlag();
|
||||||
|
if (equipFlags.HasFlag(flag))
|
||||||
|
{
|
||||||
|
_state.ChangeItem(state, slot, design.Item(slot), StateChanged.Source.Fixed);
|
||||||
|
totalEquipFlags |= flag;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stainFlag = slot.ToStainFlag();
|
||||||
|
if (equipFlags.HasFlag(stainFlag))
|
||||||
|
{
|
||||||
|
_state.ChangeStain(state, slot, design.Stain(slot), StateChanged.Source.Fixed);
|
||||||
|
totalEquipFlags |= stainFlag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (equipFlags.HasFlag(EquipFlag.Mainhand))
|
||||||
|
{
|
||||||
|
var item = design.Item(EquipSlot.MainHand);
|
||||||
|
if (state.ModelData.Item(EquipSlot.MainHand).Type == item.Type)
|
||||||
|
{
|
||||||
|
_state.ChangeItem(state, EquipSlot.MainHand, item, StateChanged.Source.Fixed);
|
||||||
|
totalEquipFlags |= EquipFlag.Mainhand;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (equipFlags.HasFlag(EquipFlag.Offhand))
|
||||||
|
{
|
||||||
|
var item = design.Item(EquipSlot.OffHand);
|
||||||
|
if (state.ModelData.Item(EquipSlot.OffHand).Type == item.Type)
|
||||||
|
{
|
||||||
|
_state.ChangeItem(state, EquipSlot.OffHand, item, StateChanged.Source.Fixed);
|
||||||
|
totalEquipFlags |= EquipFlag.Offhand;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (equipFlags.HasFlag(EquipFlag.MainhandStain))
|
||||||
|
{
|
||||||
|
_state.ChangeStain(state, EquipSlot.MainHand, design.Stain(EquipSlot.MainHand), StateChanged.Source.Fixed);
|
||||||
|
totalEquipFlags |= EquipFlag.MainhandStain;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (equipFlags.HasFlag(EquipFlag.OffhandStain))
|
||||||
|
{
|
||||||
|
_state.ChangeStain(state, EquipSlot.OffHand, design.Stain(EquipSlot.OffHand), StateChanged.Source.Fixed);
|
||||||
|
totalEquipFlags |= EquipFlag.OffhandStain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Reduce(ActorState state, in DesignData design, CustomizeFlag customizeFlags, ref CustomizeFlag totalCustomizeFlags)
|
||||||
|
{
|
||||||
|
customizeFlags &= ~totalCustomizeFlags;
|
||||||
|
if (customizeFlags == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// TODO add race/gender handling
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
_state.ChangeCustomize(state, index, value, StateChanged.Source.Fixed);
|
||||||
|
totalCustomizeFlags |= flag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Reduce(ActorState state, in DesignData design, bool applyHat, bool applyVisor, bool applyWeapon, bool applyWet,
|
||||||
|
ref byte totalMetaFlags)
|
||||||
|
{
|
||||||
|
if (applyHat && (totalMetaFlags & 0x01) == 0)
|
||||||
|
{
|
||||||
|
_state.ChangeHatState(state, design.IsHatVisible(), StateChanged.Source.Fixed);
|
||||||
|
totalMetaFlags |= 0x01;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (applyVisor && (totalMetaFlags & 0x02) == 0)
|
||||||
|
{
|
||||||
|
_state.ChangeVisorState(state, design.IsVisorToggled(), StateChanged.Source.Fixed);
|
||||||
|
totalMetaFlags |= 0x02;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (applyWeapon && (totalMetaFlags & 0x04) == 0)
|
||||||
|
{
|
||||||
|
_state.ChangeWeaponState(state, design.IsWeaponVisible(), StateChanged.Source.Fixed);
|
||||||
|
totalMetaFlags |= 0x04;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (applyWet && (totalMetaFlags & 0x08) == 0)
|
||||||
|
{
|
||||||
|
_state.ChangeWetness(state, design.IsWet(), StateChanged.Source.Fixed);
|
||||||
|
totalMetaFlags |= 0x08;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
422
Glamourer/Automation/AutoDesignManager.cs
Normal file
422
Glamourer/Automation/AutoDesignManager.cs
Normal file
|
|
@ -0,0 +1,422 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Dalamud.Interface.Internal.Notifications;
|
||||||
|
using Dalamud.Utility;
|
||||||
|
using Glamourer.Designs;
|
||||||
|
using Glamourer.Events;
|
||||||
|
using Glamourer.Interop;
|
||||||
|
using Glamourer.Services;
|
||||||
|
using Glamourer.Structs;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using OtterGui.Filesystem;
|
||||||
|
using Penumbra.GameData.Actors;
|
||||||
|
|
||||||
|
namespace Glamourer.Automation;
|
||||||
|
|
||||||
|
public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>
|
||||||
|
{
|
||||||
|
public const int CurrentVersion = 1;
|
||||||
|
|
||||||
|
private readonly SaveService _saveService;
|
||||||
|
|
||||||
|
private readonly JobService _jobs;
|
||||||
|
private readonly DesignManager _designs;
|
||||||
|
private readonly ActorService _actors;
|
||||||
|
private readonly AutomationChanged _event;
|
||||||
|
|
||||||
|
private readonly List<AutoDesignSet> _data = new();
|
||||||
|
private readonly Dictionary<ActorIdentifier, AutoDesignSet> _enabled = new();
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<ActorIdentifier, AutoDesignSet> EnabledSets
|
||||||
|
=> _enabled;
|
||||||
|
|
||||||
|
public AutoDesignManager(JobService jobs, ActorService actors, SaveService saveService, DesignManager designs, AutomationChanged @event,
|
||||||
|
FixedDesignMigrator migrator, DesignFileSystem fileSystem)
|
||||||
|
{
|
||||||
|
_jobs = jobs;
|
||||||
|
_actors = actors;
|
||||||
|
_saveService = saveService;
|
||||||
|
_designs = designs;
|
||||||
|
_event = @event;
|
||||||
|
Load();
|
||||||
|
migrator.ConsumeMigratedData(_actors, fileSystem, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<AutoDesignSet> GetEnumerator()
|
||||||
|
=> _data.GetEnumerator();
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
=> GetEnumerator();
|
||||||
|
|
||||||
|
public int Count
|
||||||
|
=> _data.Count;
|
||||||
|
|
||||||
|
public AutoDesignSet this[int index]
|
||||||
|
=> _data[index];
|
||||||
|
|
||||||
|
public void AddDesignSet(string name, ActorIdentifier identifier)
|
||||||
|
{
|
||||||
|
if (!IdentifierValid(identifier) || name.Length == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var newSet = new AutoDesignSet(name, identifier.CreatePermanent()) { Enabled = false };
|
||||||
|
_data.Add(newSet);
|
||||||
|
Save();
|
||||||
|
Glamourer.Log.Debug($"Created new design set for {identifier.Incognito(null)}.");
|
||||||
|
_event.Invoke(AutomationChanged.Type.AddedSet, newSet, (_data.Count - 1, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteDesignSet(int whichSet)
|
||||||
|
{
|
||||||
|
if (whichSet >= _data.Count || whichSet < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var set = _data[whichSet];
|
||||||
|
if (set.Enabled)
|
||||||
|
{
|
||||||
|
set.Enabled = false;
|
||||||
|
_enabled.Remove(set.Identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
Save();
|
||||||
|
Glamourer.Log.Debug($"Deleted design set {whichSet + 1}.");
|
||||||
|
_event.Invoke(AutomationChanged.Type.DeletedSet, set, whichSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Rename(int whichSet, string newName)
|
||||||
|
{
|
||||||
|
if (whichSet >= _data.Count || whichSet < 0 || newName.Length == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var set = _data[whichSet];
|
||||||
|
if (set.Name == newName)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var old = set.Name;
|
||||||
|
set.Name = newName;
|
||||||
|
Save();
|
||||||
|
Glamourer.Log.Debug($"Renamed design set {whichSet + 1} from {old} to {newName}.");
|
||||||
|
_event.Invoke(AutomationChanged.Type.RenamedSet, set, (old, newName));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void MoveSet(int whichSet, int toWhichSet)
|
||||||
|
{
|
||||||
|
if (!_data.Move(whichSet, toWhichSet))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Save();
|
||||||
|
Glamourer.Log.Debug($"Moved design set {whichSet + 1} to position {toWhichSet + 1}.");
|
||||||
|
_event.Invoke(AutomationChanged.Type.MovedSet, _data[toWhichSet], (whichSet, toWhichSet));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeIdentifier(int whichSet, ActorIdentifier to)
|
||||||
|
{
|
||||||
|
if (whichSet >= _data.Count || whichSet < 0 || !IdentifierValid(to))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var set = _data[whichSet];
|
||||||
|
if (set.Identifier == to)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var old = set.Identifier;
|
||||||
|
set.Identifier = to.CreatePermanent();
|
||||||
|
AutoDesignSet? oldEnabled = null;
|
||||||
|
if (set.Enabled)
|
||||||
|
{
|
||||||
|
_enabled.Remove(old);
|
||||||
|
if (_enabled.Remove(to, out oldEnabled))
|
||||||
|
oldEnabled.Enabled = false;
|
||||||
|
_enabled.Add(set.Identifier, set);
|
||||||
|
}
|
||||||
|
|
||||||
|
Save();
|
||||||
|
Glamourer.Log.Debug($"Changed Identifier of design set {whichSet + 1} from {old.Incognito(null)} to {to.Incognito(null)}.");
|
||||||
|
_event.Invoke(AutomationChanged.Type.ChangeIdentifier, set, (old, to, oldEnabled));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetState(int whichSet, bool value)
|
||||||
|
{
|
||||||
|
if (whichSet >= _data.Count || whichSet < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var set = _data[whichSet];
|
||||||
|
if (set.Enabled == value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
AutoDesignSet? oldEnabled = null;
|
||||||
|
if (value)
|
||||||
|
{
|
||||||
|
if (_enabled.Remove(set.Identifier, out oldEnabled))
|
||||||
|
oldEnabled.Enabled = false;
|
||||||
|
_enabled.Add(set.Identifier, set);
|
||||||
|
}
|
||||||
|
|
||||||
|
Save();
|
||||||
|
Glamourer.Log.Debug($"Changed enabled state of design set {whichSet + 1} to {value}.");
|
||||||
|
_event.Invoke(AutomationChanged.Type.ToggleSet, set, oldEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddDesign(AutoDesignSet set, Design design)
|
||||||
|
{
|
||||||
|
var newDesign = new AutoDesign()
|
||||||
|
{
|
||||||
|
Design = design,
|
||||||
|
ApplicationType = AutoDesign.Type.All,
|
||||||
|
Jobs = _jobs.JobGroups[1],
|
||||||
|
};
|
||||||
|
set.Designs.Add(newDesign);
|
||||||
|
Save();
|
||||||
|
Glamourer.Log.Debug($"Added new associated design {design.Identifier} as design {set.Designs.Count} to design set.");
|
||||||
|
_event.Invoke(AutomationChanged.Type.AddedDesign, set, set.Designs.Count - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteDesign(AutoDesignSet set, int which)
|
||||||
|
{
|
||||||
|
if (which >= set.Designs.Count || which < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
set.Designs.RemoveAt(which);
|
||||||
|
Save();
|
||||||
|
Glamourer.Log.Debug($"Removed associated design {which + 1} from design set.");
|
||||||
|
_event.Invoke(AutomationChanged.Type.DeletedDesign, set, which);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MoveDesign(AutoDesignSet set, int from, int to)
|
||||||
|
{
|
||||||
|
if (!set.Designs.Move(from, to))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Save();
|
||||||
|
Glamourer.Log.Debug($"Moved design {from + 1} to {to + 1} in design set.");
|
||||||
|
_event.Invoke(AutomationChanged.Type.MovedDesign, set, (from, to));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeDesign(AutoDesignSet set, int which, Design newDesign)
|
||||||
|
{
|
||||||
|
if (which >= set.Designs.Count || which < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var design = set.Designs[which];
|
||||||
|
if (design.Design.Identifier == newDesign.Identifier)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var old = design.Design;
|
||||||
|
design.Design = newDesign;
|
||||||
|
Save();
|
||||||
|
Glamourer.Log.Debug(
|
||||||
|
$"Changed linked design from {old.Identifier} to {newDesign.Identifier} for associated design {which + 1} in design set.");
|
||||||
|
_event.Invoke(AutomationChanged.Type.ChangedDesign, set, (which, old, newDesign));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeJobCondition(AutoDesignSet set, int which, JobGroup jobs)
|
||||||
|
{
|
||||||
|
if (which >= set.Designs.Count || which < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var design = set.Designs[which];
|
||||||
|
if (design.Jobs.Id == jobs.Id)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var old = design.Jobs;
|
||||||
|
design.Jobs = jobs;
|
||||||
|
Save();
|
||||||
|
Glamourer.Log.Debug($"Changed job condition from {old.Id} to {jobs.Id} for associated design {which + 1} in design set.");
|
||||||
|
_event.Invoke(AutomationChanged.Type.ChangedConditions, set, (which, old, jobs));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeApplicationType(AutoDesignSet set, int which, AutoDesign.Type type)
|
||||||
|
{
|
||||||
|
if (which >= set.Designs.Count || which < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
type &= AutoDesign.Type.All;
|
||||||
|
var design = set.Designs[which];
|
||||||
|
if (design.ApplicationType == type)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var old = design.ApplicationType;
|
||||||
|
design.ApplicationType = type;
|
||||||
|
Save();
|
||||||
|
Glamourer.Log.Debug($"Changed application type from {old} to {type} for associated design {which + 1} in design set.");
|
||||||
|
_event.Invoke(AutomationChanged.Type.ChangedType, set, (which, old, type));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToFilename(FilenameService fileNames)
|
||||||
|
=> fileNames.AutomationFile;
|
||||||
|
|
||||||
|
public void Save(StreamWriter writer)
|
||||||
|
{
|
||||||
|
using var j = new JsonTextWriter(writer)
|
||||||
|
{
|
||||||
|
Formatting = Formatting.Indented,
|
||||||
|
};
|
||||||
|
Serialize().WriteTo(j);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JObject Serialize()
|
||||||
|
{
|
||||||
|
var array = new JArray();
|
||||||
|
foreach (var set in _data)
|
||||||
|
array.Add(set.Serialize());
|
||||||
|
|
||||||
|
return new JObject()
|
||||||
|
{
|
||||||
|
["Version"] = CurrentVersion,
|
||||||
|
["Data"] = array,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Load()
|
||||||
|
{
|
||||||
|
var file = _saveService.FileNames.AutomationFile;
|
||||||
|
_data.Clear();
|
||||||
|
if (!File.Exists(file))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var text = File.ReadAllText(file);
|
||||||
|
var obj = JObject.Parse(text);
|
||||||
|
var version = obj["Version"]?.ToObject<int>() ?? 0;
|
||||||
|
|
||||||
|
switch (version)
|
||||||
|
{
|
||||||
|
case < 1:
|
||||||
|
case > CurrentVersion:
|
||||||
|
Glamourer.Chat.NotificationMessage("Failure to load automated designs: No valid version available.", "Error",
|
||||||
|
NotificationType.Error);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
LoadV1(obj["Data"]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Glamourer.Chat.NotificationMessage(ex, "Failure to load automated designs: Error during parsing.",
|
||||||
|
"Failure to load automated designs", "Error", NotificationType.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadV1(JToken? data)
|
||||||
|
{
|
||||||
|
if (data is not JArray array)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var obj in array)
|
||||||
|
{
|
||||||
|
var name = obj["Name"]?.ToObject<string>() ?? string.Empty;
|
||||||
|
if (name.Length == 0)
|
||||||
|
{
|
||||||
|
Glamourer.Chat.NotificationMessage("Skipped loading Automation Set: No name provided.", "Warning", NotificationType.Warning);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = _actors.AwaitedService.FromJson(obj["Identifier"] as JObject);
|
||||||
|
if (!IdentifierValid(id))
|
||||||
|
{
|
||||||
|
Glamourer.Chat.NotificationMessage("Skipped loading Automation Set: Invalid Identifier.", "Warning", NotificationType.Warning);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var set = new AutoDesignSet(name, id)
|
||||||
|
{
|
||||||
|
Enabled = obj["Enabled"]?.ToObject<bool>() ?? false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (set.Enabled)
|
||||||
|
if (!_enabled.TryAdd(set.Identifier, set))
|
||||||
|
set.Enabled = false;
|
||||||
|
|
||||||
|
_data.Add(set);
|
||||||
|
|
||||||
|
if (obj["Designs"] is not JArray designArray)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (var designObj in designArray)
|
||||||
|
{
|
||||||
|
if (designObj is not JObject j)
|
||||||
|
{
|
||||||
|
Glamourer.Chat.NotificationMessage($"Skipped loading design in Automation Set for {set.Identifier}: Unknown design.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var design = ToDesignObject(j);
|
||||||
|
if (design != null)
|
||||||
|
set.Designs.Add(design);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AutoDesign? ToDesignObject(JObject jObj)
|
||||||
|
{
|
||||||
|
var designIdentifier = jObj["Design"]?.ToObject<string>();
|
||||||
|
if (designIdentifier.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
Glamourer.Chat.NotificationMessage("Error parsing automatically applied design: No design specified.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Guid.TryParse(designIdentifier, out var guid))
|
||||||
|
{
|
||||||
|
Glamourer.Chat.NotificationMessage($"Error parsing automatically applied design: {designIdentifier} is not a valid GUID.");
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var design = _designs.Designs.FirstOrDefault(d => d.Identifier == guid);
|
||||||
|
if (design == null)
|
||||||
|
{
|
||||||
|
Glamourer.Chat.NotificationMessage($"Error parsing automatically applied design: The specified design {guid} does not exist.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var applicationType = (AutoDesign.Type)(jObj["ApplicationType"]?.ToObject<uint>() ?? 0);
|
||||||
|
|
||||||
|
|
||||||
|
var ret = new AutoDesign()
|
||||||
|
{
|
||||||
|
Design = design,
|
||||||
|
ApplicationType = applicationType & AutoDesign.Type.All,
|
||||||
|
};
|
||||||
|
|
||||||
|
var conditions = jObj["Conditions"];
|
||||||
|
if (conditions == null)
|
||||||
|
return ret;
|
||||||
|
|
||||||
|
var jobs = conditions["JobGroup"]?.ToObject<int>() ?? -1;
|
||||||
|
if (jobs >= 0)
|
||||||
|
{
|
||||||
|
if (!_jobs.JobGroups.TryGetValue((ushort)jobs, out var jobGroup))
|
||||||
|
{
|
||||||
|
Glamourer.Chat.NotificationMessage($"Error parsing automatically applied design: The job condition {jobs} does not exist.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.Jobs = jobGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Save()
|
||||||
|
=> _saveService.DelaySave(this);
|
||||||
|
|
||||||
|
private static bool IdentifierValid(ActorIdentifier identifier)
|
||||||
|
{
|
||||||
|
if (!identifier.IsValid)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return identifier.Type switch
|
||||||
|
{
|
||||||
|
IdentifierType.Player => true,
|
||||||
|
IdentifierType.Retainer => true,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
40
Glamourer/Automation/AutoDesignSet.cs
Normal file
40
Glamourer/Automation/AutoDesignSet.cs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Penumbra.GameData.Actors;
|
||||||
|
|
||||||
|
namespace Glamourer.Automation;
|
||||||
|
|
||||||
|
public class AutoDesignSet
|
||||||
|
{
|
||||||
|
public readonly List<AutoDesign> Designs;
|
||||||
|
|
||||||
|
public string Name;
|
||||||
|
public ActorIdentifier Identifier;
|
||||||
|
public bool Enabled;
|
||||||
|
|
||||||
|
public JObject Serialize()
|
||||||
|
{
|
||||||
|
var list = new JArray();
|
||||||
|
foreach (var design in Designs)
|
||||||
|
list.Add(design.Serialize());
|
||||||
|
|
||||||
|
return new JObject()
|
||||||
|
{
|
||||||
|
["Name"] = Name,
|
||||||
|
["Identifier"] = Identifier.ToJson(),
|
||||||
|
["Enabled"] = Enabled,
|
||||||
|
["Designs"] = list,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public AutoDesignSet(string name, ActorIdentifier identifier)
|
||||||
|
: this(name, identifier, new List<AutoDesign>())
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public AutoDesignSet(string name, ActorIdentifier identifier, List<AutoDesign> designs)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
Identifier = identifier;
|
||||||
|
Designs = designs;
|
||||||
|
}
|
||||||
|
}
|
||||||
111
Glamourer/Automation/FixedDesignMigrator.cs
Normal file
111
Glamourer/Automation/FixedDesignMigrator.cs
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Dalamud.Interface.Internal.Notifications;
|
||||||
|
using Glamourer.Designs;
|
||||||
|
using Glamourer.Interop;
|
||||||
|
using Glamourer.Services;
|
||||||
|
using Glamourer.Structs;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Penumbra.GameData.Actors;
|
||||||
|
using Penumbra.String;
|
||||||
|
|
||||||
|
namespace Glamourer.Automation;
|
||||||
|
|
||||||
|
public class FixedDesignMigrator
|
||||||
|
{
|
||||||
|
private readonly JobService _jobs;
|
||||||
|
private Dictionary<string, (bool, List<(string, JobGroup)>)>? _migratedData;
|
||||||
|
|
||||||
|
public FixedDesignMigrator(JobService jobs)
|
||||||
|
=> _jobs = jobs;
|
||||||
|
|
||||||
|
public void ConsumeMigratedData(ActorService actors, DesignFileSystem designFileSystem, AutoDesignManager autoManager)
|
||||||
|
{
|
||||||
|
if (_migratedData == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var data in _migratedData)
|
||||||
|
{
|
||||||
|
var enabled = data.Value.Item1;
|
||||||
|
var name = data.Key + (data.Value.Item1 ? " (Enabled)" : " (Disabled)");
|
||||||
|
if (autoManager.Any(d => name == data.Key))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var id = ActorIdentifier.Invalid;
|
||||||
|
if (ByteString.FromString(data.Key, out var byteString, false))
|
||||||
|
{
|
||||||
|
id = actors.AwaitedService.CreatePlayer(byteString, ushort.MaxValue);
|
||||||
|
if (!id.IsValid)
|
||||||
|
id = actors.AwaitedService.CreateRetainer(byteString, ActorIdentifier.RetainerType.Both);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!id.IsValid)
|
||||||
|
{
|
||||||
|
byteString = ByteString.FromSpanUnsafe("Mig Ration"u8, true, false, true);
|
||||||
|
id = actors.AwaitedService.CreatePlayer(byteString, actors.AwaitedService.Data.Worlds.First().Key);
|
||||||
|
enabled = false;
|
||||||
|
if (!id.IsValid)
|
||||||
|
{
|
||||||
|
Glamourer.Chat.NotificationMessage($"Could not migrate fixed design {data.Key}.", "Error", NotificationType.Error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
autoManager.AddDesignSet(name, id);
|
||||||
|
autoManager.SetState(autoManager.Count - 1, enabled);
|
||||||
|
var set = autoManager[^1];
|
||||||
|
foreach (var design in data.Value.Item2)
|
||||||
|
{
|
||||||
|
if (!designFileSystem.Find(design.Item1, out var child) || child is not DesignFileSystem.Leaf leaf)
|
||||||
|
{
|
||||||
|
Glamourer.Chat.NotificationMessage($"Could not find design with path {design.Item1}, skipped fixed design.", "Warning",
|
||||||
|
NotificationType.Warning);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
autoManager.AddDesign(set, leaf.Value);
|
||||||
|
autoManager.ChangeJobCondition(set, set.Designs.Count - 1, design.Item2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Migrate(JToken? data)
|
||||||
|
{
|
||||||
|
if (data is not JArray array)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var list = new List<(string Name, string Path, JobGroup Group, bool Enabled)>();
|
||||||
|
foreach (var obj in array)
|
||||||
|
{
|
||||||
|
var name = obj["Name"]?.ToObject<string>() ?? string.Empty;
|
||||||
|
if (name.Length == 0)
|
||||||
|
{
|
||||||
|
Glamourer.Chat.NotificationMessage("Could not semi-migrate fixed design: No character name available.", "Warning",
|
||||||
|
NotificationType.Warning);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = obj["Path"]?.ToObject<string>() ?? string.Empty;
|
||||||
|
if (path.Length == 0)
|
||||||
|
{
|
||||||
|
Glamourer.Chat.NotificationMessage("Could not semi-migrate fixed design: No design path available.", "Warning",
|
||||||
|
NotificationType.Warning);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var job = obj["JobGroups"]?.ToObject<int>() ?? -1;
|
||||||
|
if (job < 0 || !_jobs.JobGroups.TryGetValue((ushort)job, out var group))
|
||||||
|
{
|
||||||
|
Glamourer.Chat.NotificationMessage("Could not semi-migrate fixed design: Invalid job group specified.", "Warning",
|
||||||
|
NotificationType.Warning);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var enabled = obj["Enabled"]?.ToObject<bool>() ?? false;
|
||||||
|
list.Add((name, path, group, enabled));
|
||||||
|
}
|
||||||
|
|
||||||
|
_migratedData = list.GroupBy(t => (t.Name, t.Enabled))
|
||||||
|
.ToDictionary(kvp => kvp.Key.Name, kvp => (kvp.Key.Enabled, kvp.Select(k => (k.Path, k.Group)).ToList()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,7 @@ public class Configuration : IPluginConfiguration, ISavable
|
||||||
public bool UseRestrictedGearProtection { get; set; } = true;
|
public bool UseRestrictedGearProtection { get; set; } = true;
|
||||||
public bool OpenFoldersByDefault { get; set; } = false;
|
public bool OpenFoldersByDefault { get; set; } = false;
|
||||||
public bool AutoRedrawEquipOnChanges { get; set; } = false;
|
public bool AutoRedrawEquipOnChanges { get; set; } = false;
|
||||||
|
public bool EnableAutoDesigns { get; set; } = true;
|
||||||
public MainWindow.TabType SelectedTab { get; set; } = MainWindow.TabType.Settings;
|
public MainWindow.TabType SelectedTab { get; set; } = MainWindow.TabType.Settings;
|
||||||
public DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift);
|
public DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift);
|
||||||
|
|
||||||
|
|
@ -28,11 +29,13 @@ public class Configuration : IPluginConfiguration, ISavable
|
||||||
[JsonProperty(Order = int.MaxValue)]
|
[JsonProperty(Order = int.MaxValue)]
|
||||||
public ISortMode<Design> SortMode { get; set; } = ISortMode<Design>.FoldersFirst;
|
public ISortMode<Design> SortMode { get; set; } = ISortMode<Design>.FoldersFirst;
|
||||||
|
|
||||||
|
public string Phrasing1 { get; set; } = string.Empty;
|
||||||
|
public string Phrasing2 { get; set; } = string.Empty;
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
public bool DebugMode { get; set; } = true;
|
public bool DebugMode { get; set; } = true;
|
||||||
#else
|
#else
|
||||||
public bool DebugMode { get; set; } = false;
|
public bool DebugMode { get; set; } = false;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
public int Version { get; set; } = Constants.CurrentVersion;
|
public int Version { get; set; } = Constants.CurrentVersion;
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ public class Design : ISavable
|
||||||
|
|
||||||
#region Serialization
|
#region Serialization
|
||||||
|
|
||||||
public JObject JsonSerialize()
|
private JObject JsonSerialize()
|
||||||
{
|
{
|
||||||
var ret = new JObject
|
var ret = new JObject
|
||||||
{
|
{
|
||||||
|
|
@ -223,7 +223,7 @@ public class Design : ISavable
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public JObject SerializeEquipment()
|
private JObject SerializeEquipment()
|
||||||
{
|
{
|
||||||
static JObject Serialize(uint itemId, StainId stain, bool apply, bool applyStain)
|
static JObject Serialize(uint itemId, StainId stain, bool apply, bool applyStain)
|
||||||
=> new()
|
=> new()
|
||||||
|
|
@ -250,7 +250,7 @@ public class Design : ISavable
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public JObject SerializeCustomize()
|
private JObject SerializeCustomize()
|
||||||
{
|
{
|
||||||
var ret = new JObject()
|
var ret = new JObject()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -192,12 +192,12 @@ public class DesignManager
|
||||||
Glamourer.Log.Error("Somehow race or body type was changed in a design. This should not happen.");
|
Glamourer.Log.Error("Somehow race or body type was changed in a design. This should not happen.");
|
||||||
return;
|
return;
|
||||||
case CustomizeIndex.Clan:
|
case CustomizeIndex.Clan:
|
||||||
if (!_customizations.ChangeClan(ref design.DesignData.Customize, (SubRace)value.Value))
|
if (_customizations.ChangeClan(ref design.DesignData.Customize, (SubRace)value.Value) == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case CustomizeIndex.Gender:
|
case CustomizeIndex.Gender:
|
||||||
if (!_customizations.ChangeGender(ref design.DesignData.Customize, (Gender)(value.Value + 1)))
|
if (_customizations.ChangeGender(ref design.DesignData.Customize, (Gender)(value.Value + 1)) == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
66
Glamourer/Events/AutomationChanged.cs
Normal file
66
Glamourer/Events/AutomationChanged.cs
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
using System;
|
||||||
|
using Glamourer.Automation;
|
||||||
|
using OtterGui.Classes;
|
||||||
|
|
||||||
|
namespace Glamourer.Events;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Triggered when an automated design is changed in any way.
|
||||||
|
/// <list type="number">
|
||||||
|
/// <item>Parameter is the type of the change </item>
|
||||||
|
/// <item>Parameter is the added or changed design set or null on deletion. </item>
|
||||||
|
/// <item>Parameter is additional data depending on the type of change. </item>
|
||||||
|
/// </list>
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AutomationChanged : EventWrapper<Action<AutomationChanged.Type, AutoDesignSet?, object?>,
|
||||||
|
AutomationChanged.Priority>
|
||||||
|
{
|
||||||
|
public enum Type
|
||||||
|
{
|
||||||
|
/// <summary> Add a new set. Names and identifiers do not have to be unique. It is not enabled by default. Additional data is the index it gets added at and the name [(int, string)]. </summary>
|
||||||
|
AddedSet,
|
||||||
|
|
||||||
|
/// <summary> Delete a given set. Additional data is the index it got removed from [int].</summary>
|
||||||
|
DeletedSet,
|
||||||
|
|
||||||
|
/// <summary> Rename a given set. Names do not have to be unique. Additional data is the old name and the new name [(string, string)]. </summary>
|
||||||
|
RenamedSet,
|
||||||
|
|
||||||
|
/// <summary> Move a given set to a different position. Additional data is the old index of the set and the new index of the set [(int, int)]. </summary>
|
||||||
|
MovedSet,
|
||||||
|
|
||||||
|
/// <summary> Change the identifier a given set is associated with to another one. Additional data is the old identifier and the new one, and a potentially disabled other design set. [(ActorIdentifier, ActorIdentifier, AutoDesignSet?)]. </summary>
|
||||||
|
ChangeIdentifier,
|
||||||
|
|
||||||
|
/// <summary> Toggle the enabled state of a given set. Additional data is the thus disabled other set, if any [AutoDesignSet?]. </summary>
|
||||||
|
ToggleSet,
|
||||||
|
|
||||||
|
/// <summary> Add a new associated design to a given set. Additional data is the index it got added at [int]. </summary>
|
||||||
|
AddedDesign,
|
||||||
|
|
||||||
|
/// <summary> Remove a given associated design from a given set. Additional data is the index it got removed from [int]. </summary>
|
||||||
|
DeletedDesign,
|
||||||
|
|
||||||
|
/// <summary> Move a given associated design in the list of a given set. Additional data is the index that got moved and the index it got moved to [(int, int)]. </summary>
|
||||||
|
MovedDesign,
|
||||||
|
|
||||||
|
/// <summary> Change the linked design in an associated design for a given set. Additional data is the index of the changed associated design, the old linked design and the new linked design [(int, Design, Design)]. </summary>
|
||||||
|
ChangedDesign,
|
||||||
|
|
||||||
|
/// <summary> Change the job condition in an associated design for a given set. Additional data is the index of the changed associated design, the old job group and the new job group [(int, JobGroup, JobGroup)]. </summary>
|
||||||
|
ChangedConditions,
|
||||||
|
|
||||||
|
/// <summary> Change the application type in an associated design for a given set. Additional data is the index of the changed associated design, the old type and the new type. [(int, AutoDesign.Type, AutoDesign.Type)]. </summary>
|
||||||
|
ChangedType,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Priority
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public AutomationChanged()
|
||||||
|
: base(nameof(AutomationChanged))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public void Invoke(Type type, AutoDesignSet? set, object? data)
|
||||||
|
=> Invoke(this, type, set, data);
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ using Dalamud.Plugin;
|
||||||
using Glamourer.Gui;
|
using Glamourer.Gui;
|
||||||
using Glamourer.Interop;
|
using Glamourer.Interop;
|
||||||
using Glamourer.Services;
|
using Glamourer.Services;
|
||||||
|
using Lumina.Excel.GeneratedSheets;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
using OtterGui.Log;
|
using OtterGui.Log;
|
||||||
|
|
|
||||||
|
|
@ -24,20 +24,18 @@ public partial class CustomizationDrawer
|
||||||
|
|
||||||
private void DrawGenderSelector()
|
private void DrawGenderSelector()
|
||||||
{
|
{
|
||||||
using var font = ImRaii.PushFont(UiBuilder.IconFont);
|
|
||||||
var icon = _customize.Gender switch
|
var icon = _customize.Gender switch
|
||||||
{
|
{
|
||||||
Gender.Male when _customize.Race is Race.Hrothgar => FontAwesomeIcon.MarsDouble,
|
Gender.Male when _customize.Race is Race.Hrothgar => FontAwesomeIcon.MarsDouble,
|
||||||
Gender.Male => FontAwesomeIcon.Mars,
|
Gender.Male => FontAwesomeIcon.Mars,
|
||||||
Gender.Female => FontAwesomeIcon.Venus,
|
Gender.Female => FontAwesomeIcon.Venus,
|
||||||
|
_ => FontAwesomeIcon.Question,
|
||||||
_ => throw new Exception($"Gender value {_customize.Gender} is not a valid gender for a design."),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!ImGuiUtil.DrawDisabledButton(icon.ToIconString(), _framedIconSize, string.Empty, icon == FontAwesomeIcon.MarsDouble, true))
|
if (!ImGuiUtil.DrawDisabledButton(icon.ToIconString(), _framedIconSize, string.Empty, icon is not FontAwesomeIcon.Mars and not FontAwesomeIcon.Venus, true))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_service.ChangeGender(ref _customize, _customize.Gender is Gender.Male ? Gender.Female : Gender.Male);
|
Changed |= _service.ChangeGender(ref _customize, icon is FontAwesomeIcon.Mars ? Gender.Female : Gender.Male);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawRaceCombo()
|
private void DrawRaceCombo()
|
||||||
|
|
@ -50,7 +48,7 @@ public partial class CustomizationDrawer
|
||||||
foreach (var subRace in Enum.GetValues<SubRace>().Skip(1)) // Skip Unknown
|
foreach (var subRace in Enum.GetValues<SubRace>().Skip(1)) // Skip Unknown
|
||||||
{
|
{
|
||||||
if (ImGui.Selectable(_service.ClanName(subRace, _customize.Gender), subRace == _customize.Clan))
|
if (ImGui.Selectable(_service.ClanName(subRace, _customize.Gender), subRace == _customize.Clan))
|
||||||
_service.ChangeClan(ref _customize, subRace);
|
Changed |= _service.ChangeClan(ref _customize, subRace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@ public partial class CustomizationDrawer : IDisposable
|
||||||
private Customize _customize;
|
private Customize _customize;
|
||||||
private CustomizationSet _set = null!;
|
private CustomizationSet _set = null!;
|
||||||
|
|
||||||
public Customize Customize;
|
public Customize Customize
|
||||||
|
=> _customize;
|
||||||
|
|
||||||
public CustomizeFlag CurrentFlag { get; private set; }
|
public CustomizeFlag CurrentFlag { get; private set; }
|
||||||
public CustomizeFlag Changed { get; private set; }
|
public CustomizeFlag Changed { get; private set; }
|
||||||
|
|
@ -41,7 +42,7 @@ public partial class CustomizationDrawer : IDisposable
|
||||||
{
|
{
|
||||||
_service = service;
|
_service = service;
|
||||||
_legacyTattoo = GetLegacyTattooIcon(pi);
|
_legacyTattoo = GetLegacyTattooIcon(pi);
|
||||||
Customize = Customize.Default;
|
_customize = Customize.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
|
||||||
163
Glamourer/Gui/Equipment/EquipmentDrawer.cs
Normal file
163
Glamourer/Gui/Equipment/EquipmentDrawer.cs
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using Dalamud.Data;
|
||||||
|
using Dalamud.Interface;
|
||||||
|
using Glamourer.Designs;
|
||||||
|
using Glamourer.Services;
|
||||||
|
using ImGuiNET;
|
||||||
|
using OtterGui;
|
||||||
|
using OtterGui.Widgets;
|
||||||
|
using Penumbra.GameData.Data;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
|
using Penumbra.GameData.Structs;
|
||||||
|
|
||||||
|
namespace Glamourer.Gui.Equipment;
|
||||||
|
|
||||||
|
public class EquipmentDrawer
|
||||||
|
{
|
||||||
|
private readonly ItemManager _items;
|
||||||
|
private readonly FilterComboColors _stainCombo;
|
||||||
|
private readonly StainData _stainData;
|
||||||
|
private readonly ItemCombo[] _itemCombo;
|
||||||
|
private readonly Dictionary<FullEquipType, WeaponCombo> _weaponCombo;
|
||||||
|
|
||||||
|
public EquipmentDrawer(DataManager gameData, ItemManager items)
|
||||||
|
{
|
||||||
|
_items = items;
|
||||||
|
_stainData = items.Stains;
|
||||||
|
_stainCombo = new FilterComboColors(140,
|
||||||
|
_stainData.Data.Prepend(new KeyValuePair<byte, (string Name, uint Dye, bool Gloss)>(0, ("None", 0, false))));
|
||||||
|
_itemCombo = EquipSlotExtensions.EqdpSlots.Select(e => new ItemCombo(gameData, items, e)).ToArray();
|
||||||
|
_weaponCombo = new Dictionary<FullEquipType, WeaponCombo>(FullEquipTypeExtensions.WeaponTypes.Count * 2);
|
||||||
|
foreach (var type in Enum.GetValues<FullEquipType>())
|
||||||
|
{
|
||||||
|
if (type.ToSlot() is EquipSlot.MainHand)
|
||||||
|
_weaponCombo.TryAdd(type, new WeaponCombo(items, type));
|
||||||
|
else if (type.ToSlot() is EquipSlot.OffHand)
|
||||||
|
_weaponCombo.TryAdd(type, new WeaponCombo(items, type));
|
||||||
|
}
|
||||||
|
|
||||||
|
_weaponCombo.Add(FullEquipType.Unknown, new WeaponCombo(items, FullEquipType.Unknown));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string VerifyRestrictedGear(EquipItem gear, EquipSlot slot, Gender gender, Race race)
|
||||||
|
{
|
||||||
|
if (slot.IsAccessory())
|
||||||
|
return gear.Name;
|
||||||
|
|
||||||
|
var (changed, _) = _items.ResolveRestrictedGear(gear.Armor(), slot, race, gender);
|
||||||
|
if (changed)
|
||||||
|
return gear.Name + " (Restricted)";
|
||||||
|
|
||||||
|
return gear.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DrawArmor(EquipItem current, EquipSlot slot, out EquipItem armor, Gender gender = Gender.Unknown, Race race = Race.Unknown)
|
||||||
|
{
|
||||||
|
Debug.Assert(slot.IsEquipment() || slot.IsAccessory(), $"Called {nameof(DrawArmor)} on {slot}.");
|
||||||
|
var combo = _itemCombo[slot.ToIndex()];
|
||||||
|
armor = current;
|
||||||
|
var change = combo.Draw(VerifyRestrictedGear(armor, slot, gender, race), armor.Id, 320 * ImGuiHelpers.GlobalScale);
|
||||||
|
if (armor.ModelId.Value != 0)
|
||||||
|
{
|
||||||
|
ImGuiUtil.HoverTooltip("Right-click to clear.");
|
||||||
|
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
|
||||||
|
{
|
||||||
|
change = true;
|
||||||
|
armor = ItemManager.NothingItem(slot);
|
||||||
|
}
|
||||||
|
else if (change)
|
||||||
|
{
|
||||||
|
armor = combo.CurrentSelection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (change)
|
||||||
|
{
|
||||||
|
armor = combo.CurrentSelection;
|
||||||
|
}
|
||||||
|
|
||||||
|
return change;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DrawStain(StainId current, EquipSlot slot, out Stain stain)
|
||||||
|
{
|
||||||
|
var found = _stainData.TryGetValue(current, out stain);
|
||||||
|
var change = _stainCombo.Draw($"##stain{slot}", stain.RgbaColor, stain.Name, found);
|
||||||
|
ImGuiUtil.HoverTooltip("Right-click to clear.");
|
||||||
|
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
|
||||||
|
{
|
||||||
|
stain = Stain.None;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return change && _stainData.TryGetValue(_stainCombo.CurrentSelection.Key, out stain);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DrawMainhand(EquipItem current, bool drawAll, out EquipItem weapon)
|
||||||
|
{
|
||||||
|
weapon = current;
|
||||||
|
if (!_weaponCombo.TryGetValue(drawAll ? FullEquipType.Unknown : current.Type, out var combo))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!combo.Draw(weapon.Name, weapon.Id, 320 * ImGuiHelpers.GlobalScale))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
weapon = combo.CurrentSelection;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DrawOffhand(EquipItem current, FullEquipType mainType, out EquipItem weapon)
|
||||||
|
{
|
||||||
|
weapon = current;
|
||||||
|
var offType = mainType.Offhand();
|
||||||
|
if (offType == FullEquipType.Unknown)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!_weaponCombo.TryGetValue(offType, out var combo))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var change = combo.Draw(weapon.Name, weapon.Id, 320 * ImGuiHelpers.GlobalScale);
|
||||||
|
if (!offType.IsOffhandType() && weapon.ModelId.Value != 0)
|
||||||
|
{
|
||||||
|
ImGuiUtil.HoverTooltip("Right-click to clear.");
|
||||||
|
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
|
||||||
|
{
|
||||||
|
change = true;
|
||||||
|
weapon = ItemManager.NothingItem(offType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (change)
|
||||||
|
{
|
||||||
|
weapon = combo.CurrentSelection;
|
||||||
|
}
|
||||||
|
|
||||||
|
return change;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DrawApply(Design design, EquipSlot slot, out bool enabled)
|
||||||
|
=> DrawCheckbox($"##apply{slot}", design.DoApplyEquip(slot), out enabled);
|
||||||
|
|
||||||
|
public bool DrawApplyStain(Design design, EquipSlot slot, out bool enabled)
|
||||||
|
=> DrawCheckbox($"##applyStain{slot}", design.DoApplyStain(slot), out enabled);
|
||||||
|
|
||||||
|
private static bool DrawCheckbox(string label, bool value, out bool on)
|
||||||
|
{
|
||||||
|
var ret = ImGuiUtil.Checkbox(label, string.Empty, value, v => value = v);
|
||||||
|
on = value;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DrawVisor(bool current, out bool on)
|
||||||
|
=> DrawCheckbox("##visorToggled", current, out on);
|
||||||
|
|
||||||
|
public bool DrawHat(bool current, out bool on)
|
||||||
|
=> DrawCheckbox("##hatVisible", current, out on);
|
||||||
|
|
||||||
|
public bool DrawWeapon(bool current, out bool on)
|
||||||
|
=> DrawCheckbox("##weaponVisible", current, out on);
|
||||||
|
|
||||||
|
public bool DrawWetness(bool current, out bool on)
|
||||||
|
=> DrawCheckbox("##wetness", current, out on);
|
||||||
|
}
|
||||||
103
Glamourer/Gui/Equipment/ItemCombo.cs
Normal file
103
Glamourer/Gui/Equipment/ItemCombo.cs
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Dalamud.Data;
|
||||||
|
using Glamourer.Services;
|
||||||
|
using ImGuiNET;
|
||||||
|
using Lumina.Excel.GeneratedSheets;
|
||||||
|
using OtterGui;
|
||||||
|
using OtterGui.Classes;
|
||||||
|
using OtterGui.Raii;
|
||||||
|
using OtterGui.Widgets;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
|
using Penumbra.GameData.Structs;
|
||||||
|
|
||||||
|
namespace Glamourer.Gui.Equipment;
|
||||||
|
|
||||||
|
public sealed class ItemCombo : FilterComboCache<EquipItem>
|
||||||
|
{
|
||||||
|
public readonly string Label;
|
||||||
|
private uint _currentItem;
|
||||||
|
|
||||||
|
public ItemCombo(DataManager gameData, ItemManager items, EquipSlot slot)
|
||||||
|
: base(() => GetItems(items, slot))
|
||||||
|
{
|
||||||
|
Label = GetLabel(gameData, slot);
|
||||||
|
_currentItem = ItemManager.NothingId(slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void DrawList(float width, float itemHeight)
|
||||||
|
{
|
||||||
|
base.DrawList(width, itemHeight);
|
||||||
|
if (NewSelection != null && Items.Count > NewSelection.Value)
|
||||||
|
CurrentSelection = Items[NewSelection.Value];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override int UpdateCurrentSelected(int currentSelected)
|
||||||
|
{
|
||||||
|
if (CurrentSelection.Id == _currentItem)
|
||||||
|
return currentSelected;
|
||||||
|
|
||||||
|
CurrentSelectionIdx = Items.IndexOf(i => i.Id == _currentItem);
|
||||||
|
CurrentSelection = CurrentSelectionIdx >= 0 ? Items[CurrentSelectionIdx] : default;
|
||||||
|
return base.UpdateCurrentSelected(CurrentSelectionIdx);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Draw(string previewName, uint previewIdx, float width)
|
||||||
|
{
|
||||||
|
_currentItem = previewIdx;
|
||||||
|
return Draw(Label, previewName, string.Empty, width, ImGui.GetTextLineHeightWithSpacing());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool DrawSelectable(int globalIdx, bool selected)
|
||||||
|
{
|
||||||
|
var obj = Items[globalIdx];
|
||||||
|
var name = ToString(obj);
|
||||||
|
var ret = ImGui.Selectable(name, selected);
|
||||||
|
ImGui.SameLine();
|
||||||
|
using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF808080);
|
||||||
|
ImGuiUtil.RightAlign($"({obj.ModelId.Value}-{obj.Variant})");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsVisible(int globalIndex, LowerString filter)
|
||||||
|
=> base.IsVisible(globalIndex, filter) || filter.IsContained(Items[globalIndex].ModelId.Value.ToString());
|
||||||
|
|
||||||
|
protected override string ToString(EquipItem obj)
|
||||||
|
=> obj.Name;
|
||||||
|
|
||||||
|
private static string GetLabel(DataManager gameData, EquipSlot slot)
|
||||||
|
{
|
||||||
|
var sheet = gameData.GetExcelSheet<Addon>()!;
|
||||||
|
|
||||||
|
return slot switch
|
||||||
|
{
|
||||||
|
EquipSlot.Head => sheet.GetRow(740)?.Text.ToString() ?? "Head",
|
||||||
|
EquipSlot.Body => sheet.GetRow(741)?.Text.ToString() ?? "Body",
|
||||||
|
EquipSlot.Hands => sheet.GetRow(742)?.Text.ToString() ?? "Hands",
|
||||||
|
EquipSlot.Legs => sheet.GetRow(744)?.Text.ToString() ?? "Legs",
|
||||||
|
EquipSlot.Feet => sheet.GetRow(745)?.Text.ToString() ?? "Feet",
|
||||||
|
EquipSlot.Ears => sheet.GetRow(746)?.Text.ToString() ?? "Ears",
|
||||||
|
EquipSlot.Neck => sheet.GetRow(747)?.Text.ToString() ?? "Neck",
|
||||||
|
EquipSlot.Wrists => sheet.GetRow(748)?.Text.ToString() ?? "Wrists",
|
||||||
|
EquipSlot.RFinger => sheet.GetRow(749)?.Text.ToString() ?? "Right Ring",
|
||||||
|
EquipSlot.LFinger => sheet.GetRow(750)?.Text.ToString() ?? "Left Ring",
|
||||||
|
_ => string.Empty,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<EquipItem> GetItems(ItemManager items, EquipSlot slot)
|
||||||
|
{
|
||||||
|
var nothing = ItemManager.NothingItem(slot);
|
||||||
|
if (!items.ItemService.AwaitedService.TryGetValue(slot.ToEquipType(), out var list))
|
||||||
|
return new[]
|
||||||
|
{
|
||||||
|
nothing,
|
||||||
|
};
|
||||||
|
|
||||||
|
var enumerable = list.AsEnumerable();
|
||||||
|
if (slot.IsEquipment())
|
||||||
|
enumerable = enumerable.Append(ItemManager.SmallClothesItem(slot));
|
||||||
|
return enumerable.OrderBy(i => i.Name).Prepend(nothing).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
89
Glamourer/Gui/Equipment/WeaponCombo.cs
Normal file
89
Glamourer/Gui/Equipment/WeaponCombo.cs
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Glamourer.Services;
|
||||||
|
using ImGuiNET;
|
||||||
|
using OtterGui;
|
||||||
|
using OtterGui.Classes;
|
||||||
|
using OtterGui.Raii;
|
||||||
|
using OtterGui.Widgets;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
|
using Penumbra.GameData.Structs;
|
||||||
|
|
||||||
|
namespace Glamourer.Gui.Equipment;
|
||||||
|
|
||||||
|
public sealed class WeaponCombo : FilterComboCache<EquipItem>
|
||||||
|
{
|
||||||
|
public readonly string Label;
|
||||||
|
private uint _currentItemId;
|
||||||
|
|
||||||
|
public WeaponCombo(ItemManager items, FullEquipType type)
|
||||||
|
: base(() => GetWeapons(items, type))
|
||||||
|
=> Label = GetLabel(type);
|
||||||
|
|
||||||
|
protected override void DrawList(float width, float itemHeight)
|
||||||
|
{
|
||||||
|
base.DrawList(width, itemHeight);
|
||||||
|
if (NewSelection != null && Items.Count > NewSelection.Value)
|
||||||
|
CurrentSelection = Items[NewSelection.Value];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override int UpdateCurrentSelected(int currentSelected)
|
||||||
|
{
|
||||||
|
if (CurrentSelection.Id == _currentItemId)
|
||||||
|
return currentSelected;
|
||||||
|
|
||||||
|
CurrentSelectionIdx = Items.IndexOf(i => i.Id == _currentItemId);
|
||||||
|
CurrentSelection = CurrentSelectionIdx >= 0 ? Items[CurrentSelectionIdx] : default;
|
||||||
|
return base.UpdateCurrentSelected(CurrentSelectionIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Draw(string previewName, uint previewId, float width)
|
||||||
|
{
|
||||||
|
_currentItemId = previewId;
|
||||||
|
return Draw(Label, previewName, string.Empty, width, ImGui.GetTextLineHeightWithSpacing());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool DrawSelectable(int globalIdx, bool selected)
|
||||||
|
{
|
||||||
|
var obj = Items[globalIdx];
|
||||||
|
var name = ToString(obj);
|
||||||
|
var ret = ImGui.Selectable(name, selected);
|
||||||
|
ImGui.SameLine();
|
||||||
|
using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF808080);
|
||||||
|
ImGuiUtil.RightAlign($"({obj.ModelId.Value}-{obj.WeaponType.Value}-{obj.Variant})");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsVisible(int globalIndex, LowerString filter)
|
||||||
|
=> base.IsVisible(globalIndex, filter) || filter.IsContained(Items[globalIndex].ModelId.Value.ToString());
|
||||||
|
|
||||||
|
protected override string ToString(EquipItem obj)
|
||||||
|
=> obj.Name;
|
||||||
|
|
||||||
|
private static string GetLabel(FullEquipType type)
|
||||||
|
=> type is FullEquipType.Unknown ? "Mainhand" : type.ToName();
|
||||||
|
|
||||||
|
private static IReadOnlyList<EquipItem> GetWeapons(ItemManager items, FullEquipType type)
|
||||||
|
{
|
||||||
|
if (type is FullEquipType.Unknown)
|
||||||
|
{
|
||||||
|
var enumerable = Array.Empty<EquipItem>().AsEnumerable();
|
||||||
|
foreach (var t in Enum.GetValues<FullEquipType>().Where(e => e.ToSlot() is EquipSlot.MainHand))
|
||||||
|
{
|
||||||
|
if (items.ItemService.AwaitedService.TryGetValue(t, out var l))
|
||||||
|
enumerable = enumerable.Concat(l);
|
||||||
|
}
|
||||||
|
|
||||||
|
return enumerable.OrderBy(e => e.Name).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!items.ItemService.AwaitedService.TryGetValue(type, out var list))
|
||||||
|
return Array.Empty<EquipItem>();
|
||||||
|
|
||||||
|
if (type.ToSlot() is EquipSlot.OffHand && !type.IsOffhandType())
|
||||||
|
return list.OrderBy(e => e.Name).Prepend(ItemManager.NothingItem(type)).ToList();
|
||||||
|
|
||||||
|
return list.OrderBy(e => e.Name).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using Glamourer.Customization;
|
||||||
|
using Glamourer.Events;
|
||||||
using Glamourer.Gui.Customization;
|
using Glamourer.Gui.Customization;
|
||||||
|
using Glamourer.Gui.Equipment;
|
||||||
using Glamourer.Interop.Structs;
|
using Glamourer.Interop.Structs;
|
||||||
using Glamourer.State;
|
using Glamourer.State;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
|
|
@ -14,6 +17,7 @@ public class ActorPanel
|
||||||
private readonly ActorSelector _selector;
|
private readonly ActorSelector _selector;
|
||||||
private readonly StateManager _stateManager;
|
private readonly StateManager _stateManager;
|
||||||
private readonly CustomizationDrawer _customizationDrawer;
|
private readonly CustomizationDrawer _customizationDrawer;
|
||||||
|
private readonly EquipmentDrawer _equipmentDrawer;
|
||||||
|
|
||||||
private ActorIdentifier _identifier;
|
private ActorIdentifier _identifier;
|
||||||
private string _actorName = string.Empty;
|
private string _actorName = string.Empty;
|
||||||
|
|
@ -21,11 +25,13 @@ public class ActorPanel
|
||||||
private ActorData _data;
|
private ActorData _data;
|
||||||
private ActorState? _state;
|
private ActorState? _state;
|
||||||
|
|
||||||
public ActorPanel(ActorSelector selector, StateManager stateManager, CustomizationDrawer customizationDrawer)
|
public ActorPanel(ActorSelector selector, StateManager stateManager, CustomizationDrawer customizationDrawer,
|
||||||
|
EquipmentDrawer equipmentDrawer)
|
||||||
{
|
{
|
||||||
_selector = selector;
|
_selector = selector;
|
||||||
_stateManager = stateManager;
|
_stateManager = stateManager;
|
||||||
_customizationDrawer = customizationDrawer;
|
_customizationDrawer = customizationDrawer;
|
||||||
|
_equipmentDrawer = equipmentDrawer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Draw()
|
public void Draw()
|
||||||
|
|
@ -76,46 +82,40 @@ public class ActorPanel
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (_customizationDrawer.Draw(_state.ModelData.Customize, false))
|
if (_customizationDrawer.Draw(_state.ModelData.Customize, false))
|
||||||
|
_stateManager.ChangeCustomize(_state, _customizationDrawer.Customize, _customizationDrawer.Changed, StateChanged.Source.Manual);
|
||||||
|
|
||||||
|
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
||||||
{
|
{
|
||||||
|
var stain = _state.ModelData.Stain(slot);
|
||||||
|
if (_equipmentDrawer.DrawStain(stain, slot, out var newStain))
|
||||||
|
_stateManager.ChangeStain(_state, slot, newStain.RowIndex, StateChanged.Source.Manual);
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
var armor = _state.ModelData.Item(slot);
|
||||||
|
if (_equipmentDrawer.DrawArmor(armor, slot, out var newArmor, _state.ModelData.Customize.Gender, _state.ModelData.Customize.Race))
|
||||||
|
_stateManager.ChangeEquip(_state, slot, newArmor, newStain.RowIndex, StateChanged.Source.Manual);
|
||||||
|
}
|
||||||
|
|
||||||
|
var mhStain = _state.ModelData.Stain(EquipSlot.MainHand);
|
||||||
|
if (_equipmentDrawer.DrawStain(mhStain, EquipSlot.MainHand, out var newMhStain))
|
||||||
|
_stateManager.ChangeStain(_state, EquipSlot.MainHand, newMhStain.RowIndex, StateChanged.Source.Manual);
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
var mh = _state.ModelData.Item(EquipSlot.MainHand);
|
||||||
|
if (_equipmentDrawer.DrawMainhand(mh, false, out var newMh))
|
||||||
|
_stateManager.ChangeEquip(_state, EquipSlot.MainHand, newMh, newMhStain.RowIndex, StateChanged.Source.Manual);
|
||||||
|
|
||||||
|
if (newMh.Type.Offhand() is not FullEquipType.Unknown)
|
||||||
|
{
|
||||||
|
var ohStain = _state.ModelData.Stain(EquipSlot.OffHand);
|
||||||
|
if (_equipmentDrawer.DrawStain(ohStain, EquipSlot.OffHand, out var newOhStain))
|
||||||
|
_stateManager.ChangeStain(_state, EquipSlot.OffHand, newOhStain.RowIndex, StateChanged.Source.Manual);
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
var oh = _state.ModelData.Item(EquipSlot.OffHand);
|
||||||
|
if (_equipmentDrawer.DrawMainhand(oh, false, out var newOh))
|
||||||
|
_stateManager.ChangeEquip(_state, EquipSlot.OffHand, newOh, newOhStain.RowIndex, StateChanged.Source.Manual);
|
||||||
}
|
}
|
||||||
// if (_currentData.Valid)
|
|
||||||
// _currentSave.Initialize(_items, _currentData.Objects[0]);
|
|
||||||
//
|
|
||||||
// RevertButton();
|
|
||||||
// ActorDebug.Draw(_currentSave.ModelData);
|
|
||||||
// return;
|
|
||||||
//
|
|
||||||
// if (_main._customizationDrawer.Draw(_currentSave.ModelData.Customize, _identifier.Type == IdentifierType.Special))
|
|
||||||
// _activeDesigns.ChangeCustomize(_currentSave, _main._customizationDrawer.Changed, _main._customizationDrawer.Customize.Data,
|
|
||||||
// false);
|
|
||||||
//
|
|
||||||
// foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
|
||||||
// {
|
|
||||||
// var current = _currentSave.Armor(slot);
|
|
||||||
// if (_main._equipmentDrawer.DrawStain(current.Stain, slot, out var stain))
|
|
||||||
// _activeDesigns.ChangeStain(_currentSave, slot, stain.RowIndex, false);
|
|
||||||
// ImGui.SameLine();
|
|
||||||
// if (_main._equipmentDrawer.DrawArmor(current, slot, out var armor, _currentSave.ModelData.Customize.Gender,
|
|
||||||
// _currentSave.ModelData.Customize.Race))
|
|
||||||
// _activeDesigns.ChangeEquipment(_currentSave, slot, armor, false);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// var currentMain = _currentSave.WeaponMain;
|
|
||||||
// if (_main._equipmentDrawer.DrawStain(currentMain.Stain, EquipSlot.MainHand, out var stainMain))
|
|
||||||
// _activeDesigns.ChangeStain(_currentSave, EquipSlot.MainHand, stainMain.RowIndex, false);
|
|
||||||
// ImGui.SameLine();
|
|
||||||
// _main._equipmentDrawer.DrawMainhand(currentMain, true, out var main);
|
|
||||||
// if (currentMain.Type.Offhand() != FullEquipType.Unknown)
|
|
||||||
// {
|
|
||||||
// var currentOff = _currentSave.WeaponOff;
|
|
||||||
// if (_main._equipmentDrawer.DrawStain(currentOff.Stain, EquipSlot.OffHand, out var stainOff))
|
|
||||||
// _activeDesigns.ChangeStain(_currentSave, EquipSlot.OffHand, stainOff.RowIndex, false);
|
|
||||||
// ImGui.SameLine();
|
|
||||||
// _main._equipmentDrawer.DrawOffhand(currentOff, main.Type, out var off);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (_main._equipmentDrawer.DrawVisor(_currentSave, out var value))
|
|
||||||
// _activeDesigns.ChangeVisor(_currentSave, value, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ using Dalamud.Interface;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
using Glamourer.Api;
|
using Glamourer.Api;
|
||||||
|
using Glamourer.Automation;
|
||||||
using Glamourer.Customization;
|
using Glamourer.Customization;
|
||||||
using Glamourer.Designs;
|
using Glamourer.Designs;
|
||||||
using Glamourer.Events;
|
using Glamourer.Events;
|
||||||
|
|
@ -40,13 +41,16 @@ public unsafe class DebugTab : ITab
|
||||||
private readonly ObjectTable _objects;
|
private readonly ObjectTable _objects;
|
||||||
private readonly ObjectManager _objectManager;
|
private readonly ObjectManager _objectManager;
|
||||||
private readonly GlamourerIpc _ipc;
|
private readonly GlamourerIpc _ipc;
|
||||||
|
private readonly PhrasingService _phrasing;
|
||||||
|
|
||||||
private readonly ItemManager _items;
|
private readonly ItemManager _items;
|
||||||
private readonly ActorService _actors;
|
private readonly ActorService _actors;
|
||||||
private readonly CustomizationService _customization;
|
private readonly CustomizationService _customization;
|
||||||
|
private readonly JobService _jobs;
|
||||||
|
|
||||||
private readonly DesignManager _designManager;
|
private readonly DesignManager _designManager;
|
||||||
private readonly DesignFileSystem _designFileSystem;
|
private readonly DesignFileSystem _designFileSystem;
|
||||||
|
private readonly AutoDesignManager _autoDesignManager;
|
||||||
|
|
||||||
private readonly PenumbraChangedItemTooltip _penumbraTooltip;
|
private readonly PenumbraChangedItemTooltip _penumbraTooltip;
|
||||||
|
|
||||||
|
|
@ -61,7 +65,8 @@ public unsafe class DebugTab : ITab
|
||||||
UpdateSlotService updateSlotService, WeaponService weaponService, PenumbraService penumbra,
|
UpdateSlotService updateSlotService, WeaponService weaponService, PenumbraService penumbra,
|
||||||
ActorService actors, ItemManager items, CustomizationService customization, ObjectManager objectManager,
|
ActorService actors, ItemManager items, CustomizationService customization, ObjectManager objectManager,
|
||||||
DesignFileSystem designFileSystem, DesignManager designManager, StateManager state, Configuration config,
|
DesignFileSystem designFileSystem, DesignManager designManager, StateManager state, Configuration config,
|
||||||
PenumbraChangedItemTooltip penumbraTooltip, MetaService metaService, GlamourerIpc ipc, DalamudPluginInterface pluginInterface)
|
PenumbraChangedItemTooltip penumbraTooltip, MetaService metaService, GlamourerIpc ipc, DalamudPluginInterface pluginInterface,
|
||||||
|
AutoDesignManager autoDesignManager, JobService jobs, PhrasingService phrasing)
|
||||||
{
|
{
|
||||||
_changeCustomizeService = changeCustomizeService;
|
_changeCustomizeService = changeCustomizeService;
|
||||||
_visorService = visorService;
|
_visorService = visorService;
|
||||||
|
|
@ -81,6 +86,9 @@ public unsafe class DebugTab : ITab
|
||||||
_metaService = metaService;
|
_metaService = metaService;
|
||||||
_ipc = ipc;
|
_ipc = ipc;
|
||||||
_pluginInterface = pluginInterface;
|
_pluginInterface = pluginInterface;
|
||||||
|
_autoDesignManager = autoDesignManager;
|
||||||
|
_jobs = jobs;
|
||||||
|
_phrasing = phrasing;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ReadOnlySpan<byte> Label
|
public ReadOnlySpan<byte> Label
|
||||||
|
|
@ -97,6 +105,7 @@ public unsafe class DebugTab : ITab
|
||||||
DrawPenumbraHeader();
|
DrawPenumbraHeader();
|
||||||
DrawDesigns();
|
DrawDesigns();
|
||||||
DrawState();
|
DrawState();
|
||||||
|
DrawAutoDesigns();
|
||||||
DrawIpc();
|
DrawIpc();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -376,14 +385,22 @@ public unsafe class DebugTab : ITab
|
||||||
|
|
||||||
if (ImGui.SmallButton("++"))
|
if (ImGui.SmallButton("++"))
|
||||||
{
|
{
|
||||||
modelCustomize.Set(type, (CustomizeValue)(modelCustomize[type].Value + 1));
|
var value = modelCustomize[type].Value;
|
||||||
|
var (_, mask) = type.ToByteAndMask();
|
||||||
|
var shift = BitOperations.TrailingZeroCount(mask);
|
||||||
|
var newValue = value + (1 << shift);
|
||||||
|
modelCustomize.Set(type, (CustomizeValue)newValue);
|
||||||
_changeCustomizeService.UpdateCustomize(model, modelCustomize.Data);
|
_changeCustomizeService.UpdateCustomize(model, modelCustomize.Data);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if (ImGui.SmallButton("--"))
|
if (ImGui.SmallButton("--"))
|
||||||
{
|
{
|
||||||
modelCustomize.Set(type, (CustomizeValue)(modelCustomize[type].Value - 1));
|
var value = modelCustomize[type].Value;
|
||||||
|
var (_, mask) = type.ToByteAndMask();
|
||||||
|
var shift = BitOperations.TrailingZeroCount(mask);
|
||||||
|
var newValue = value - (1 << shift);
|
||||||
|
modelCustomize.Set(type, (CustomizeValue)newValue);
|
||||||
_changeCustomizeService.UpdateCustomize(model, modelCustomize.Data);
|
_changeCustomizeService.UpdateCustomize(model, modelCustomize.Data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -483,6 +500,44 @@ public unsafe class DebugTab : ITab
|
||||||
DrawItemService();
|
DrawItemService();
|
||||||
DrawStainService();
|
DrawStainService();
|
||||||
DrawCustomizationService();
|
DrawCustomizationService();
|
||||||
|
DrawJobService();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawJobService()
|
||||||
|
{
|
||||||
|
using var tree = ImRaii.TreeNode("Job Service");
|
||||||
|
if (!tree)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using (var t = ImRaii.TreeNode("Jobs"))
|
||||||
|
{
|
||||||
|
if (t)
|
||||||
|
{
|
||||||
|
using var table = ImRaii.Table("##jobs", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
|
||||||
|
if (table)
|
||||||
|
foreach (var (id, job) in _jobs.Jobs)
|
||||||
|
{
|
||||||
|
ImGuiUtil.DrawTableColumn(id.ToString("D2"));
|
||||||
|
ImGuiUtil.DrawTableColumn(job.Name);
|
||||||
|
ImGuiUtil.DrawTableColumn(job.Abbreviation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var t = ImRaii.TreeNode("Job Groups"))
|
||||||
|
{
|
||||||
|
if (t)
|
||||||
|
{
|
||||||
|
using var table = ImRaii.Table("##groups", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
|
||||||
|
if (table)
|
||||||
|
foreach (var (id, group) in _jobs.JobGroups)
|
||||||
|
{
|
||||||
|
ImGuiUtil.DrawTableColumn(id.ToString("D2"));
|
||||||
|
ImGuiUtil.DrawTableColumn(group.Name);
|
||||||
|
ImGuiUtil.DrawTableColumn(group.Count.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _gamePath = string.Empty;
|
private string _gamePath = string.Empty;
|
||||||
|
|
@ -1116,6 +1171,66 @@ public unsafe class DebugTab : ITab
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Auto Designs
|
||||||
|
|
||||||
|
private void DrawAutoDesigns()
|
||||||
|
{
|
||||||
|
if (!ImGui.CollapsingHeader("Auto Designs"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
DrawPhrasingService();
|
||||||
|
|
||||||
|
foreach (var (set, idx) in _autoDesignManager.WithIndex())
|
||||||
|
{
|
||||||
|
using var id = ImRaii.PushId(idx);
|
||||||
|
using var tree = ImRaii.TreeNode(set.Name);
|
||||||
|
if (!tree)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
using var table = ImRaii.Table("##autoDesign", 2, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
|
||||||
|
if (!table)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ImGuiUtil.DrawTableColumn("Name");
|
||||||
|
ImGuiUtil.DrawTableColumn(set.Name);
|
||||||
|
|
||||||
|
ImGuiUtil.DrawTableColumn("Index");
|
||||||
|
ImGuiUtil.DrawTableColumn(idx.ToString());
|
||||||
|
|
||||||
|
ImGuiUtil.DrawTableColumn("Enabled");
|
||||||
|
ImGuiUtil.DrawTableColumn(set.Enabled.ToString());
|
||||||
|
|
||||||
|
ImGuiUtil.DrawTableColumn("Actor");
|
||||||
|
ImGuiUtil.DrawTableColumn(set.Identifier.ToString());
|
||||||
|
|
||||||
|
foreach (var (design, designIdx) in set.Designs.WithIndex())
|
||||||
|
{
|
||||||
|
ImGuiUtil.DrawTableColumn($"{design.Design.Name} ({designIdx})");
|
||||||
|
ImGuiUtil.DrawTableColumn($"{design.ApplicationType} {design.Jobs.Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawPhrasingService()
|
||||||
|
{
|
||||||
|
using var tree = ImRaii.TreeNode("Phrasing");
|
||||||
|
if (!tree)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using var table = ImRaii.Table("phrasing", 3, ImGuiTableFlags.SizingFixedFit);
|
||||||
|
if (!table)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ImGuiUtil.DrawTableColumn("Phrasing 1");
|
||||||
|
ImGuiUtil.DrawTableColumn(_config.Phrasing1);
|
||||||
|
ImGuiUtil.DrawTableColumn(_phrasing.Phrasing1.ToString());
|
||||||
|
ImGuiUtil.DrawTableColumn("Phrasing 2");
|
||||||
|
ImGuiUtil.DrawTableColumn(_config.Phrasing2);
|
||||||
|
ImGuiUtil.DrawTableColumn(_phrasing.Phrasing2.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region IPC
|
#region IPC
|
||||||
|
|
||||||
private string _gameObjectName = string.Empty;
|
private string _gameObjectName = string.Empty;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
using Glamourer.Customization;
|
using Glamourer.Customization;
|
||||||
using Glamourer.Designs;
|
using Glamourer.Designs;
|
||||||
using Glamourer.Gui.Customization;
|
using Glamourer.Gui.Customization;
|
||||||
|
using Glamourer.Gui.Equipment;
|
||||||
using Glamourer.Interop;
|
using Glamourer.Interop;
|
||||||
using Glamourer.Interop.Penumbra;
|
using Glamourer.Interop.Penumbra;
|
||||||
using Glamourer.State;
|
using Glamourer.State;
|
||||||
|
|
@ -21,24 +22,17 @@ public class DesignPanel
|
||||||
private readonly DesignManager _manager;
|
private readonly DesignManager _manager;
|
||||||
private readonly CustomizationDrawer _customizationDrawer;
|
private readonly CustomizationDrawer _customizationDrawer;
|
||||||
private readonly StateManager _state;
|
private readonly StateManager _state;
|
||||||
private readonly PenumbraService _penumbra;
|
private readonly EquipmentDrawer _equipmentDrawer;
|
||||||
private readonly UpdateSlotService _updateSlot;
|
|
||||||
private readonly WeaponService _weaponService;
|
|
||||||
private readonly ChangeCustomizeService _changeCustomizeService;
|
|
||||||
|
|
||||||
public DesignPanel(DesignFileSystemSelector selector, CustomizationDrawer customizationDrawer, DesignManager manager, ObjectManager objects,
|
public DesignPanel(DesignFileSystemSelector selector, CustomizationDrawer customizationDrawer, DesignManager manager, ObjectManager objects,
|
||||||
StateManager state, PenumbraService penumbra, ChangeCustomizeService changeCustomizeService, WeaponService weaponService,
|
StateManager state, EquipmentDrawer equipmentDrawer)
|
||||||
UpdateSlotService updateSlot)
|
|
||||||
{
|
{
|
||||||
_selector = selector;
|
_selector = selector;
|
||||||
_customizationDrawer = customizationDrawer;
|
_customizationDrawer = customizationDrawer;
|
||||||
_manager = manager;
|
_manager = manager;
|
||||||
_objects = objects;
|
_objects = objects;
|
||||||
_state = state;
|
_state = state;
|
||||||
_penumbra = penumbra;
|
_equipmentDrawer = equipmentDrawer;
|
||||||
_changeCustomizeService = changeCustomizeService;
|
|
||||||
_weaponService = weaponService;
|
|
||||||
_updateSlot = updateSlot;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Draw()
|
public void Draw()
|
||||||
|
|
@ -60,5 +54,38 @@ public class DesignPanel
|
||||||
}
|
}
|
||||||
|
|
||||||
_customizationDrawer.Draw(design.DesignData.Customize, design.WriteProtected());
|
_customizationDrawer.Draw(design.DesignData.Customize, design.WriteProtected());
|
||||||
|
|
||||||
|
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
||||||
|
{
|
||||||
|
var stain = design.DesignData.Stain(slot);
|
||||||
|
if (_equipmentDrawer.DrawStain(stain, slot, out var newStain))
|
||||||
|
_manager.ChangeStain(design, slot, newStain.RowIndex);
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
var armor = design.DesignData.Item(slot);
|
||||||
|
if (_equipmentDrawer.DrawArmor(armor, slot, out var newArmor, design.DesignData.Customize.Gender, design.DesignData.Customize.Race))
|
||||||
|
_manager.ChangeEquip(design, slot, newArmor);
|
||||||
|
}
|
||||||
|
|
||||||
|
var mhStain = design.DesignData.Stain(EquipSlot.MainHand);
|
||||||
|
if (_equipmentDrawer.DrawStain(mhStain, EquipSlot.MainHand, out var newMhStain))
|
||||||
|
_manager.ChangeStain(design, EquipSlot.MainHand, newMhStain.RowIndex);
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
var mh = design.DesignData.Item(EquipSlot.MainHand);
|
||||||
|
if (_equipmentDrawer.DrawMainhand(mh, true, out var newMh))
|
||||||
|
_manager.ChangeWeapon(design, EquipSlot.MainHand, newMh);
|
||||||
|
|
||||||
|
if (newMh.Type.Offhand() is not FullEquipType.Unknown)
|
||||||
|
{
|
||||||
|
var ohStain = design.DesignData.Stain(EquipSlot.OffHand);
|
||||||
|
if (_equipmentDrawer.DrawStain(ohStain, EquipSlot.OffHand, out var newOhStain))
|
||||||
|
_manager.ChangeStain(design, EquipSlot.OffHand, newOhStain.RowIndex);
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
var oh = design.DesignData.Item(EquipSlot.OffHand);
|
||||||
|
if (_equipmentDrawer.DrawMainhand(oh, false, out var newOh))
|
||||||
|
_manager.ChangeWeapon(design, EquipSlot.OffHand, newOh);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using System.Runtime.CompilerServices;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Glamourer.Gui.Tabs.DesignTab;
|
using Glamourer.Gui.Tabs.DesignTab;
|
||||||
using Glamourer.Interop.Penumbra;
|
using Glamourer.Interop.Penumbra;
|
||||||
|
using Glamourer.Services;
|
||||||
using Glamourer.State;
|
using Glamourer.State;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
|
|
@ -16,19 +17,25 @@ public class SettingsTab : ITab
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
private readonly DesignFileSystemSelector _selector;
|
private readonly DesignFileSystemSelector _selector;
|
||||||
private readonly StateListener _stateListener;
|
private readonly StateListener _stateListener;
|
||||||
|
private readonly PhrasingService _phrasingService;
|
||||||
private readonly PenumbraAutoRedraw _autoRedraw;
|
private readonly PenumbraAutoRedraw _autoRedraw;
|
||||||
|
|
||||||
public SettingsTab(Configuration config, DesignFileSystemSelector selector, StateListener stateListener, PenumbraAutoRedraw autoRedraw)
|
public SettingsTab(Configuration config, DesignFileSystemSelector selector, StateListener stateListener,
|
||||||
|
PhrasingService phrasingService, PenumbraAutoRedraw autoRedraw)
|
||||||
{
|
{
|
||||||
_config = config;
|
_config = config;
|
||||||
_selector = selector;
|
_selector = selector;
|
||||||
_stateListener = stateListener;
|
_stateListener = stateListener;
|
||||||
_autoRedraw = autoRedraw;
|
_phrasingService = phrasingService;
|
||||||
|
_autoRedraw = autoRedraw;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ReadOnlySpan<byte> Label
|
public ReadOnlySpan<byte> Label
|
||||||
=> "Settings"u8;
|
=> "Settings"u8;
|
||||||
|
|
||||||
|
private string? _tmpPhrasing1 = null;
|
||||||
|
private string? _tmpPhrasing2 = null;
|
||||||
|
|
||||||
public void DrawContent()
|
public void DrawContent()
|
||||||
{
|
{
|
||||||
using var child = ImRaii.Child("MainWindowChild");
|
using var child = ImRaii.Child("MainWindowChild");
|
||||||
|
|
@ -36,6 +43,8 @@ public class SettingsTab : ITab
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Checkbox("Enabled", "Enable main functionality of keeping and applying state.", _stateListener.Enabled, _stateListener.Enable);
|
Checkbox("Enabled", "Enable main functionality of keeping and applying state.", _stateListener.Enabled, _stateListener.Enable);
|
||||||
|
Checkbox("Enable Auto Designs", "Enable the application of designs associated to characters to be applied automatically.",
|
||||||
|
_config.EnableAutoDesigns, v => _config.EnableAutoDesigns = v);
|
||||||
Checkbox("Restricted Gear Protection",
|
Checkbox("Restricted Gear Protection",
|
||||||
"Use gender- and race-appropriate models when detecting certain items not available for a characters current gender and race.",
|
"Use gender- and race-appropriate models when detecting certain items not available for a characters current gender and race.",
|
||||||
_config.UseRestrictedGearProtection, v => _config.UseRestrictedGearProtection = v);
|
_config.UseRestrictedGearProtection, v => _config.UseRestrictedGearProtection = v);
|
||||||
|
|
@ -53,6 +62,24 @@ public class SettingsTab : ITab
|
||||||
Checkbox("Debug Mode", "Show the debug tab. Only useful for debugging or advanced use.", _config.DebugMode, v => _config.DebugMode = v);
|
Checkbox("Debug Mode", "Show the debug tab. Only useful for debugging or advanced use.", _config.DebugMode, v => _config.DebugMode = v);
|
||||||
DrawColorSettings();
|
DrawColorSettings();
|
||||||
|
|
||||||
|
_tmpPhrasing1 ??= _config.Phrasing1;
|
||||||
|
ImGui.InputText("Phrasing 1", ref _tmpPhrasing1, 512);
|
||||||
|
|
||||||
|
if (ImGui.IsItemDeactivatedAfterEdit())
|
||||||
|
{
|
||||||
|
_phrasingService.SetPhrasing1(_tmpPhrasing1);
|
||||||
|
_tmpPhrasing1 = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_tmpPhrasing2 ??= _config.Phrasing2;
|
||||||
|
ImGui.InputText("Phrasing 2", ref _tmpPhrasing2, 512);
|
||||||
|
|
||||||
|
if (ImGui.IsItemDeactivatedAfterEdit())
|
||||||
|
{
|
||||||
|
_phrasingService.SetPhrasing2(_tmpPhrasing2);
|
||||||
|
_tmpPhrasing2 = null;
|
||||||
|
}
|
||||||
|
|
||||||
MainWindow.DrawSupportButtons();
|
MainWindow.DrawSupportButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
49
Glamourer/Interop/JobService.cs
Normal file
49
Glamourer/Interop/JobService.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Dalamud.Data;
|
||||||
|
using Dalamud.Hooking;
|
||||||
|
using Dalamud.Utility.Signatures;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
|
using Glamourer.Interop.Structs;
|
||||||
|
using Glamourer.Structs;
|
||||||
|
|
||||||
|
namespace Glamourer.Interop;
|
||||||
|
|
||||||
|
public class JobService : IDisposable
|
||||||
|
{
|
||||||
|
private readonly nint _characterDataOffset;
|
||||||
|
|
||||||
|
public readonly IReadOnlyDictionary<byte, Job> Jobs;
|
||||||
|
public readonly IReadOnlyDictionary<ushort, JobGroup> JobGroups;
|
||||||
|
|
||||||
|
public event Action<Actor, Job>? JobChanged;
|
||||||
|
|
||||||
|
public JobService(DataManager gameData)
|
||||||
|
{
|
||||||
|
SignatureHelper.Initialise(this);
|
||||||
|
_characterDataOffset = Marshal.OffsetOf<Character>(nameof(Character.CharacterData));
|
||||||
|
Jobs = GameData.Jobs(gameData);
|
||||||
|
JobGroups = GameData.JobGroups(gameData);
|
||||||
|
_changeJobHook.Enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_changeJobHook.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private delegate void ChangeJobDelegate(nint data, uint job);
|
||||||
|
|
||||||
|
[Signature(Sigs.ChangeJob, DetourName = nameof(ChangeJobDetour))]
|
||||||
|
private readonly Hook<ChangeJobDelegate> _changeJobHook = null!;
|
||||||
|
|
||||||
|
private void ChangeJobDetour(nint data, uint jobIndex)
|
||||||
|
{
|
||||||
|
_changeJobHook.Original(data, jobIndex);
|
||||||
|
var actor = (Actor)(data - _characterDataOffset);
|
||||||
|
var job = Jobs.TryGetValue((byte) jobIndex, out var j) ? j : Jobs[0];
|
||||||
|
Glamourer.Log.Excessive($"{actor} changed job to {job}");
|
||||||
|
JobChanged?.Invoke(actor, job);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -88,7 +88,7 @@ public readonly unsafe struct Model : IEquatable<Model>
|
||||||
|
|
||||||
/// <summary> Only valid for humans. </summary>
|
/// <summary> Only valid for humans. </summary>
|
||||||
public CharacterArmor GetArmor(EquipSlot slot)
|
public CharacterArmor GetArmor(EquipSlot slot)
|
||||||
=> ((CharacterArmor*)AsHuman->EquipSlotData)[slot.ToIndex()];
|
=> ((CharacterArmor*)&AsHuman->Head)[slot.ToIndex()];
|
||||||
|
|
||||||
public Customize GetCustomize()
|
public Customize GetCustomize()
|
||||||
=> *(Customize*)&AsHuman->Customize;
|
=> *(Customize*)&AsHuman->Customize;
|
||||||
|
|
|
||||||
|
|
@ -51,11 +51,14 @@ public unsafe class WeaponService : IDisposable
|
||||||
_ => EquipSlot.Unknown,
|
_ => EquipSlot.Unknown,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var tmpWeapon = weapon;
|
||||||
// First call the regular function.
|
// First call the regular function.
|
||||||
if (equipSlot is not EquipSlot.Unknown)
|
if (equipSlot is not EquipSlot.Unknown)
|
||||||
_event.Invoke(actor, equipSlot, ref weapon);
|
_event.Invoke(actor, equipSlot, ref tmpWeapon);
|
||||||
|
|
||||||
_loadWeaponHook.Original(drawData, slot, weapon.Value, redrawOnEquality, unk2, skipGameObject, unk4);
|
_loadWeaponHook.Original(drawData, slot, weapon.Value, redrawOnEquality, unk2, skipGameObject, unk4);
|
||||||
|
if (tmpWeapon.Value != weapon.Value)
|
||||||
|
_loadWeaponHook.Original(drawData, slot, tmpWeapon.Value, 1, unk2, 1, unk4);
|
||||||
Glamourer.Log.Excessive(
|
Glamourer.Log.Excessive(
|
||||||
$"Weapon reloaded for 0x{actor.Address:X} ({actor.Utf8Name}) with attributes {slot} {weapon.Value:X14}, {redrawOnEquality}, {unk2}, {skipGameObject}, {unk4}");
|
$"Weapon reloaded for 0x{actor.Address:X} ({actor.Utf8Name}) with attributes {slot} {weapon.Value:X14}, {redrawOnEquality}, {unk2}, {skipGameObject}, {unk4}");
|
||||||
}
|
}
|
||||||
|
|
@ -88,8 +91,10 @@ public unsafe class WeaponService : IDisposable
|
||||||
|
|
||||||
public void LoadStain(Actor character, EquipSlot slot, StainId stain)
|
public void LoadStain(Actor character, EquipSlot slot, StainId stain)
|
||||||
{
|
{
|
||||||
var value = slot == EquipSlot.OffHand ? character.AsCharacter->DrawData.OffHandModel : character.AsCharacter->DrawData.MainHandModel;
|
var mdl = character.Model;
|
||||||
var weapon = new CharacterWeapon(value.Value) { Stain = stain.Value };
|
var (_, _, mh, oh) = mdl.GetWeapons(character);
|
||||||
|
var value = slot == EquipSlot.OffHand ? oh : mh;
|
||||||
|
var weapon = value.With(value.Set.Value == 0 ? 0 : stain);
|
||||||
LoadWeapon(character, slot, weapon);
|
LoadWeapon(character, slot, weapon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using Glamourer.Automation;
|
||||||
using Glamourer.Gui;
|
using Glamourer.Gui;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
|
@ -7,13 +8,17 @@ namespace Glamourer.Services;
|
||||||
|
|
||||||
public class ConfigMigrationService
|
public class ConfigMigrationService
|
||||||
{
|
{
|
||||||
private readonly SaveService _saveService;
|
private readonly SaveService _saveService;
|
||||||
|
private readonly FixedDesignMigrator _fixedDesignMigrator;
|
||||||
|
|
||||||
private Configuration _config = null!;
|
private Configuration _config = null!;
|
||||||
private JObject _data = null!;
|
private JObject _data = null!;
|
||||||
|
|
||||||
public ConfigMigrationService(SaveService saveService)
|
public ConfigMigrationService(SaveService saveService, FixedDesignMigrator fixedDesignMigrator)
|
||||||
=> _saveService = saveService;
|
{
|
||||||
|
_saveService = saveService;
|
||||||
|
_fixedDesignMigrator = fixedDesignMigrator;
|
||||||
|
}
|
||||||
|
|
||||||
public void Migrate(Configuration config)
|
public void Migrate(Configuration config)
|
||||||
{
|
{
|
||||||
|
|
@ -34,6 +39,7 @@ public class ConfigMigrationService
|
||||||
if (_config.Version > 1)
|
if (_config.Version > 1)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
_fixedDesignMigrator.Migrate(_data["FixedDesigns"]);
|
||||||
_config.Version = 2;
|
_config.Version = 2;
|
||||||
var customizationColor = _data["CustomizationColor"]?.ToObject<uint>() ?? ColorId.CustomizationDesign.Data().DefaultColor;
|
var customizationColor = _data["CustomizationColor"]?.ToObject<uint>() ?? ColorId.CustomizationDesign.Data().DefaultColor;
|
||||||
_config.Colors[ColorId.CustomizationDesign] = customizationColor;
|
_config.Colors[ColorId.CustomizationDesign] = customizationColor;
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,42 @@ public sealed class CustomizationService : AsyncServiceWrapper<ICustomizationMan
|
||||||
: base(nameof(CustomizationService), () => CustomizationManager.Create(pi, gameData))
|
: base(nameof(CustomizationService), () => CustomizationManager.Create(pi, gameData))
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
|
public (Customize NewValue, CustomizeFlag Applied) Combine(Customize oldValues, Customize newValues, CustomizeFlag applyWhich)
|
||||||
|
{
|
||||||
|
CustomizeFlag applied = 0;
|
||||||
|
Customize ret = default;
|
||||||
|
ret.Load(oldValues);
|
||||||
|
if (applyWhich.HasFlag(CustomizeFlag.Clan))
|
||||||
|
{
|
||||||
|
ChangeClan(ref ret, newValues.Clan);
|
||||||
|
applied |= CustomizeFlag.Clan;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (applyWhich.HasFlag(CustomizeFlag.Gender))
|
||||||
|
if (ret.Race is not Race.Hrothgar || newValues.Gender is not Gender.Female)
|
||||||
|
{
|
||||||
|
ChangeGender(ref ret, newValues.Gender);
|
||||||
|
applied |= CustomizeFlag.Gender;
|
||||||
|
}
|
||||||
|
|
||||||
|
var set = AwaitedService.GetList(ret.Clan, ret.Gender);
|
||||||
|
foreach (var index in Enum.GetValues<CustomizeIndex>())
|
||||||
|
{
|
||||||
|
var flag = index.ToFlag();
|
||||||
|
if (!applyWhich.HasFlag(flag))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var value = newValues[index];
|
||||||
|
if (IsCustomizationValid(set, ret.Face, index, value))
|
||||||
|
{
|
||||||
|
ret[index] = value;
|
||||||
|
applied |= flag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (ret, applied);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary> In languages other than english the actual clan name may depend on gender. </summary>
|
/// <summary> In languages other than english the actual clan name may depend on gender. </summary>
|
||||||
public string ClanName(SubRace race, Gender gender)
|
public string ClanName(SubRace race, Gender gender)
|
||||||
{
|
{
|
||||||
|
|
@ -175,53 +211,59 @@ public sealed class CustomizationService : AsyncServiceWrapper<ICustomizationMan
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Change a clan while keeping all other customizations valid. </summary>
|
/// <summary> Change a clan while keeping all other customizations valid. </summary>
|
||||||
public bool ChangeClan(ref Customize customize, SubRace newClan)
|
public CustomizeFlag ChangeClan(ref Customize customize, SubRace newClan)
|
||||||
{
|
{
|
||||||
if (customize.Clan == newClan)
|
if (customize.Clan == newClan)
|
||||||
return false;
|
return 0;
|
||||||
|
|
||||||
if (ValidateClan(newClan, newClan.ToRace(), out var newRace, out newClan).Length > 0)
|
if (ValidateClan(newClan, newClan.ToRace(), out var newRace, out newClan).Length > 0)
|
||||||
return false;
|
return 0;
|
||||||
|
|
||||||
|
var flags = CustomizeFlag.Clan | CustomizeFlag.Race;
|
||||||
customize.Race = newRace;
|
customize.Race = newRace;
|
||||||
customize.Clan = newClan;
|
customize.Clan = newClan;
|
||||||
|
|
||||||
// TODO Female Hrothgar
|
// TODO Female Hrothgar
|
||||||
if (newRace == Race.Hrothgar)
|
if (newRace == Race.Hrothgar)
|
||||||
customize.Gender = Gender.Male;
|
{
|
||||||
|
customize.Gender = Gender.Male;
|
||||||
|
flags |= CustomizeFlag.Gender;
|
||||||
|
}
|
||||||
|
|
||||||
var set = AwaitedService.GetList(customize.Clan, customize.Gender);
|
var set = AwaitedService.GetList(customize.Clan, customize.Gender);
|
||||||
FixValues(set, ref customize);
|
return FixValues(set, ref customize) | flags;
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Change a gender while keeping all other customizations valid. </summary>
|
/// <summary> Change a gender while keeping all other customizations valid. </summary>
|
||||||
public bool ChangeGender(ref Customize customize, Gender newGender)
|
public CustomizeFlag ChangeGender(ref Customize customize, Gender newGender)
|
||||||
{
|
{
|
||||||
if (customize.Gender == newGender)
|
if (customize.Gender == newGender)
|
||||||
return false;
|
return 0;
|
||||||
|
|
||||||
// TODO Female Hrothgar
|
// TODO Female Hrothgar
|
||||||
if (customize.Race is Race.Hrothgar)
|
if (customize.Race is Race.Hrothgar)
|
||||||
return false;
|
return 0;
|
||||||
|
|
||||||
if (ValidateGender(customize.Race, newGender, out newGender).Length > 0)
|
if (ValidateGender(customize.Race, newGender, out newGender).Length > 0)
|
||||||
return false;
|
return 0;
|
||||||
|
|
||||||
customize.Gender = newGender;
|
customize.Gender = newGender;
|
||||||
var set = AwaitedService.GetList(customize.Clan, customize.Gender);
|
var set = AwaitedService.GetList(customize.Clan, customize.Gender);
|
||||||
FixValues(set, ref customize);
|
return FixValues(set, ref customize) | CustomizeFlag.Gender;
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void FixValues(CustomizationSet set, ref Customize customize)
|
private static CustomizeFlag FixValues(CustomizationSet set, ref Customize customize)
|
||||||
{
|
{
|
||||||
|
CustomizeFlag flags = 0;
|
||||||
foreach (var idx in Enum.GetValues<CustomizeIndex>().Where(set.IsAvailable))
|
foreach (var idx in Enum.GetValues<CustomizeIndex>().Where(set.IsAvailable))
|
||||||
{
|
{
|
||||||
if (ValidateCustomizeValue(set, customize.Face, idx, customize[idx], out var fixedValue).Length > 0)
|
if (ValidateCustomizeValue(set, customize.Face, idx, customize[idx], out var fixedValue).Length > 0)
|
||||||
customize[idx] = fixedValue;
|
{
|
||||||
|
customize[idx] = fixedValue;
|
||||||
|
flags |= idx.ToFlag();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return flags;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,13 @@ public class FilenameService
|
||||||
public readonly string DesignFileSystem;
|
public readonly string DesignFileSystem;
|
||||||
public readonly string MigrationDesignFile;
|
public readonly string MigrationDesignFile;
|
||||||
public readonly string DesignDirectory;
|
public readonly string DesignDirectory;
|
||||||
|
public readonly string AutomationFile;
|
||||||
|
|
||||||
public FilenameService(DalamudPluginInterface pi)
|
public FilenameService(DalamudPluginInterface pi)
|
||||||
{
|
{
|
||||||
ConfigDirectory = pi.ConfigDirectory.FullName;
|
ConfigDirectory = pi.ConfigDirectory.FullName;
|
||||||
ConfigFile = pi.ConfigFile.FullName;
|
ConfigFile = pi.ConfigFile.FullName;
|
||||||
|
AutomationFile = Path.Combine(ConfigDirectory, "automation.json");
|
||||||
DesignFileSystem = Path.Combine(ConfigDirectory, "sort_order.json");
|
DesignFileSystem = Path.Combine(ConfigDirectory, "sort_order.json");
|
||||||
MigrationDesignFile = Path.Combine(ConfigDirectory, "Designs.json");
|
MigrationDesignFile = Path.Combine(ConfigDirectory, "Designs.json");
|
||||||
DesignDirectory = Path.Combine(ConfigDirectory, "designs");
|
DesignDirectory = Path.Combine(ConfigDirectory, "designs");
|
||||||
|
|
|
||||||
54
Glamourer/Services/PhrasingService.cs
Normal file
54
Glamourer/Services/PhrasingService.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Glamourer.Services;
|
||||||
|
|
||||||
|
public class PhrasingService
|
||||||
|
{
|
||||||
|
private readonly Configuration _config;
|
||||||
|
private readonly SHA256 _hasher = SHA256.Create();
|
||||||
|
|
||||||
|
public bool Phrasing1 { get; private set; }
|
||||||
|
public bool Phrasing2 { get; private set; }
|
||||||
|
|
||||||
|
public PhrasingService(Configuration config)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
Phrasing1 = CheckPhrasing(_config.Phrasing1, P1);
|
||||||
|
Phrasing2 = CheckPhrasing(_config.Phrasing2, P2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetPhrasing1(string newPhrasing)
|
||||||
|
{
|
||||||
|
if (_config.Phrasing1 == newPhrasing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_config.Phrasing1 = newPhrasing;
|
||||||
|
_config.Save();
|
||||||
|
Phrasing1 = CheckPhrasing(newPhrasing, P1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetPhrasing2(string newPhrasing)
|
||||||
|
{
|
||||||
|
if (_config.Phrasing2 == newPhrasing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_config.Phrasing2 = newPhrasing;
|
||||||
|
_config.Save();
|
||||||
|
Phrasing2 = CheckPhrasing(newPhrasing, P2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CheckPhrasing(string phrasing, ReadOnlySpan<byte> data)
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(phrasing));
|
||||||
|
var sha = _hasher.ComputeHash(stream);
|
||||||
|
return data.SequenceEqual(sha);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @formatter:off
|
||||||
|
private static ReadOnlySpan<byte> P1 => new byte[] { 0xD1, 0x35, 0xD7, 0x18, 0xBE, 0x45, 0x42, 0xBD, 0x88, 0x77, 0x7E, 0xC4, 0x41, 0x06, 0x34, 0x4D, 0x71, 0x3A, 0xC5, 0xCC, 0xA4, 0x1B, 0x7D, 0x3F, 0x3B, 0x86, 0x07, 0xCB, 0x63, 0xD7, 0xF9, 0xDB };
|
||||||
|
private static ReadOnlySpan<byte> P2 => new byte[] { 0x6A, 0x84, 0x12, 0xEA, 0x3B, 0x03, 0x2E, 0xD9, 0xA3, 0x51, 0xB0, 0x4F, 0xE7, 0x4D, 0x59, 0x87, 0xA9, 0xA1, 0x6E, 0x08, 0xC7, 0x3E, 0xD3, 0x15, 0xEE, 0x40, 0x2C, 0xB3, 0x44, 0x78, 0x1F, 0xA0 };
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using Glamourer.Api;
|
using Glamourer.Api;
|
||||||
|
using Glamourer.Automation;
|
||||||
using Glamourer.Designs;
|
using Glamourer.Designs;
|
||||||
using Glamourer.Events;
|
using Glamourer.Events;
|
||||||
using Glamourer.Gui;
|
using Glamourer.Gui;
|
||||||
using Glamourer.Gui.Customization;
|
using Glamourer.Gui.Customization;
|
||||||
|
using Glamourer.Gui.Equipment;
|
||||||
using Glamourer.Gui.Tabs;
|
using Glamourer.Gui.Tabs;
|
||||||
using Glamourer.Gui.Tabs.ActorTab;
|
using Glamourer.Gui.Tabs.ActorTab;
|
||||||
using Glamourer.Gui.Tabs.DesignTab;
|
using Glamourer.Gui.Tabs.DesignTab;
|
||||||
|
|
@ -47,6 +49,7 @@ public static class ServiceManager
|
||||||
.AddSingleton<BackupService>()
|
.AddSingleton<BackupService>()
|
||||||
.AddSingleton<FrameworkManager>()
|
.AddSingleton<FrameworkManager>()
|
||||||
.AddSingleton<SaveService>()
|
.AddSingleton<SaveService>()
|
||||||
|
.AddSingleton<PhrasingService>()
|
||||||
.AddSingleton<ConfigMigrationService>()
|
.AddSingleton<ConfigMigrationService>()
|
||||||
.AddSingleton<Configuration>();
|
.AddSingleton<Configuration>();
|
||||||
|
|
||||||
|
|
@ -54,6 +57,7 @@ public static class ServiceManager
|
||||||
=> services.AddSingleton<VisorStateChanged>()
|
=> services.AddSingleton<VisorStateChanged>()
|
||||||
.AddSingleton<SlotUpdating>()
|
.AddSingleton<SlotUpdating>()
|
||||||
.AddSingleton<DesignChanged>()
|
.AddSingleton<DesignChanged>()
|
||||||
|
.AddSingleton<AutomationChanged>()
|
||||||
.AddSingleton<StateChanged>()
|
.AddSingleton<StateChanged>()
|
||||||
.AddSingleton<WeaponLoading>()
|
.AddSingleton<WeaponLoading>()
|
||||||
.AddSingleton<HeadGearVisibilityChanged>()
|
.AddSingleton<HeadGearVisibilityChanged>()
|
||||||
|
|
@ -74,11 +78,15 @@ public static class ServiceManager
|
||||||
.AddSingleton<WeaponService>()
|
.AddSingleton<WeaponService>()
|
||||||
.AddSingleton<PenumbraService>()
|
.AddSingleton<PenumbraService>()
|
||||||
.AddSingleton<ObjectManager>()
|
.AddSingleton<ObjectManager>()
|
||||||
.AddSingleton<PenumbraAutoRedraw>();
|
.AddSingleton<PenumbraAutoRedraw>()
|
||||||
|
.AddSingleton<JobService>();
|
||||||
|
|
||||||
private static IServiceCollection AddDesigns(this IServiceCollection services)
|
private static IServiceCollection AddDesigns(this IServiceCollection services)
|
||||||
=> services.AddSingleton<DesignManager>()
|
=> services.AddSingleton<DesignManager>()
|
||||||
.AddSingleton<DesignFileSystem>();
|
.AddSingleton<DesignFileSystem>()
|
||||||
|
.AddSingleton<AutoDesignManager>()
|
||||||
|
.AddSingleton<AutoDesignApplier>()
|
||||||
|
.AddSingleton<FixedDesignMigrator>();
|
||||||
|
|
||||||
private static IServiceCollection AddState(this IServiceCollection services)
|
private static IServiceCollection AddState(this IServiceCollection services)
|
||||||
=> services.AddSingleton<StateManager>()
|
=> services.AddSingleton<StateManager>()
|
||||||
|
|
@ -94,6 +102,7 @@ public static class ServiceManager
|
||||||
.AddSingleton<MainWindow>()
|
.AddSingleton<MainWindow>()
|
||||||
.AddSingleton<GlamourerWindowSystem>()
|
.AddSingleton<GlamourerWindowSystem>()
|
||||||
.AddSingleton<CustomizationDrawer>()
|
.AddSingleton<CustomizationDrawer>()
|
||||||
|
.AddSingleton<EquipmentDrawer>()
|
||||||
.AddSingleton<DesignFileSystemSelector>()
|
.AddSingleton<DesignFileSystemSelector>()
|
||||||
.AddSingleton<DesignPanel>()
|
.AddSingleton<DesignPanel>()
|
||||||
.AddSingleton<DesignTab>()
|
.AddSingleton<DesignTab>()
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using Glamourer.Customization;
|
using System;
|
||||||
|
using Glamourer.Customization;
|
||||||
using Glamourer.Designs;
|
using Glamourer.Designs;
|
||||||
using Glamourer.Events;
|
using Glamourer.Events;
|
||||||
using Glamourer.Structs;
|
using Glamourer.Structs;
|
||||||
|
|
@ -20,7 +21,7 @@ public class ActorState
|
||||||
ModelId,
|
ModelId,
|
||||||
}
|
}
|
||||||
|
|
||||||
public ActorIdentifier Identifier { get; internal init; }
|
public readonly ActorIdentifier Identifier;
|
||||||
|
|
||||||
/// <summary> This should always represent the unmodified state of the draw object. </summary>
|
/// <summary> This should always represent the unmodified state of the draw object. </summary>
|
||||||
public DesignData BaseData;
|
public DesignData BaseData;
|
||||||
|
|
@ -33,7 +34,7 @@ public class ActorState
|
||||||
.Repeat(StateChanged.Source.Game, EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices + 5).ToArray();
|
.Repeat(StateChanged.Source.Game, EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices + 5).ToArray();
|
||||||
|
|
||||||
internal ActorState(ActorIdentifier identifier)
|
internal ActorState(ActorIdentifier identifier)
|
||||||
=> Identifier = identifier;
|
=> Identifier = identifier.CreatePermanent();
|
||||||
|
|
||||||
public ref StateChanged.Source this[EquipSlot slot, bool stain]
|
public ref StateChanged.Source this[EquipSlot slot, bool stain]
|
||||||
=> ref _sources[slot.ToIndex() + (stain ? EquipFlagExtensions.NumEquipFlags / 2 : 0)];
|
=> ref _sources[slot.ToIndex() + (stain ? EquipFlagExtensions.NumEquipFlags / 2 : 0)];
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,22 @@
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Glamourer.Customization;
|
using Glamourer.Customization;
|
||||||
using Glamourer.Interop;
|
using Glamourer.Interop;
|
||||||
|
using Glamourer.Interop.Penumbra;
|
||||||
using Glamourer.Interop.Structs;
|
using Glamourer.Interop.Structs;
|
||||||
using Glamourer.Services;
|
using Glamourer.Services;
|
||||||
|
using Penumbra.Api.Enums;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
|
|
||||||
namespace Glamourer.State;
|
namespace Glamourer.State;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This class applies changes made to state to actual objects in the game.
|
||||||
|
/// It handles applying those changes as well as redrawing the actor if necessary.
|
||||||
|
/// </summary>
|
||||||
public class StateEditor
|
public class StateEditor
|
||||||
{
|
{
|
||||||
|
private readonly PenumbraService _penumbra;
|
||||||
private readonly UpdateSlotService _updateSlot;
|
private readonly UpdateSlotService _updateSlot;
|
||||||
private readonly VisorService _visor;
|
private readonly VisorService _visor;
|
||||||
private readonly WeaponService _weapon;
|
private readonly WeaponService _weapon;
|
||||||
|
|
@ -17,45 +24,63 @@ public class StateEditor
|
||||||
private readonly ItemManager _items;
|
private readonly ItemManager _items;
|
||||||
|
|
||||||
public StateEditor(UpdateSlotService updateSlot, VisorService visor, WeaponService weapon, ChangeCustomizeService changeCustomize,
|
public StateEditor(UpdateSlotService updateSlot, VisorService visor, WeaponService weapon, ChangeCustomizeService changeCustomize,
|
||||||
ItemManager items)
|
ItemManager items, PenumbraService penumbra)
|
||||||
{
|
{
|
||||||
_updateSlot = updateSlot;
|
_updateSlot = updateSlot;
|
||||||
_visor = visor;
|
_visor = visor;
|
||||||
_weapon = weapon;
|
_weapon = weapon;
|
||||||
_changeCustomize = changeCustomize;
|
_changeCustomize = changeCustomize;
|
||||||
_items = items;
|
_items = items;
|
||||||
|
_penumbra = penumbra;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary> Changing the model ID simply requires guaranteed redrawing. </summary>
|
||||||
|
public void ChangeModelId(ActorData data, uint modelId)
|
||||||
|
{
|
||||||
|
foreach (var actor in data.Objects)
|
||||||
|
_penumbra.RedrawObject(actor, RedrawType.Redraw);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Change the customization values of actors either by applying them via update or redrawing,
|
||||||
|
/// this depends on whether the changes include changes to Race, Gender, Body Type or Face.
|
||||||
|
/// </summary>
|
||||||
public void ChangeCustomize(ActorData data, Customize customize)
|
public void ChangeCustomize(ActorData data, Customize customize)
|
||||||
{
|
{
|
||||||
foreach (var actor in data.Objects)
|
foreach (var actor in data.Objects)
|
||||||
_changeCustomize.UpdateCustomize(actor, customize.Data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ChangeCustomize(ActorData data, CustomizeIndex idx, CustomizeValue value)
|
|
||||||
{
|
|
||||||
foreach (var actor in data.Objects.Where(a => a.IsCharacter))
|
|
||||||
{
|
{
|
||||||
var mdl = actor.Model;
|
var mdl = actor.Model;
|
||||||
var customize = mdl.GetCustomize();
|
if (!mdl.IsHuman)
|
||||||
customize[idx] = value;
|
continue;
|
||||||
_changeCustomize.UpdateCustomize(mdl, customize.Data);
|
|
||||||
|
var flags = Customize.Compare(mdl.GetCustomize(), customize);
|
||||||
|
if (!flags.RequiresRedraw())
|
||||||
|
_changeCustomize.UpdateCustomize(mdl, customize.Data);
|
||||||
|
else
|
||||||
|
_penumbra.RedrawObject(actor, RedrawType.Redraw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ChangeArmor(ActorState state, ActorData data, EquipSlot slot)
|
/// <summary>
|
||||||
|
/// Change a single piece of armor and/or stain depending on slot.
|
||||||
|
/// This uses the current customization of the model to potentially prevent restricted gear types from appearing.
|
||||||
|
/// This never requires redrawing.
|
||||||
|
/// </summary>
|
||||||
|
public void ChangeArmor(ActorData data, EquipSlot slot, CharacterArmor armor)
|
||||||
{
|
{
|
||||||
var armor = state.ModelData.Armor(slot);
|
|
||||||
foreach (var actor in data.Objects.Where(a => a.IsCharacter))
|
foreach (var actor in data.Objects.Where(a => a.IsCharacter))
|
||||||
{
|
{
|
||||||
var mdl = actor.Model;
|
var mdl = actor.Model;
|
||||||
var customize = mdl.IsHuman ? mdl.GetCustomize() : actor.GetCustomize();
|
var customize = mdl.IsHuman ? mdl.GetCustomize() : actor.GetCustomize();
|
||||||
var (_, resolvedItem) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender);
|
var (_, resolvedItem) = _items.ResolveRestrictedGear(armor, slot, customize.Race, customize.Gender);
|
||||||
_updateSlot.UpdateSlot(actor.Model, slot, resolvedItem);
|
_updateSlot.UpdateSlot(actor.Model, slot, resolvedItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Change the stain of a single piece of armor or weapon.
|
||||||
|
/// If the offhand is empty, the stain will be fixed to 0 to prevent crashes.
|
||||||
|
/// </summary>
|
||||||
public void ChangeStain(ActorData data, EquipSlot slot, StainId stain)
|
public void ChangeStain(ActorData data, EquipSlot slot, StainId stain)
|
||||||
{
|
{
|
||||||
var idx = slot.ToIndex();
|
var idx = slot.ToIndex();
|
||||||
|
|
@ -76,18 +101,34 @@ public class StateEditor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ChangeMainhand(ActorData data, EquipItem weapon)
|
/// <summary> Apply a weapon to the appropriate slot. </summary>
|
||||||
|
public void ChangeWeapon(ActorData data, EquipSlot slot, EquipItem item, StainId stain)
|
||||||
{
|
{
|
||||||
foreach (var actor in data.Objects.Where(a => a.IsCharacter))
|
if (slot is EquipSlot.MainHand)
|
||||||
_weapon.LoadWeapon(actor, EquipSlot.MainHand, weapon.Weapon());
|
ChangeMainhand(data, item, stain);
|
||||||
|
else
|
||||||
|
ChangeOffhand(data, item, stain);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ChangeOffhand(ActorData data, EquipItem weapon)
|
/// <summary>
|
||||||
|
/// Apply a weapon to the mainhand. If the weapon type has no associated offhand type, apply both.
|
||||||
|
/// </summary>
|
||||||
|
public void ChangeMainhand(ActorData data, EquipItem weapon, StainId stain)
|
||||||
{
|
{
|
||||||
|
var slot = weapon.Type.Offhand() == FullEquipType.Unknown ? EquipSlot.BothHand : EquipSlot.MainHand;
|
||||||
foreach (var actor in data.Objects.Where(a => a.IsCharacter))
|
foreach (var actor in data.Objects.Where(a => a.IsCharacter))
|
||||||
_weapon.LoadWeapon(actor, EquipSlot.OffHand, weapon.Weapon());
|
_weapon.LoadWeapon(actor, slot, weapon.Weapon().With(stain));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary> Apply a weapon to the offhand. </summary>
|
||||||
|
public void ChangeOffhand(ActorData data, EquipItem weapon, StainId stain)
|
||||||
|
{
|
||||||
|
stain = weapon.ModelId.Value == 0 ? 0 : stain;
|
||||||
|
foreach (var actor in data.Objects.Where(a => a.IsCharacter))
|
||||||
|
_weapon.LoadWeapon(actor, EquipSlot.OffHand, weapon.Weapon().With(stain));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Change the visor state of actors only on the draw object. </summary>
|
||||||
public void ChangeVisor(ActorData data, bool value)
|
public void ChangeVisor(ActorData data, bool value)
|
||||||
{
|
{
|
||||||
foreach (var actor in data.Objects.Where(a => a.IsCharacter))
|
foreach (var actor in data.Objects.Where(a => a.IsCharacter))
|
||||||
|
|
@ -100,18 +141,21 @@ public class StateEditor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary> Change the forced wetness state on actors. </summary>
|
||||||
public unsafe void ChangeWetness(ActorData data, bool value)
|
public unsafe void ChangeWetness(ActorData data, bool value)
|
||||||
{
|
{
|
||||||
foreach (var actor in data.Objects.Where(a => a.IsCharacter))
|
foreach (var actor in data.Objects.Where(a => a.IsCharacter))
|
||||||
actor.AsCharacter->IsGPoseWet = value;
|
actor.AsCharacter->IsGPoseWet = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary> Change the hat-visibility state on actors. </summary>
|
||||||
public unsafe void ChangeHatState(ActorData data, bool value)
|
public unsafe void ChangeHatState(ActorData data, bool value)
|
||||||
{
|
{
|
||||||
foreach (var actor in data.Objects.Where(a => a.IsCharacter))
|
foreach (var actor in data.Objects.Where(a => a.IsCharacter))
|
||||||
actor.AsCharacter->DrawData.HideHeadgear(0, !value);
|
actor.AsCharacter->DrawData.HideHeadgear(0, !value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary> Change the weapon-visibility state on actors. </summary>
|
||||||
public unsafe void ChangeWeaponState(ActorData data, bool value)
|
public unsafe void ChangeWeaponState(ActorData data, bool value)
|
||||||
{
|
{
|
||||||
foreach (var actor in data.Objects.Where(a => a.IsCharacter))
|
foreach (var actor in data.Objects.Where(a => a.IsCharacter))
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using Glamourer.Automation;
|
||||||
using Glamourer.Customization;
|
using Glamourer.Customization;
|
||||||
using Glamourer.Events;
|
using Glamourer.Events;
|
||||||
using Glamourer.Interop.Penumbra;
|
using Glamourer.Interop.Penumbra;
|
||||||
|
|
@ -10,6 +11,11 @@ using Penumbra.GameData.Structs;
|
||||||
|
|
||||||
namespace Glamourer.State;
|
namespace Glamourer.State;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This class handles all game events that could cause a drawn model to change,
|
||||||
|
/// it always updates the base state for existing states,
|
||||||
|
/// and either discards the changes or updates the model state too.
|
||||||
|
/// </summary>
|
||||||
public class StateListener : IDisposable
|
public class StateListener : IDisposable
|
||||||
{
|
{
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
|
|
@ -22,6 +28,7 @@ public class StateListener : IDisposable
|
||||||
private readonly HeadGearVisibilityChanged _headGearVisibility;
|
private readonly HeadGearVisibilityChanged _headGearVisibility;
|
||||||
private readonly VisorStateChanged _visorState;
|
private readonly VisorStateChanged _visorState;
|
||||||
private readonly WeaponVisibilityChanged _weaponVisibility;
|
private readonly WeaponVisibilityChanged _weaponVisibility;
|
||||||
|
private readonly AutoDesignApplier _autoDesignApplier;
|
||||||
|
|
||||||
public bool Enabled
|
public bool Enabled
|
||||||
{
|
{
|
||||||
|
|
@ -31,7 +38,7 @@ public class StateListener : IDisposable
|
||||||
|
|
||||||
public StateListener(StateManager manager, ItemManager items, PenumbraService penumbra, ActorService actors, Configuration config,
|
public StateListener(StateManager manager, ItemManager items, PenumbraService penumbra, ActorService actors, Configuration config,
|
||||||
SlotUpdating slotUpdating, WeaponLoading weaponLoading, VisorStateChanged visorState, WeaponVisibilityChanged weaponVisibility,
|
SlotUpdating slotUpdating, WeaponLoading weaponLoading, VisorStateChanged visorState, WeaponVisibilityChanged weaponVisibility,
|
||||||
HeadGearVisibilityChanged headGearVisibility)
|
HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier)
|
||||||
{
|
{
|
||||||
_manager = manager;
|
_manager = manager;
|
||||||
_items = items;
|
_items = items;
|
||||||
|
|
@ -43,6 +50,7 @@ public class StateListener : IDisposable
|
||||||
_visorState = visorState;
|
_visorState = visorState;
|
||||||
_weaponVisibility = weaponVisibility;
|
_weaponVisibility = weaponVisibility;
|
||||||
_headGearVisibility = headGearVisibility;
|
_headGearVisibility = headGearVisibility;
|
||||||
|
_autoDesignApplier = autoDesignApplier;
|
||||||
|
|
||||||
if (Enabled)
|
if (Enabled)
|
||||||
Subscribe();
|
Subscribe();
|
||||||
|
|
@ -68,48 +76,83 @@ public class StateListener : IDisposable
|
||||||
Unsubscribe();
|
Unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary> The result of updating the base state of an ActorState. </summary>
|
||||||
private enum UpdateState
|
private enum UpdateState
|
||||||
{
|
{
|
||||||
|
/// <summary> The base state is the same as prior state. </summary>
|
||||||
NoChange,
|
NoChange,
|
||||||
|
|
||||||
|
/// <summary> The game requests an update to a state that does not agree with the actor state. </summary>
|
||||||
Transformed,
|
Transformed,
|
||||||
|
|
||||||
|
/// <summary> The base state changed compared to prior state. </summary>
|
||||||
Change,
|
Change,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked when a new draw object is created from a game object.
|
||||||
|
/// We need to update all state: Model ID, Customize and Equipment.
|
||||||
|
/// Weapons and meta flags are updated independently.
|
||||||
|
/// We also need to apply fixed designs here (TODO).
|
||||||
|
/// </summary>
|
||||||
private unsafe void OnCreatingCharacterBase(nint actorPtr, string _, nint modelPtr, nint customizePtr, nint equipDataPtr)
|
private unsafe void OnCreatingCharacterBase(nint actorPtr, string _, nint modelPtr, nint customizePtr, nint equipDataPtr)
|
||||||
{
|
{
|
||||||
// TODO: Fixed Designs.
|
|
||||||
var actor = (Actor)actorPtr;
|
var actor = (Actor)actorPtr;
|
||||||
var identifier = actor.GetIdentifier(_actors.AwaitedService);
|
var identifier = actor.GetIdentifier(_actors.AwaitedService);
|
||||||
|
|
||||||
var modelId = *(uint*)modelPtr;
|
var modelId = *(uint*)modelPtr;
|
||||||
ref var customize = ref *(Customize*)customizePtr;
|
ref var customize = ref *(Customize*)customizePtr;
|
||||||
if (_manager.TryGetValue(identifier, out var state))
|
if (_manager.TryGetValue(identifier, out var state))
|
||||||
|
{
|
||||||
|
_autoDesignApplier.Reduce(actor, identifier, state);
|
||||||
switch (UpdateBaseData(actor, state, modelId, customizePtr, equipDataPtr))
|
switch (UpdateBaseData(actor, state, modelId, customizePtr, equipDataPtr))
|
||||||
{
|
{
|
||||||
case UpdateState.Change: break;
|
case UpdateState.Change: break;
|
||||||
case UpdateState.Transformed: break;
|
case UpdateState.Transformed: break;
|
||||||
case UpdateState.NoChange:
|
case UpdateState.NoChange:
|
||||||
UpdateBaseData(actor, state, customize);
|
switch (UpdateBaseData(actor, state, customize))
|
||||||
|
{
|
||||||
|
case UpdateState.Transformed: break;
|
||||||
|
case UpdateState.Change: break;
|
||||||
|
case UpdateState.NoChange:
|
||||||
|
customize = state.ModelData.Customize;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
||||||
|
HandleEquipSlot(actor, state, slot, ref ((CharacterArmor*)equipDataPtr)[slot.ToIndex()]);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (_config.UseRestrictedGearProtection && modelId == 0)
|
if (modelId == 0)
|
||||||
ProtectRestrictedGear(equipDataPtr, customize.Race, customize.Gender);
|
ProtectRestrictedGear(equipDataPtr, customize.Race, customize.Gender);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A draw model loads a new equipment piece.
|
||||||
|
/// Update base data, apply or update model data, and protect against restricted gear.
|
||||||
|
/// </summary>
|
||||||
private void OnSlotUpdating(Model model, EquipSlot slot, Ref<CharacterArmor> armor, Ref<ulong> returnValue)
|
private void OnSlotUpdating(Model model, EquipSlot slot, Ref<CharacterArmor> armor, Ref<ulong> returnValue)
|
||||||
{
|
{
|
||||||
// TODO handle hat state
|
var actor = _penumbra.GameObjectFromDrawObject(model);
|
||||||
var actor = _penumbra.GameObjectFromDrawObject(model);
|
|
||||||
var customize = model.GetCustomize();
|
|
||||||
if (actor.Identifier(_actors.AwaitedService, out var identifier)
|
if (actor.Identifier(_actors.AwaitedService, out var identifier)
|
||||||
&& _manager.TryGetValue(identifier, out var state))
|
&& _manager.TryGetValue(identifier, out var state))
|
||||||
ApplyEquipmentPiece(actor, state, slot, ref armor.Value);
|
HandleEquipSlot(actor, state, slot, ref armor.Value);
|
||||||
|
|
||||||
if (_config.UseRestrictedGearProtection)
|
if (!_config.UseRestrictedGearProtection)
|
||||||
(_, armor.Value) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender);
|
return;
|
||||||
|
|
||||||
|
var customize = model.GetCustomize();
|
||||||
|
(_, armor.Value) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A game object loads a new weapon.
|
||||||
|
/// Update base data, apply or update model data.
|
||||||
|
/// Verify consistent weapon types.
|
||||||
|
/// </summary>
|
||||||
private void OnWeaponLoading(Actor actor, EquipSlot slot, Ref<CharacterWeapon> weapon)
|
private void OnWeaponLoading(Actor actor, EquipSlot slot, Ref<CharacterWeapon> weapon)
|
||||||
{
|
{
|
||||||
if (!actor.Identifier(_actors.AwaitedService, out var identifier)
|
if (!actor.Identifier(_actors.AwaitedService, out var identifier)
|
||||||
|
|
@ -117,134 +160,283 @@ public class StateListener : IDisposable
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ref var actorWeapon = ref weapon.Value;
|
ref var actorWeapon = ref weapon.Value;
|
||||||
var stateItem = state.ModelData.Item(slot);
|
var baseType = state.BaseData.Item(slot).Type;
|
||||||
if (actorWeapon.Set.Value != stateItem.ModelId.Value
|
var apply = false;
|
||||||
|| actorWeapon.Type.Value != stateItem.WeaponType
|
switch (UpdateBaseData(actor, state, slot, actorWeapon))
|
||||||
|| actorWeapon.Variant != stateItem.Variant)
|
|
||||||
{
|
{
|
||||||
var oldActorItem = state.BaseData.Item(slot);
|
// Do nothing. But this usually can not happen because the hooked function also writes to game objects later.
|
||||||
if (oldActorItem.ModelId.Value == actorWeapon.Set.Value
|
case UpdateState.Transformed: break;
|
||||||
&& oldActorItem.WeaponType.Value == actorWeapon.Type.Value
|
case UpdateState.Change:
|
||||||
&& oldActorItem.Variant == actorWeapon.Variant)
|
|
||||||
{
|
|
||||||
actorWeapon.Set = stateItem.ModelId;
|
|
||||||
actorWeapon.Type = stateItem.WeaponType;
|
|
||||||
actorWeapon.Variant = stateItem.Variant;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var identified = _items.Identify(slot, actorWeapon.Set, actorWeapon.Type, (byte)actorWeapon.Variant,
|
|
||||||
slot == EquipSlot.OffHand ? state.BaseData.Item(EquipSlot.MainHand).Type : FullEquipType.Unknown);
|
|
||||||
state.BaseData.SetItem(slot, identified);
|
|
||||||
if (state[slot, false] is not StateChanged.Source.Fixed)
|
if (state[slot, false] is not StateChanged.Source.Fixed)
|
||||||
{
|
{
|
||||||
state.ModelData.SetItem(slot, identified);
|
state.ModelData.SetItem(slot, state.BaseData.Item(slot));
|
||||||
state[slot, false] = StateChanged.Source.Game;
|
state[slot, false] = StateChanged.Source.Game;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
actorWeapon.Set = stateItem.ModelId;
|
apply = true;
|
||||||
actorWeapon.Type = stateItem.Variant;
|
|
||||||
actorWeapon.Variant = stateItem.Variant;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var stateStain = state.ModelData.Stain(slot);
|
if (state[slot, false] is not StateChanged.Source.Fixed)
|
||||||
if (actorWeapon.Stain.Value != stateStain.Value)
|
|
||||||
{
|
|
||||||
var oldActorStain = state.BaseData.Stain(slot);
|
|
||||||
if (state[slot, true] is not StateChanged.Source.Fixed)
|
|
||||||
{
|
|
||||||
state.ModelData.SetStain(slot, actorWeapon.Stain);
|
|
||||||
state[slot, true] = StateChanged.Source.Game;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
actorWeapon.Stain = stateStain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void ApplyCustomize(Actor actor, ActorState state, ref Customize customize)
|
|
||||||
{
|
|
||||||
var actorCustomize = actor.GetCustomize();
|
|
||||||
ref var oldActorCustomize = ref state.BaseData.Customize;
|
|
||||||
ref var stateCustomize = ref state.ModelData.Customize;
|
|
||||||
foreach (var idx in Enum.GetValues<CustomizeIndex>())
|
|
||||||
{
|
|
||||||
var value = customize[idx];
|
|
||||||
var actorValue = actorCustomize[idx];
|
|
||||||
if (value.Value != actorValue.Value)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var stateValue = stateCustomize[idx];
|
|
||||||
if (value.Value == stateValue.Value)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (oldActorCustomize[idx].Value == actorValue.Value)
|
|
||||||
{
|
|
||||||
customize[idx] = stateValue;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
oldActorCustomize[idx] = actorValue;
|
|
||||||
if (state[idx] is StateChanged.Source.Fixed)
|
|
||||||
{
|
{
|
||||||
state.ModelData.Customize[idx] = value;
|
state.ModelData.SetStain(slot, state.BaseData.Stain(slot));
|
||||||
state[idx] = StateChanged.Source.Game;
|
state[slot, true] = StateChanged.Source.Game;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
customize[idx] = stateValue;
|
apply = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
break;
|
||||||
|
case UpdateState.NoChange:
|
||||||
|
apply = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apply)
|
||||||
|
{
|
||||||
|
// Only allow overwriting identical weapons
|
||||||
|
var newWeapon = state.ModelData.Weapon(slot);
|
||||||
|
if (baseType is FullEquipType.Unknown || baseType == state.ModelData.Item(slot).Type)
|
||||||
|
actorWeapon = newWeapon;
|
||||||
|
else if (actorWeapon.Set.Value != 0)
|
||||||
|
actorWeapon = actorWeapon.With(newWeapon.Stain);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void ApplyEquipment(Actor actor, ActorState state, CharacterArmor* equipData)
|
/// <summary> Update base data for a single changed equipment slot. </summary>
|
||||||
|
private UpdateState UpdateBaseData(Actor actor, ActorState state, EquipSlot slot, CharacterArmor armor)
|
||||||
{
|
{
|
||||||
// TODO: Handle hat state
|
var actorArmor = actor.GetArmor(slot);
|
||||||
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
// The actor armor does not correspond to the model armor, thus the actor is transformed.
|
||||||
ApplyEquipmentPiece(actor, state, slot, ref *equipData++);
|
// This also prevents it from changing values due to hat state.
|
||||||
|
if (actorArmor.Value != armor.Value)
|
||||||
|
return UpdateState.Transformed;
|
||||||
|
|
||||||
|
var baseData = state.BaseData.Armor(slot);
|
||||||
|
var change = UpdateState.NoChange;
|
||||||
|
if (baseData.Stain != armor.Stain)
|
||||||
|
{
|
||||||
|
state.BaseData.SetStain(slot, armor.Stain);
|
||||||
|
change = UpdateState.Change;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseData.Set.Value != armor.Set.Value || baseData.Variant != armor.Variant)
|
||||||
|
{
|
||||||
|
var item = _items.Identify(slot, armor.Set, armor.Variant);
|
||||||
|
state.BaseData.SetItem(slot, item);
|
||||||
|
change = UpdateState.Change;
|
||||||
|
}
|
||||||
|
|
||||||
|
return change;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyEquipmentPiece(Actor actor, ActorState state, EquipSlot slot, ref CharacterArmor armor)
|
/// <summary> Handle a full equip slot update for base data and model data. </summary>
|
||||||
|
private void HandleEquipSlot(Actor actor, ActorState state, EquipSlot slot, ref CharacterArmor armor)
|
||||||
{
|
{
|
||||||
var changeState = UpdateBaseData(actor, state, slot, armor);
|
switch (UpdateBaseData(actor, state, slot, armor))
|
||||||
if (changeState is UpdateState.Transformed)
|
{
|
||||||
|
// Transformed also handles invisible hat state.
|
||||||
|
case UpdateState.Transformed: break;
|
||||||
|
// Base data changed equipment while actors were not there.
|
||||||
|
// Update model state if not on fixed design.
|
||||||
|
case UpdateState.Change:
|
||||||
|
var apply = false;
|
||||||
|
if (state[slot, false] is not StateChanged.Source.Fixed)
|
||||||
|
{
|
||||||
|
state.ModelData.SetItem(slot, state.BaseData.Item(slot));
|
||||||
|
state[slot, false] = StateChanged.Source.Game;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
apply = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state[slot, true] is not StateChanged.Source.Fixed)
|
||||||
|
{
|
||||||
|
state.ModelData.SetStain(slot, state.BaseData.Stain(slot));
|
||||||
|
state[slot, true] = StateChanged.Source.Game;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
apply = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apply)
|
||||||
|
armor = state.ModelData.Armor(slot);
|
||||||
|
|
||||||
|
break;
|
||||||
|
// Use current model data.
|
||||||
|
case UpdateState.NoChange:
|
||||||
|
armor = state.ModelData.Armor(slot);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Update base data for a single changed weapon slot. </summary>
|
||||||
|
private UpdateState UpdateBaseData(Actor _, ActorState state, EquipSlot slot, CharacterWeapon weapon)
|
||||||
|
{
|
||||||
|
var baseData = state.BaseData.Weapon(slot);
|
||||||
|
var change = UpdateState.NoChange;
|
||||||
|
|
||||||
|
if (baseData.Stain != weapon.Stain)
|
||||||
|
{
|
||||||
|
state.BaseData.SetStain(slot, weapon.Stain);
|
||||||
|
change = UpdateState.Change;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseData.Set.Value != weapon.Set.Value || baseData.Type.Value != weapon.Type.Value || baseData.Variant != weapon.Variant)
|
||||||
|
{
|
||||||
|
var item = _items.Identify(slot, weapon.Set, weapon.Type, (byte)weapon.Variant,
|
||||||
|
slot is EquipSlot.OffHand ? state.BaseData.Item(EquipSlot.MainHand).Type : FullEquipType.Unknown);
|
||||||
|
state.BaseData.SetItem(slot, item);
|
||||||
|
change = UpdateState.Change;
|
||||||
|
}
|
||||||
|
|
||||||
|
return change;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update the base data starting with the model id.
|
||||||
|
/// If the model id changed, and is not a transformation, we need to reload the entire base state from scratch.
|
||||||
|
/// Non-Humans are handled differently than humans.
|
||||||
|
/// </summary>
|
||||||
|
private unsafe UpdateState UpdateBaseData(Actor actor, ActorState state, uint modelId, nint customizeData, nint equipData)
|
||||||
|
{
|
||||||
|
// Model ID does not agree between game object and new draw object => Transformation.
|
||||||
|
if (modelId != (uint)actor.AsCharacter->CharacterData.ModelCharaId)
|
||||||
|
return UpdateState.Transformed;
|
||||||
|
|
||||||
|
// Model ID did not change to stored state.
|
||||||
|
if (modelId == state.BaseData.ModelId)
|
||||||
|
return UpdateState.NoChange;
|
||||||
|
|
||||||
|
// Model ID did change, reload entire state accordingly.
|
||||||
|
if (modelId == 0)
|
||||||
|
state.BaseData.LoadNonHuman(modelId, *(Customize*)customizeData, (byte*)equipData);
|
||||||
|
else
|
||||||
|
state.BaseData = _manager.FromActor(actor);
|
||||||
|
|
||||||
|
return UpdateState.Change;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update the customize base data of a state.
|
||||||
|
/// This should rarely result in changes,
|
||||||
|
/// 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)
|
||||||
|
{
|
||||||
|
// Customize array does not agree between game object and draw object => transformation.
|
||||||
|
if (!actor.GetCustomize().Equals(customize))
|
||||||
|
return UpdateState.Transformed;
|
||||||
|
|
||||||
|
// Customize array did not change to stored state.
|
||||||
|
if (state.BaseData.Customize.Equals(customize))
|
||||||
|
return UpdateState.NoChange;
|
||||||
|
|
||||||
|
// Update customize base state.
|
||||||
|
state.BaseData.Customize.Load(customize);
|
||||||
|
return UpdateState.Change;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Handle visor state changes made by the game. </summary>
|
||||||
|
private void OnVisorChange(Model model, Ref<bool> value)
|
||||||
|
{
|
||||||
|
// Find appropriate actor and state.
|
||||||
|
// We do not need to handle fixed designs,
|
||||||
|
// since a fixed design would already have established state-tracking.
|
||||||
|
var actor = _penumbra.GameObjectFromDrawObject(model);
|
||||||
|
if (!actor.Identifier(_actors.AwaitedService, out var identifier))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (changeState is UpdateState.NoChange)
|
if (!_manager.TryGetValue(identifier, out var state))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Update visor base state.
|
||||||
|
if (state.BaseData.SetVisor(value))
|
||||||
{
|
{
|
||||||
armor = state.ModelData.Armor(slot);
|
// if base state changed, either overwrite the actual value if we have fixed values,
|
||||||
|
// or overwrite the stored model state with the new one.
|
||||||
|
if (state[ActorState.MetaFlag.VisorState] is StateChanged.Source.Fixed)
|
||||||
|
value.Value = state.ModelData.IsVisorToggled();
|
||||||
|
else
|
||||||
|
_manager.ChangeVisorState(state, value, StateChanged.Source.Game);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var modelArmor = state.ModelData.Armor(slot);
|
// if base state did not change, overwrite the value with the model state one.
|
||||||
if (armor.Value == modelArmor.Value)
|
value.Value = state.ModelData.IsVisorToggled();
|
||||||
return;
|
|
||||||
|
|
||||||
if (state[slot, false] is StateChanged.Source.Fixed)
|
|
||||||
{
|
|
||||||
armor.Set = modelArmor.Set;
|
|
||||||
armor.Variant = modelArmor.Variant;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_manager.ChangeEquip(state, slot, state.BaseData.Item(slot), StateChanged.Source.Game);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state[slot, true] is StateChanged.Source.Fixed)
|
|
||||||
armor.Stain = modelArmor.Stain;
|
|
||||||
else
|
|
||||||
_manager.ChangeStain(state, slot, state.BaseData.Stain(slot), StateChanged.Source.Game);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary> Handle Hat Visibility changes. These act on the game object. </summary>
|
||||||
|
private void OnHeadGearVisibilityChange(Actor actor, Ref<bool> value)
|
||||||
|
{
|
||||||
|
// Find appropriate state.
|
||||||
|
// We do not need to handle fixed designs,
|
||||||
|
// if there is no model that caused a fixed design to exist yet,
|
||||||
|
// we also do not care about the invisible model.
|
||||||
|
if (!actor.Identifier(_actors.AwaitedService, out var identifier))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_manager.TryGetValue(identifier, out var state))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Update hat visibility state.
|
||||||
|
if (state.BaseData.SetHatVisible(value))
|
||||||
|
{
|
||||||
|
// if base state changed, either overwrite the actual value if we have fixed values,
|
||||||
|
// or overwrite the stored model state with the new one.
|
||||||
|
if (state[ActorState.MetaFlag.HatState] is StateChanged.Source.Fixed)
|
||||||
|
value.Value = state.ModelData.IsHatVisible();
|
||||||
|
else
|
||||||
|
_manager.ChangeHatState(state, value, StateChanged.Source.Game);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// if base state did not change, overwrite the value with the model state one.
|
||||||
|
value.Value = state.ModelData.IsHatVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Handle Weapon Visibility changes. These act on the game object. </summary>
|
||||||
|
private void OnWeaponVisibilityChange(Actor actor, Ref<bool> value)
|
||||||
|
{
|
||||||
|
// Find appropriate state.
|
||||||
|
// We do not need to handle fixed designs,
|
||||||
|
// if there is no model that caused a fixed design to exist yet,
|
||||||
|
// we also do not care about the invisible model.
|
||||||
|
if (!actor.Identifier(_actors.AwaitedService, out var identifier))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_manager.TryGetValue(identifier, out var state))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Update weapon visibility state.
|
||||||
|
if (state.BaseData.SetWeaponVisible(value))
|
||||||
|
{
|
||||||
|
// if base state changed, either overwrite the actual value if we have fixed values,
|
||||||
|
// or overwrite the stored model state with the new one.
|
||||||
|
if (state[ActorState.MetaFlag.WeaponState] is StateChanged.Source.Fixed)
|
||||||
|
value.Value = state.ModelData.IsWeaponVisible();
|
||||||
|
else
|
||||||
|
_manager.ChangeWeaponState(state, value, StateChanged.Source.Game);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// if base state did not change, overwrite the value with the model state one.
|
||||||
|
value.Value = state.ModelData.IsWeaponVisible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Protect a given equipment data array against restricted gear if enabled. </summary>
|
||||||
private unsafe void ProtectRestrictedGear(nint equipDataPtr, Race race, Gender gender)
|
private unsafe void ProtectRestrictedGear(nint equipDataPtr, Race race, Gender gender)
|
||||||
{
|
{
|
||||||
|
if (!_config.UseRestrictedGearProtection)
|
||||||
|
return;
|
||||||
|
|
||||||
var idx = 0;
|
var idx = 0;
|
||||||
var ptr = (CharacterArmor*)equipDataPtr;
|
var ptr = (CharacterArmor*)equipDataPtr;
|
||||||
for (var end = ptr + 10; ptr < end; ++ptr)
|
for (var end = ptr + 10; ptr < end; ++ptr)
|
||||||
|
|
@ -274,144 +466,4 @@ public class StateListener : IDisposable
|
||||||
_headGearVisibility.Unsubscribe(OnHeadGearVisibilityChange);
|
_headGearVisibility.Unsubscribe(OnHeadGearVisibilityChange);
|
||||||
_weaponVisibility.Unsubscribe(OnWeaponVisibilityChange);
|
_weaponVisibility.Unsubscribe(OnWeaponVisibilityChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
private UpdateState UpdateBaseData(Actor actor, ActorState state, EquipSlot slot, CharacterArmor armor)
|
|
||||||
{
|
|
||||||
var actorArmor = actor.GetArmor(slot);
|
|
||||||
// The actor armor does not correspond to the model armor, thus the actor is transformed.
|
|
||||||
// This also prevents it from changing values due to hat state.
|
|
||||||
if (actorArmor.Value != armor.Value)
|
|
||||||
return UpdateState.Transformed;
|
|
||||||
|
|
||||||
var baseData = state.BaseData.Armor(slot);
|
|
||||||
var change = UpdateState.NoChange;
|
|
||||||
if (baseData.Stain != armor.Stain)
|
|
||||||
{
|
|
||||||
state.BaseData.SetStain(slot, armor.Stain);
|
|
||||||
change = UpdateState.Change;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (baseData.Set.Value != armor.Set.Value || baseData.Variant != armor.Variant)
|
|
||||||
{
|
|
||||||
var item = _items.Identify(slot, armor.Set, armor.Variant);
|
|
||||||
state.BaseData.SetItem(slot, item);
|
|
||||||
change = UpdateState.Change;
|
|
||||||
}
|
|
||||||
|
|
||||||
return change;
|
|
||||||
}
|
|
||||||
|
|
||||||
private UpdateState UpdateBaseData(Actor actor, ActorState state, EquipSlot slot, CharacterWeapon weapon)
|
|
||||||
{
|
|
||||||
var baseData = state.BaseData.Weapon(slot);
|
|
||||||
var change = UpdateState.NoChange;
|
|
||||||
|
|
||||||
if (baseData.Stain != weapon.Stain)
|
|
||||||
{
|
|
||||||
state.BaseData.SetStain(slot, weapon.Stain);
|
|
||||||
change = UpdateState.Change;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (baseData.Set.Value != weapon.Set.Value || baseData.Type.Value != weapon.Type.Value || baseData.Variant != weapon.Variant)
|
|
||||||
{
|
|
||||||
var item = _items.Identify(slot, weapon.Set, weapon.Type, (byte)weapon.Variant,
|
|
||||||
slot is EquipSlot.OffHand ? state.BaseData.Item(EquipSlot.MainHand).Type : FullEquipType.Unknown);
|
|
||||||
state.BaseData.SetItem(slot, item);
|
|
||||||
change = UpdateState.Change;
|
|
||||||
}
|
|
||||||
|
|
||||||
return change;
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe UpdateState UpdateBaseData(Actor actor, ActorState state, uint modelId, nint customizeData, nint equipData)
|
|
||||||
{
|
|
||||||
if (modelId != (uint)actor.AsCharacter->CharacterData.ModelCharaId)
|
|
||||||
return UpdateState.Transformed;
|
|
||||||
|
|
||||||
if (modelId == state.BaseData.ModelId)
|
|
||||||
return UpdateState.NoChange;
|
|
||||||
|
|
||||||
if (modelId == 0)
|
|
||||||
state.BaseData.LoadNonHuman(modelId, *(Customize*)customizeData, (byte*)equipData);
|
|
||||||
else
|
|
||||||
state.BaseData = _manager.FromActor(actor);
|
|
||||||
|
|
||||||
return UpdateState.Change;
|
|
||||||
}
|
|
||||||
|
|
||||||
private UpdateState UpdateBaseData(Actor actor, ActorState state, Customize customize)
|
|
||||||
{
|
|
||||||
if (!actor.GetCustomize().Equals(customize))
|
|
||||||
return UpdateState.Transformed;
|
|
||||||
|
|
||||||
if (state.BaseData.Customize.Equals(customize))
|
|
||||||
return UpdateState.NoChange;
|
|
||||||
|
|
||||||
state.BaseData.Customize.Load(customize);
|
|
||||||
return UpdateState.Change;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnVisorChange(Model model, Ref<bool> value)
|
|
||||||
{
|
|
||||||
var actor = _penumbra.GameObjectFromDrawObject(model);
|
|
||||||
if (!actor.Identifier(_actors.AwaitedService, out var identifier))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!_manager.TryGetValue(identifier, out var state))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (state.BaseData.SetVisor(value))
|
|
||||||
{
|
|
||||||
if (state[ActorState.MetaFlag.VisorState] is StateChanged.Source.Fixed)
|
|
||||||
value.Value = state.ModelData.IsVisorToggled();
|
|
||||||
else
|
|
||||||
_manager.ChangeVisorState(state, value, StateChanged.Source.Game);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
value.Value = state.ModelData.IsVisorToggled();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnHeadGearVisibilityChange(Actor actor, Ref<bool> value)
|
|
||||||
{
|
|
||||||
if (!actor.Identifier(_actors.AwaitedService, out var identifier))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!_manager.TryGetValue(identifier, out var state))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (state.BaseData.SetHatVisible(value))
|
|
||||||
{
|
|
||||||
if (state[ActorState.MetaFlag.HatState] is StateChanged.Source.Fixed)
|
|
||||||
value.Value = state.ModelData.IsHatVisible();
|
|
||||||
else
|
|
||||||
_manager.ChangeHatState(state, value, StateChanged.Source.Game);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
value.Value = state.ModelData.IsHatVisible();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnWeaponVisibilityChange(Actor actor, Ref<bool> value)
|
|
||||||
{
|
|
||||||
if (!actor.Identifier(_actors.AwaitedService, out var identifier))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!_manager.TryGetValue(identifier, out var state))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (state.BaseData.SetWeaponVisible(value))
|
|
||||||
{
|
|
||||||
if (state[ActorState.MetaFlag.WeaponState] is StateChanged.Source.Fixed)
|
|
||||||
value.Value = state.ModelData.IsWeaponVisible();
|
|
||||||
else
|
|
||||||
_manager.ChangeWeaponState(state, value, StateChanged.Source.Game);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
value.Value = state.ModelData.IsWeaponVisible();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,12 @@ using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Cryptography;
|
|
||||||
using Glamourer.Customization;
|
using Glamourer.Customization;
|
||||||
using Glamourer.Designs;
|
using Glamourer.Designs;
|
||||||
using Glamourer.Events;
|
using Glamourer.Events;
|
||||||
using Glamourer.Interop;
|
using Glamourer.Interop;
|
||||||
using Glamourer.Interop.Penumbra;
|
|
||||||
using Glamourer.Interop.Structs;
|
using Glamourer.Interop.Structs;
|
||||||
using Glamourer.Services;
|
using Glamourer.Services;
|
||||||
using Glamourer.Structs;
|
|
||||||
using Lumina.Excel.GeneratedSheets;
|
|
||||||
using OtterGui.Log;
|
|
||||||
using Penumbra.GameData.Actors;
|
using Penumbra.GameData.Actors;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
|
|
@ -30,114 +25,20 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
|
||||||
private readonly ObjectManager _objects;
|
private readonly ObjectManager _objects;
|
||||||
private readonly StateEditor _editor;
|
private readonly StateEditor _editor;
|
||||||
|
|
||||||
private readonly PenumbraService _penumbra;
|
|
||||||
|
|
||||||
private readonly Dictionary<ActorIdentifier, ActorState> _states = new();
|
private readonly Dictionary<ActorIdentifier, ActorState> _states = new();
|
||||||
|
|
||||||
public StateManager(ActorService actors, ItemManager items, CustomizationService customizations, VisorService visor, StateChanged @event,
|
public StateManager(ActorService actors, ItemManager items, CustomizationService customizations, VisorService visor, StateChanged @event,
|
||||||
PenumbraService penumbra, ObjectManager objects, StateEditor editor)
|
ObjectManager objects, StateEditor editor)
|
||||||
{
|
{
|
||||||
_actors = actors;
|
_actors = actors;
|
||||||
_items = items;
|
_items = items;
|
||||||
_customizations = customizations;
|
_customizations = customizations;
|
||||||
_visor = visor;
|
_visor = visor;
|
||||||
_event = @event;
|
_event = @event;
|
||||||
_penumbra = penumbra;
|
|
||||||
_objects = objects;
|
_objects = objects;
|
||||||
_editor = editor;
|
_editor = editor;
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
|
||||||
BaseData = 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<KeyValuePair<ActorIdentifier, ActorState>> GetEnumerator()
|
public IEnumerator<KeyValuePair<ActorIdentifier, ActorState>> GetEnumerator()
|
||||||
=> _states.GetEnumerator();
|
=> _states.GetEnumerator();
|
||||||
|
|
||||||
|
|
@ -162,15 +63,54 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
|
||||||
public IEnumerable<ActorState> Values
|
public IEnumerable<ActorState> Values
|
||||||
=> _states.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 bool GetOrCreate(ActorIdentifier identifier, Actor actor, [NotNullWhen(true)] out ActorState? state)
|
||||||
|
{
|
||||||
|
if (TryGetValue(identifier, out state))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var designData = FromActor(actor);
|
||||||
|
// Initial Creation has identical base and model data.
|
||||||
|
state = new ActorState(identifier)
|
||||||
|
{
|
||||||
|
ModelData = designData,
|
||||||
|
BaseData = designData,
|
||||||
|
};
|
||||||
|
// 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)
|
public unsafe DesignData FromActor(Actor actor)
|
||||||
{
|
{
|
||||||
var ret = new DesignData();
|
var ret = new DesignData();
|
||||||
|
// If the given actor is not a character, just return a default character.
|
||||||
if (!actor.IsCharacter)
|
if (!actor.IsCharacter)
|
||||||
{
|
{
|
||||||
ret.SetDefaultEquipment(_items);
|
ret.SetDefaultEquipment(_items);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Model ID is only unambiguously contained in the game object.
|
||||||
|
// The draw object only has the object type.
|
||||||
|
// TODO do this right.
|
||||||
if (actor.AsCharacter->CharacterData.ModelCharaId != 0)
|
if (actor.AsCharacter->CharacterData.ModelCharaId != 0)
|
||||||
{
|
{
|
||||||
ret.LoadNonHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId, *(Customize*)&actor.AsCharacter->DrawData.CustomizeData,
|
ret.LoadNonHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId, *(Customize*)&actor.AsCharacter->DrawData.CustomizeData,
|
||||||
|
|
@ -182,14 +122,23 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
|
||||||
CharacterWeapon main;
|
CharacterWeapon main;
|
||||||
CharacterWeapon off;
|
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);
|
ret.SetHatVisible(!actor.AsCharacter->DrawData.IsHatHidden);
|
||||||
|
|
||||||
|
// Use the draw object if it is a human.
|
||||||
if (model.IsHuman)
|
if (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 head = ret.IsHatVisible() ? model.GetArmor(EquipSlot.Head) : actor.GetArmor(EquipSlot.Head);
|
||||||
var headItem = _items.Identify(EquipSlot.Head, head.Set, head.Variant);
|
var headItem = _items.Identify(EquipSlot.Head, head.Set, head.Variant);
|
||||||
ret.SetItem(EquipSlot.Head, headItem);
|
ret.SetItem(EquipSlot.Head, headItem);
|
||||||
ret.SetStain(EquipSlot.Head, head.Stain);
|
ret.SetStain(EquipSlot.Head, head.Stain);
|
||||||
|
|
||||||
|
// The other slots can be used from the draw object.
|
||||||
foreach (var slot in EquipSlotExtensions.EqdpSlots.Skip(1))
|
foreach (var slot in EquipSlotExtensions.EqdpSlots.Skip(1))
|
||||||
{
|
{
|
||||||
var armor = model.GetArmor(slot);
|
var armor = model.GetArmor(slot);
|
||||||
|
|
@ -198,12 +147,17 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
|
||||||
ret.SetStain(slot, armor.Stain);
|
ret.SetStain(slot, armor.Stain);
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.Customize = model.GetCustomize();
|
// Weapons use the draw objects of the weapons, but require the game object either way.
|
||||||
(_, _, main, off) = model.GetWeapons(actor);
|
(_, _, 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(_visor.GetVisorState(model));
|
ret.SetVisor(_visor.GetVisorState(model));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// Obtain all data from the game object.
|
||||||
|
ret.Customize = actor.GetCustomize();
|
||||||
|
|
||||||
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
||||||
{
|
{
|
||||||
var armor = actor.GetArmor(slot);
|
var armor = actor.GetArmor(slot);
|
||||||
|
|
@ -212,12 +166,12 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
|
||||||
ret.SetStain(slot, armor.Stain);
|
ret.SetStain(slot, armor.Stain);
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.Customize = actor.GetCustomize();
|
main = actor.GetMainhand();
|
||||||
main = actor.GetMainhand();
|
off = actor.GetOffhand();
|
||||||
off = actor.GetOffhand();
|
|
||||||
ret.SetVisor(actor.AsCharacter->DrawData.IsVisorToggled);
|
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 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);
|
var offItem = _items.Identify(EquipSlot.OffHand, off.Set, off.Type, (byte)off.Variant, mainItem.Type);
|
||||||
ret.SetItem(EquipSlot.MainHand, mainItem);
|
ret.SetItem(EquipSlot.MainHand, mainItem);
|
||||||
|
|
@ -225,39 +179,232 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
|
||||||
ret.SetItem(EquipSlot.OffHand, offItem);
|
ret.SetItem(EquipSlot.OffHand, offItem);
|
||||||
ret.SetStain(EquipSlot.OffHand, off.Stain);
|
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);
|
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);
|
ret.SetWeaponVisible(!actor.AsCharacter->DrawData.IsWeaponHidden);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Change Values
|
||||||
|
|
||||||
/// <summary> Change a customization value. </summary>
|
/// <summary> Change a customization value. </summary>
|
||||||
public void ChangeCustomize(ActorState state, ActorData data, CustomizeIndex idx, CustomizeValue value, StateChanged.Source source,
|
public void ChangeCustomize(ActorState state, CustomizeIndex idx, CustomizeValue value, StateChanged.Source source)
|
||||||
bool force)
|
|
||||||
{
|
{
|
||||||
ref var s = ref state[idx];
|
// Update state data.
|
||||||
if (s is StateChanged.Source.Fixed && source is StateChanged.Source.Game)
|
var old = state.ModelData.Customize[idx];
|
||||||
return;
|
|
||||||
|
|
||||||
var oldValue = state.ModelData.Customize[idx];
|
|
||||||
if (oldValue == value && !force)
|
|
||||||
return;
|
|
||||||
|
|
||||||
state.ModelData.Customize[idx] = value;
|
state.ModelData.Customize[idx] = value;
|
||||||
|
state[idx] = source;
|
||||||
|
|
||||||
Glamourer.Log.Excessive(
|
// Update draw objects.
|
||||||
$"Changed customize {idx.ToDefaultName()} for {state.Identifier} ({string.Join(", ", data.Objects.Select(o => $"0x{o.Address}"))}) from {oldValue.Value} to {value.Value}.");
|
_objects.Update();
|
||||||
_event.Invoke(StateChanged.Type.Customize, source, state, data, (oldValue, value, idx));
|
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
|
||||||
|
if (source is StateChanged.Source.Manual)
|
||||||
|
_editor.ChangeCustomize(objects, state.ModelData.Customize);
|
||||||
|
|
||||||
|
// Meta.
|
||||||
|
Glamourer.Log.Verbose(
|
||||||
|
$"Set {idx.ToDefaultName()} customizations in state {state.Identifier} from {old.Value} to {value.Value}. [Affecting {objects.ToLazyString("nothing")}.]");
|
||||||
|
_event.Invoke(StateChanged.Type.Customize, source, state, objects, (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)
|
||||||
|
{
|
||||||
|
// Update state data.
|
||||||
|
var old = state.ModelData.Customize;
|
||||||
|
var (customize, applied) = _customizations.Combine(state.ModelData.Customize, customizeInput, apply);
|
||||||
|
if (applied == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
state.ModelData.Customize = customize;
|
||||||
|
foreach (var type in Enum.GetValues<CustomizeIndex>())
|
||||||
|
{
|
||||||
|
var flag = type.ToFlag();
|
||||||
|
if (applied.HasFlag(flag))
|
||||||
|
state[type] = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update draw objects.
|
||||||
|
_objects.Update();
|
||||||
|
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
|
||||||
|
if (source is StateChanged.Source.Manual)
|
||||||
|
_editor.ChangeCustomize(objects, state.ModelData.Customize);
|
||||||
|
|
||||||
|
// Meta.
|
||||||
|
Glamourer.Log.Verbose(
|
||||||
|
$"Set {applied} customizations in state {state.Identifier} from {old} to {customize}. [Affecting {objects.ToLazyString("nothing")}.]");
|
||||||
|
_event.Invoke(StateChanged.Type.Customize, source, state, objects, (old, customize, 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)"/> instead. </remarks>
|
||||||
|
public void ChangeItem(ActorState state, EquipSlot slot, EquipItem item, StateChanged.Source source)
|
||||||
|
{
|
||||||
|
// Update state data.
|
||||||
|
var old = state.ModelData.Item(slot);
|
||||||
|
state.ModelData.SetItem(slot, item);
|
||||||
|
state[slot, false] = source;
|
||||||
|
var type = slot is EquipSlot.MainHand or EquipSlot.OffHand ? StateChanged.Type.Weapon : StateChanged.Type.Equip;
|
||||||
|
|
||||||
|
// Update draw objects.
|
||||||
|
_objects.Update();
|
||||||
|
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
|
||||||
|
if (source is StateChanged.Source.Manual)
|
||||||
|
if (type == StateChanged.Type.Equip)
|
||||||
|
_editor.ChangeArmor(objects, slot, state.ModelData.Armor(slot));
|
||||||
|
else
|
||||||
|
_editor.ChangeWeapon(objects, slot, state.ModelData.Item(slot), state.ModelData.Stain(slot));
|
||||||
|
|
||||||
|
// Meta.
|
||||||
|
Glamourer.Log.Verbose(
|
||||||
|
$"Set {slot.ToName()} in state {state.Identifier} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}). [Affecting {objects.ToLazyString("nothing")}.]");
|
||||||
|
_event.Invoke(type, source, state, objects, (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)
|
||||||
|
{
|
||||||
|
// Update state data.
|
||||||
|
var old = state.ModelData.Item(slot);
|
||||||
|
var oldStain = state.ModelData.Stain(slot);
|
||||||
|
state.ModelData.SetItem(slot, item);
|
||||||
|
state.ModelData.SetStain(slot, stain);
|
||||||
|
state[slot, false] = source;
|
||||||
|
state[slot, true] = source;
|
||||||
|
var type = slot is EquipSlot.MainHand or EquipSlot.OffHand ? StateChanged.Type.Weapon : StateChanged.Type.Equip;
|
||||||
|
|
||||||
|
// Update draw objects.
|
||||||
|
_objects.Update();
|
||||||
|
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
|
||||||
|
if (source is StateChanged.Source.Manual)
|
||||||
|
if (type == StateChanged.Type.Equip)
|
||||||
|
_editor.ChangeArmor(objects, slot, state.ModelData.Armor(slot));
|
||||||
|
else
|
||||||
|
_editor.ChangeWeapon(objects, slot, state.ModelData.Item(slot), state.ModelData.Stain(slot));
|
||||||
|
|
||||||
|
// Meta.
|
||||||
|
Glamourer.Log.Verbose(
|
||||||
|
$"Set {slot.ToName()} in state {state.Identifier} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}) and its stain from {oldStain.Value} to {stain.Value}. [Affecting {objects.ToLazyString("nothing")}.]");
|
||||||
|
_event.Invoke(type, source, state, objects, (old, item, slot));
|
||||||
|
_event.Invoke(StateChanged.Type.Stain, source, state, objects, (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)"/> instead. </remarks>
|
||||||
|
public void ChangeStain(ActorState state, EquipSlot slot, StainId stain, StateChanged.Source source)
|
||||||
|
{
|
||||||
|
// Update state data.
|
||||||
|
var old = state.ModelData.Stain(slot);
|
||||||
|
state.ModelData.SetStain(slot, stain);
|
||||||
|
state[slot, true] = source;
|
||||||
|
|
||||||
|
// Update draw objects.
|
||||||
|
_objects.Update();
|
||||||
|
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
|
||||||
|
if (source is StateChanged.Source.Manual)
|
||||||
|
_editor.ChangeStain(objects, slot, stain);
|
||||||
|
|
||||||
|
// Meta.
|
||||||
|
Glamourer.Log.Verbose(
|
||||||
|
$"Set {slot.ToName()} stain in state {state.Identifier} from {old.Value} to {stain.Value}. [Affecting {objects.ToLazyString("nothing")}.]");
|
||||||
|
_event.Invoke(StateChanged.Type.Stain, source, state, objects, (old, stain, slot));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Change hat visibility. </summary>
|
||||||
|
public void ChangeHatState(ActorState state, bool value, StateChanged.Source source)
|
||||||
|
{
|
||||||
|
// Update state data.
|
||||||
|
var old = state.ModelData.IsHatVisible();
|
||||||
|
state.ModelData.SetHatVisible(value);
|
||||||
|
state[ActorState.MetaFlag.HatState] = source;
|
||||||
|
|
||||||
|
// Update draw objects / game objects.
|
||||||
|
_objects.Update();
|
||||||
|
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
|
||||||
|
if (source is StateChanged.Source.Manual)
|
||||||
|
_editor.ChangeHatState(objects, value);
|
||||||
|
|
||||||
|
// Meta.
|
||||||
|
Glamourer.Log.Verbose(
|
||||||
|
$"Set Head Gear Visibility in state {state.Identifier} from {old} to {value}. [Affecting {objects.ToLazyString("nothing")}.]");
|
||||||
|
_event.Invoke(StateChanged.Type.Other, source, state, objects, (old, value, ActorState.MetaFlag.HatState));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Change weapon visibility. </summary>
|
||||||
|
public void ChangeWeaponState(ActorState state, bool value, StateChanged.Source source)
|
||||||
|
{
|
||||||
|
// Update state data.
|
||||||
|
var old = state.ModelData.IsWeaponVisible();
|
||||||
|
state.ModelData.SetWeaponVisible(value);
|
||||||
|
state[ActorState.MetaFlag.WeaponState] = source;
|
||||||
|
|
||||||
|
// Update draw objects / game objects.
|
||||||
|
_objects.Update();
|
||||||
|
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
|
||||||
|
if (source is StateChanged.Source.Manual)
|
||||||
|
_editor.ChangeWeaponState(objects, value);
|
||||||
|
|
||||||
|
// Meta.
|
||||||
|
Glamourer.Log.Verbose(
|
||||||
|
$"Set Weapon Visibility in state {state.Identifier} from {old} to {value}. [Affecting {objects.ToLazyString("nothing")}.]");
|
||||||
|
_event.Invoke(StateChanged.Type.Other, source, state, objects, (old, value, ActorState.MetaFlag.WeaponState));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Change visor state. </summary>
|
||||||
|
public void ChangeVisorState(ActorState state, bool value, StateChanged.Source source)
|
||||||
|
{
|
||||||
|
// Update state data.
|
||||||
|
var old = state.ModelData.IsVisorToggled();
|
||||||
|
state.ModelData.SetVisor(value);
|
||||||
|
state[ActorState.MetaFlag.VisorState] = source;
|
||||||
|
|
||||||
|
// Update draw objects.
|
||||||
|
_objects.Update();
|
||||||
|
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
|
||||||
|
if (source is StateChanged.Source.Manual)
|
||||||
|
_editor.ChangeVisor(objects, value);
|
||||||
|
|
||||||
|
// Meta.
|
||||||
|
Glamourer.Log.Verbose(
|
||||||
|
$"Set Visor State in state {state.Identifier} from {old} to {value}. [Affecting {objects.ToLazyString("nothing")}.]");
|
||||||
|
_event.Invoke(StateChanged.Type.Other, source, state, objects, (old, value, ActorState.MetaFlag.VisorState));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Set GPose Wetness. </summary>
|
||||||
|
public void ChangeWetness(ActorState state, bool value, StateChanged.Source source)
|
||||||
|
{
|
||||||
|
// Update state data.
|
||||||
|
var old = state.ModelData.IsWet();
|
||||||
|
state.ModelData.SetIsWet(value);
|
||||||
|
state[ActorState.MetaFlag.Wetness] = source;
|
||||||
|
|
||||||
|
// Update draw objects / game objects.
|
||||||
|
_objects.Update();
|
||||||
|
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
|
||||||
|
_editor.ChangeWetness(objects, value);
|
||||||
|
|
||||||
|
// Meta.
|
||||||
|
Glamourer.Log.Verbose(
|
||||||
|
$"Set Wetness in state {state.Identifier} from {old} to {value}. [Affecting {objects.ToLazyString("nothing")}.]");
|
||||||
|
_event.Invoke(StateChanged.Type.Other, state[ActorState.MetaFlag.Wetness], state, objects, (old, value, ActorState.MetaFlag.Wetness));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
public void ApplyDesign(Design design, ActorState state)
|
public void ApplyDesign(Design design, ActorState state)
|
||||||
{
|
{
|
||||||
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
void HandleEquip(EquipSlot slot, bool applyPiece, bool applyStain)
|
||||||
{
|
{
|
||||||
switch (design.DoApplyEquip(slot), design.DoApplyStain(slot))
|
switch (applyPiece, applyStain)
|
||||||
{
|
{
|
||||||
case (false, false): continue;
|
case (false, false): break;
|
||||||
case (true, false):
|
case (true, false):
|
||||||
ChangeEquip(state, slot, design.DesignData.Item(slot), StateChanged.Source.Manual);
|
ChangeItem(state, slot, design.DesignData.Item(slot), StateChanged.Source.Manual);
|
||||||
break;
|
break;
|
||||||
case (false, true):
|
case (false, true):
|
||||||
ChangeStain(state, slot, design.DesignData.Stain(slot), StateChanged.Source.Manual);
|
ChangeStain(state, slot, design.DesignData.Stain(slot), StateChanged.Source.Manual);
|
||||||
|
|
@ -267,6 +414,19 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
||||||
|
HandleEquip(slot, design.DoApplyEquip(slot), design.DoApplyStain(slot));
|
||||||
|
|
||||||
|
HandleEquip(EquipSlot.MainHand,
|
||||||
|
design.DoApplyEquip(EquipSlot.MainHand)
|
||||||
|
&& design.DesignData.Item(EquipSlot.MainHand).Type == state.BaseData.Item(EquipSlot.MainHand).Type,
|
||||||
|
design.DoApplyStain(EquipSlot.MainHand));
|
||||||
|
HandleEquip(EquipSlot.OffHand,
|
||||||
|
design.DoApplyEquip(EquipSlot.OffHand)
|
||||||
|
&& design.DesignData.Item(EquipSlot.OffHand).Type == state.BaseData.Item(EquipSlot.OffHand).Type,
|
||||||
|
design.DoApplyStain(EquipSlot.OffHand));
|
||||||
|
|
||||||
if (design.DoApplyHatVisible())
|
if (design.DoApplyHatVisible())
|
||||||
ChangeHatState(state, design.DesignData.IsHatVisible(), StateChanged.Source.Manual);
|
ChangeHatState(state, design.DesignData.IsHatVisible(), StateChanged.Source.Manual);
|
||||||
if (design.DoApplyWeaponVisible())
|
if (design.DoApplyWeaponVisible())
|
||||||
|
|
@ -274,17 +434,29 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
|
||||||
if (design.DoApplyVisorToggle())
|
if (design.DoApplyVisorToggle())
|
||||||
ChangeVisorState(state, design.DesignData.IsVisorToggled(), StateChanged.Source.Manual);
|
ChangeVisorState(state, design.DesignData.IsVisorToggled(), StateChanged.Source.Manual);
|
||||||
if (design.DoApplyWetness())
|
if (design.DoApplyWetness())
|
||||||
ChangeWetness(state, design.DesignData.IsWet());
|
ChangeWetness(state, design.DesignData.IsWet(), StateChanged.Source.Manual);
|
||||||
|
ChangeCustomize(state, design.DesignData.Customize, design.ApplyCustomize, StateChanged.Source.Manual);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ResetState(ActorState state)
|
public void ResetState(ActorState state)
|
||||||
{
|
{
|
||||||
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
|
|
||||||
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
||||||
{
|
|
||||||
ChangeEquip(state, slot, state.BaseData.Item(slot), state.BaseData.Stain(slot), StateChanged.Source.Game);
|
ChangeEquip(state, slot, state.BaseData.Item(slot), state.BaseData.Stain(slot), StateChanged.Source.Game);
|
||||||
_editor.ChangeArmor(state, objects, slot);
|
|
||||||
}
|
ChangeEquip(state, EquipSlot.MainHand, state.BaseData.Item(EquipSlot.MainHand), state.BaseData.Stain(EquipSlot.MainHand),
|
||||||
|
StateChanged.Source.Game);
|
||||||
|
ChangeEquip(state, EquipSlot.OffHand, state.BaseData.Item(EquipSlot.OffHand), state.BaseData.Stain(EquipSlot.OffHand),
|
||||||
|
StateChanged.Source.Game);
|
||||||
|
ChangeHatState(state, state.BaseData.IsHatVisible(), StateChanged.Source.Game);
|
||||||
|
ChangeVisorState(state, state.BaseData.IsVisorToggled(), StateChanged.Source.Game);
|
||||||
|
ChangeWeaponState(state, state.BaseData.IsWeaponVisible(), StateChanged.Source.Game);
|
||||||
|
ChangeWetness(state, false, StateChanged.Source.Game);
|
||||||
|
ChangeCustomize(state, state.BaseData.Customize, CustomizeFlagExtensions.All, StateChanged.Source.Game);
|
||||||
|
|
||||||
|
_objects.Update();
|
||||||
|
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
|
||||||
|
foreach (var actor in objects.Objects)
|
||||||
|
ReapplyState(actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ReapplyState(Actor actor)
|
public void ReapplyState(Actor actor)
|
||||||
|
|
@ -292,215 +464,24 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
|
||||||
if (!GetOrCreate(actor, out var state))
|
if (!GetOrCreate(actor, out var state))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_objects.Update();
|
var mdl = actor.Model;
|
||||||
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
|
if (!mdl.IsHuman)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var data = new ActorData(actor, string.Empty);
|
||||||
|
var customizeFlags = Customize.Compare(mdl.GetCustomize(), state.ModelData.Customize);
|
||||||
|
|
||||||
|
_editor.ChangeCustomize(data, state.ModelData.Customize);
|
||||||
|
if (customizeFlags.RequiresRedraw())
|
||||||
|
return;
|
||||||
|
|
||||||
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
foreach (var slot in EquipSlotExtensions.EqdpSlots)
|
||||||
_editor.ChangeArmor(state, objects, slot);
|
_editor.ChangeArmor(data, slot, state.ModelData.Armor(slot));
|
||||||
|
_editor.ChangeMainhand(data, state.ModelData.Item(EquipSlot.MainHand), state.ModelData.Stain(EquipSlot.MainHand));
|
||||||
|
_editor.ChangeOffhand(data, state.ModelData.Item(EquipSlot.OffHand), state.ModelData.Stain(EquipSlot.OffHand));
|
||||||
|
_editor.ChangeWetness(data, false);
|
||||||
|
_editor.ChangeWeaponState(data, state.ModelData.IsWeaponVisible());
|
||||||
|
_editor.ChangeHatState(data, state.ModelData.IsHatVisible());
|
||||||
|
_editor.ChangeVisor(data, state.ModelData.IsVisorToggled());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ChangeEquip(ActorState state, EquipSlot slot, EquipItem item, StateChanged.Source source)
|
|
||||||
{
|
|
||||||
var old = state.ModelData.Item(slot);
|
|
||||||
state.ModelData.SetItem(slot, item);
|
|
||||||
state[slot, false] = source;
|
|
||||||
_objects.Update();
|
|
||||||
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
|
|
||||||
if (source is StateChanged.Source.Manual)
|
|
||||||
_editor.ChangeArmor(state, objects, slot);
|
|
||||||
Glamourer.Log.Verbose(
|
|
||||||
$"Set {slot.ToName()} equipment piece in state {state.Identifier} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}). [Affecting {objects.ToLazyString("nothing")}.]");
|
|
||||||
_event.Invoke(StateChanged.Type.Equip, source, state, objects, (old, item, slot));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ChangeEquip(ActorState state, EquipSlot slot, EquipItem item, StainId stain, StateChanged.Source source)
|
|
||||||
{
|
|
||||||
var old = state.ModelData.Item(slot);
|
|
||||||
var oldStain = state.ModelData.Stain(slot);
|
|
||||||
state.ModelData.SetItem(slot, item);
|
|
||||||
state.ModelData.SetStain(slot, stain);
|
|
||||||
state[slot, false] = source;
|
|
||||||
state[slot, true] = source;
|
|
||||||
_objects.Update();
|
|
||||||
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
|
|
||||||
if (source is StateChanged.Source.Manual)
|
|
||||||
_editor.ChangeArmor(state, objects, slot);
|
|
||||||
Glamourer.Log.Verbose(
|
|
||||||
$"Set {slot.ToName()} equipment piece in state {state.Identifier} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}) and its stain from {oldStain.Value} to {stain.Value}. [Affecting {objects.ToLazyString("nothing")}.]");
|
|
||||||
_event.Invoke(StateChanged.Type.Equip, source, state, objects, (old, item, slot));
|
|
||||||
_event.Invoke(StateChanged.Type.Stain, source, state, objects, (oldStain, stain, slot));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ChangeStain(ActorState state, EquipSlot slot, StainId stain, StateChanged.Source source)
|
|
||||||
{
|
|
||||||
var old = state.ModelData.Stain(slot);
|
|
||||||
state.ModelData.SetStain(slot, stain);
|
|
||||||
state[slot, true] = source;
|
|
||||||
_objects.Update();
|
|
||||||
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
|
|
||||||
if (source is StateChanged.Source.Manual)
|
|
||||||
_editor.ChangeArmor(state, objects, slot);
|
|
||||||
Glamourer.Log.Verbose(
|
|
||||||
$"Set {slot.ToName()} stain in state {state.Identifier} from {old.Value} to {stain.Value}. [Affecting {objects.ToLazyString("nothing")}.]");
|
|
||||||
_event.Invoke(StateChanged.Type.Stain, source, state, objects, (old, stain, slot));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ChangeHatState(ActorState state, bool value, StateChanged.Source source)
|
|
||||||
{
|
|
||||||
var old = state.ModelData.IsHatVisible();
|
|
||||||
state.ModelData.SetHatVisible(value);
|
|
||||||
state[ActorState.MetaFlag.HatState] = source;
|
|
||||||
_objects.Update();
|
|
||||||
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
|
|
||||||
if (source is StateChanged.Source.Manual)
|
|
||||||
_editor.ChangeHatState(objects, value);
|
|
||||||
Glamourer.Log.Verbose(
|
|
||||||
$"Set Head Gear Visibility in state {state.Identifier} from {old} to {value}. [Affecting {objects.ToLazyString("nothing")}.]");
|
|
||||||
_event.Invoke(StateChanged.Type.Other, source, state, objects, (old, value, ActorState.MetaFlag.HatState));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ChangeWeaponState(ActorState state, bool value, StateChanged.Source source)
|
|
||||||
{
|
|
||||||
var old = state.ModelData.IsWeaponVisible();
|
|
||||||
state.ModelData.SetWeaponVisible(value);
|
|
||||||
state[ActorState.MetaFlag.WeaponState] = source;
|
|
||||||
_objects.Update();
|
|
||||||
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
|
|
||||||
if (source is StateChanged.Source.Manual)
|
|
||||||
_editor.ChangeWeaponState(objects, value);
|
|
||||||
Glamourer.Log.Verbose(
|
|
||||||
$"Set Weapon Visibility in state {state.Identifier} from {old} to {value}. [Affecting {objects.ToLazyString("nothing")}.]");
|
|
||||||
_event.Invoke(StateChanged.Type.Other, source, state, objects, (old, value, ActorState.MetaFlag.WeaponState));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ChangeVisorState(ActorState state, bool value, StateChanged.Source source)
|
|
||||||
{
|
|
||||||
var old = state.ModelData.IsVisorToggled();
|
|
||||||
state.ModelData.SetVisor(value);
|
|
||||||
state[ActorState.MetaFlag.VisorState] = source;
|
|
||||||
_objects.Update();
|
|
||||||
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
|
|
||||||
if (source is StateChanged.Source.Manual)
|
|
||||||
_editor.ChangeVisor(objects, value);
|
|
||||||
Glamourer.Log.Verbose(
|
|
||||||
$"Set Visor State in state {state.Identifier} from {old} to {value}. [Affecting {objects.ToLazyString("nothing")}.]");
|
|
||||||
_event.Invoke(StateChanged.Type.Other, source, state, objects, (old, value, ActorState.MetaFlag.VisorState));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ChangeWetness(ActorState state, bool value)
|
|
||||||
{
|
|
||||||
var old = state.ModelData.IsWet();
|
|
||||||
state.ModelData.SetIsWet(value);
|
|
||||||
state[ActorState.MetaFlag.Wetness] = value ? StateChanged.Source.Manual : StateChanged.Source.Game;
|
|
||||||
_objects.Update();
|
|
||||||
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
|
|
||||||
_editor.ChangeWetness(objects, value);
|
|
||||||
Glamourer.Log.Verbose(
|
|
||||||
$"Set Wetness in state {state.Identifier} from {old} to {value}. [Affecting {objects.ToLazyString("nothing")}.]");
|
|
||||||
_event.Invoke(StateChanged.Type.Other, state[ActorState.MetaFlag.Wetness], state, objects, (old, value, ActorState.MetaFlag.Wetness));
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
///// <summary> Change whether to apply a specific customize value. </summary>
|
|
||||||
//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);
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
|
|
||||||
//
|
|
||||||
///// <summary> Change a weapon. </summary>
|
|
||||||
//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;
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
///// <summary> Change whether to apply a specific equipment piece. </summary>
|
|
||||||
//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);
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
///// <summary> Change the stain for any equipment piece. </summary>
|
|
||||||
//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));
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
///// <summary> Change whether to apply a specific stain. </summary>
|
|
||||||
//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);
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue