This commit is contained in:
Ottermandias 2023-06-28 01:39:53 +02:00
parent 63e82d19dc
commit e57538561f
34 changed files with 2428 additions and 720 deletions

View file

@ -4,6 +4,7 @@ using System.Linq;
using System.Reflection;
using Dalamud;
using Dalamud.Data;
using Dalamud.Logging;
using Dalamud.Plugin;
using Dalamud.Utility;
using Lumina.Excel;
@ -68,7 +69,7 @@ public partial class CustomizationOptions
{
var tmp = new TemporaryData(gameData, this);
_icons = new IconStorage(pi, gameData, _customizationSets.Length * 50);
_valid = tmp.Valid;
_valid = tmp.Valid;
SetNames(gameData, tmp);
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.
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.
var hairList = new List<CustomizeData>(row.Unknown30);
// Hairstyles can be found starting at Unknown66.
@ -435,7 +436,8 @@ public partial class CustomizationOptions
}
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.
private CustomizeData[] GetFacePaints(SubRace race, Gender gender)
{
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
var paintList = new List<CustomizeData>(row.Unknown37);
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
var paintList = new List<CustomizeData>(row.Unknown37);
// Number of available face paints is at Unknown37.
for (var i = 0; i < row.Unknown37; ++i)
{
@ -478,12 +479,14 @@ public partial class CustomizationOptions
var paintRow = _customizeSheet.GetRow(customizeIdx);
// Facepaint Row from CustomizeSheet might not be set in case of unlockable facepaints.
paintList.Add(paintRow != null
? new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)paintRow.FeatureID, paintRow.Icon,
(ushort)paintRow.RowId)
: new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)i, customizeIdx));
if (paintRow != null)
{
paintList.Add(new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)paintRow.FeatureID, paintRow.Icon,
(ushort)paintRow.RowId));
}
else
paintList.Add(new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)i, customizeIdx));
}
return paintList.ToArray();
}

View file

@ -8,7 +8,9 @@ public unsafe struct Customize
public Penumbra.GameData.Structs.CustomizeData Data;
public Customize(in Penumbra.GameData.Structs.CustomizeData data)
=> Data = data;
{
Data = data.Clone();
}
public Race Race
{

View file

@ -48,7 +48,7 @@ public enum CustomizeFlag : ulong
public static class CustomizeFlagExtensions
{
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)
=> (flags & RedrawRequired) != 0;

View 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;
}

View 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;
}
}
}

View 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,
};
}
}

View 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;
}
}

View 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()));
}
}

View file

@ -21,6 +21,7 @@ public class Configuration : IPluginConfiguration, ISavable
public bool UseRestrictedGearProtection { get; set; } = true;
public bool OpenFoldersByDefault { 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 DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift);
@ -28,11 +29,13 @@ public class Configuration : IPluginConfiguration, ISavable
[JsonProperty(Order = int.MaxValue)]
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
public bool DebugMode { get; set; } = true;
#else
public bool DebugMode { get; set; } = false;
public bool DebugMode { get; set; } = false;
#endif
public int Version { get; set; } = Constants.CurrentVersion;

View file

@ -205,7 +205,7 @@ public class Design : ISavable
#region Serialization
public JObject JsonSerialize()
private JObject JsonSerialize()
{
var ret = new JObject
{
@ -223,7 +223,7 @@ public class Design : ISavable
return ret;
}
public JObject SerializeEquipment()
private JObject SerializeEquipment()
{
static JObject Serialize(uint itemId, StainId stain, bool apply, bool applyStain)
=> new()
@ -250,7 +250,7 @@ public class Design : ISavable
return ret;
}
public JObject SerializeCustomize()
private JObject SerializeCustomize()
{
var ret = new JObject()
{

View file

@ -192,12 +192,12 @@ public class DesignManager
Glamourer.Log.Error("Somehow race or body type was changed in a design. This should not happen.");
return;
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;
break;
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;
break;

View 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);
}

View file

@ -3,6 +3,7 @@ using Dalamud.Plugin;
using Glamourer.Gui;
using Glamourer.Interop;
using Glamourer.Services;
using Lumina.Excel.GeneratedSheets;
using Microsoft.Extensions.DependencyInjection;
using OtterGui.Classes;
using OtterGui.Log;

View file

@ -24,20 +24,18 @@ public partial class CustomizationDrawer
private void DrawGenderSelector()
{
using var font = ImRaii.PushFont(UiBuilder.IconFont);
var icon = _customize.Gender switch
{
Gender.Male when _customize.Race is Race.Hrothgar => FontAwesomeIcon.MarsDouble,
Gender.Male => FontAwesomeIcon.Mars,
Gender.Female => FontAwesomeIcon.Venus,
_ => throw new Exception($"Gender value {_customize.Gender} is not a valid gender for a design."),
_ => FontAwesomeIcon.Question,
};
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;
_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()
@ -50,7 +48,7 @@ public partial class CustomizationDrawer
foreach (var subRace in Enum.GetValues<SubRace>().Skip(1)) // Skip Unknown
{
if (ImGui.Selectable(_service.ClanName(subRace, _customize.Gender), subRace == _customize.Clan))
_service.ChangeClan(ref _customize, subRace);
Changed |= _service.ChangeClan(ref _customize, subRace);
}
}
}

View file

@ -20,7 +20,8 @@ public partial class CustomizationDrawer : IDisposable
private Customize _customize;
private CustomizationSet _set = null!;
public Customize Customize;
public Customize Customize
=> _customize;
public CustomizeFlag CurrentFlag { get; private set; }
public CustomizeFlag Changed { get; private set; }
@ -41,7 +42,7 @@ public partial class CustomizationDrawer : IDisposable
{
_service = service;
_legacyTattoo = GetLegacyTattooIcon(pi);
Customize = Customize.Default;
_customize = Customize.Default;
}
public void Dispose()

View 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);
}

View 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();
}
}

View 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();
}
}

View file

@ -1,5 +1,8 @@
using System.Numerics;
using Glamourer.Customization;
using Glamourer.Events;
using Glamourer.Gui.Customization;
using Glamourer.Gui.Equipment;
using Glamourer.Interop.Structs;
using Glamourer.State;
using ImGuiNET;
@ -14,6 +17,7 @@ public class ActorPanel
private readonly ActorSelector _selector;
private readonly StateManager _stateManager;
private readonly CustomizationDrawer _customizationDrawer;
private readonly EquipmentDrawer _equipmentDrawer;
private ActorIdentifier _identifier;
private string _actorName = string.Empty;
@ -21,11 +25,13 @@ public class ActorPanel
private ActorData _data;
private ActorState? _state;
public ActorPanel(ActorSelector selector, StateManager stateManager, CustomizationDrawer customizationDrawer)
public ActorPanel(ActorSelector selector, StateManager stateManager, CustomizationDrawer customizationDrawer,
EquipmentDrawer equipmentDrawer)
{
_selector = selector;
_stateManager = stateManager;
_customizationDrawer = customizationDrawer;
_equipmentDrawer = equipmentDrawer;
}
public void Draw()
@ -76,46 +82,40 @@ public class ActorPanel
return;
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);
}

View file

@ -9,6 +9,7 @@ using Dalamud.Interface;
using Dalamud.Plugin;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Api;
using Glamourer.Automation;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Events;
@ -40,13 +41,16 @@ public unsafe class DebugTab : ITab
private readonly ObjectTable _objects;
private readonly ObjectManager _objectManager;
private readonly GlamourerIpc _ipc;
private readonly PhrasingService _phrasing;
private readonly ItemManager _items;
private readonly ActorService _actors;
private readonly CustomizationService _customization;
private readonly JobService _jobs;
private readonly DesignManager _designManager;
private readonly DesignFileSystem _designFileSystem;
private readonly DesignManager _designManager;
private readonly DesignFileSystem _designFileSystem;
private readonly AutoDesignManager _autoDesignManager;
private readonly PenumbraChangedItemTooltip _penumbraTooltip;
@ -61,7 +65,8 @@ public unsafe class DebugTab : ITab
UpdateSlotService updateSlotService, WeaponService weaponService, PenumbraService penumbra,
ActorService actors, ItemManager items, CustomizationService customization, ObjectManager objectManager,
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;
_visorService = visorService;
@ -81,6 +86,9 @@ public unsafe class DebugTab : ITab
_metaService = metaService;
_ipc = ipc;
_pluginInterface = pluginInterface;
_autoDesignManager = autoDesignManager;
_jobs = jobs;
_phrasing = phrasing;
}
public ReadOnlySpan<byte> Label
@ -97,6 +105,7 @@ public unsafe class DebugTab : ITab
DrawPenumbraHeader();
DrawDesigns();
DrawState();
DrawAutoDesigns();
DrawIpc();
}
@ -376,14 +385,22 @@ public unsafe class DebugTab : ITab
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);
}
ImGui.SameLine();
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);
}
@ -483,6 +500,44 @@ public unsafe class DebugTab : ITab
DrawItemService();
DrawStainService();
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;
@ -1116,6 +1171,66 @@ public unsafe class DebugTab : ITab
#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
private string _gameObjectName = string.Empty;

View file

@ -2,6 +2,7 @@
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Gui.Customization;
using Glamourer.Gui.Equipment;
using Glamourer.Interop;
using Glamourer.Interop.Penumbra;
using Glamourer.State;
@ -21,24 +22,17 @@ public class DesignPanel
private readonly DesignManager _manager;
private readonly CustomizationDrawer _customizationDrawer;
private readonly StateManager _state;
private readonly PenumbraService _penumbra;
private readonly UpdateSlotService _updateSlot;
private readonly WeaponService _weaponService;
private readonly ChangeCustomizeService _changeCustomizeService;
private readonly EquipmentDrawer _equipmentDrawer;
public DesignPanel(DesignFileSystemSelector selector, CustomizationDrawer customizationDrawer, DesignManager manager, ObjectManager objects,
StateManager state, PenumbraService penumbra, ChangeCustomizeService changeCustomizeService, WeaponService weaponService,
UpdateSlotService updateSlot)
StateManager state, EquipmentDrawer equipmentDrawer)
{
_selector = selector;
_customizationDrawer = customizationDrawer;
_manager = manager;
_objects = objects;
_state = state;
_penumbra = penumbra;
_changeCustomizeService = changeCustomizeService;
_weaponService = weaponService;
_updateSlot = updateSlot;
_selector = selector;
_customizationDrawer = customizationDrawer;
_manager = manager;
_objects = objects;
_state = state;
_equipmentDrawer = equipmentDrawer;
}
public void Draw()
@ -60,5 +54,38 @@ public class DesignPanel
}
_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);
}
}
}

View file

@ -3,6 +3,7 @@ using System.Runtime.CompilerServices;
using Dalamud.Interface;
using Glamourer.Gui.Tabs.DesignTab;
using Glamourer.Interop.Penumbra;
using Glamourer.Services;
using Glamourer.State;
using ImGuiNET;
using OtterGui;
@ -16,19 +17,25 @@ public class SettingsTab : ITab
private readonly Configuration _config;
private readonly DesignFileSystemSelector _selector;
private readonly StateListener _stateListener;
private readonly PhrasingService _phrasingService;
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;
_selector = selector;
_stateListener = stateListener;
_autoRedraw = autoRedraw;
_config = config;
_selector = selector;
_stateListener = stateListener;
_phrasingService = phrasingService;
_autoRedraw = autoRedraw;
}
public ReadOnlySpan<byte> Label
=> "Settings"u8;
private string? _tmpPhrasing1 = null;
private string? _tmpPhrasing2 = null;
public void DrawContent()
{
using var child = ImRaii.Child("MainWindowChild");
@ -36,6 +43,8 @@ public class SettingsTab : ITab
return;
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",
"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);
@ -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);
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();
}

View 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);
}
}

View file

@ -88,7 +88,7 @@ public readonly unsafe struct Model : IEquatable<Model>
/// <summary> Only valid for humans. </summary>
public CharacterArmor GetArmor(EquipSlot slot)
=> ((CharacterArmor*)AsHuman->EquipSlotData)[slot.ToIndex()];
=> ((CharacterArmor*)&AsHuman->Head)[slot.ToIndex()];
public Customize GetCustomize()
=> *(Customize*)&AsHuman->Customize;

View file

@ -51,11 +51,14 @@ public unsafe class WeaponService : IDisposable
_ => EquipSlot.Unknown,
};
var tmpWeapon = weapon;
// First call the regular function.
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);
if (tmpWeapon.Value != weapon.Value)
_loadWeaponHook.Original(drawData, slot, tmpWeapon.Value, 1, unk2, 1, unk4);
Glamourer.Log.Excessive(
$"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)
{
var value = slot == EquipSlot.OffHand ? character.AsCharacter->DrawData.OffHandModel : character.AsCharacter->DrawData.MainHandModel;
var weapon = new CharacterWeapon(value.Value) { Stain = stain.Value };
var mdl = character.Model;
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);
}
}
}

View file

@ -1,5 +1,6 @@
using System;
using System.IO;
using Glamourer.Automation;
using Glamourer.Gui;
using Newtonsoft.Json.Linq;
@ -7,13 +8,17 @@ namespace Glamourer.Services;
public class ConfigMigrationService
{
private readonly SaveService _saveService;
private readonly SaveService _saveService;
private readonly FixedDesignMigrator _fixedDesignMigrator;
private Configuration _config = null!;
private JObject _data = null!;
public ConfigMigrationService(SaveService saveService)
=> _saveService = saveService;
public ConfigMigrationService(SaveService saveService, FixedDesignMigrator fixedDesignMigrator)
{
_saveService = saveService;
_fixedDesignMigrator = fixedDesignMigrator;
}
public void Migrate(Configuration config)
{
@ -34,6 +39,7 @@ public class ConfigMigrationService
if (_config.Version > 1)
return;
_fixedDesignMigrator.Migrate(_data["FixedDesigns"]);
_config.Version = 2;
var customizationColor = _data["CustomizationColor"]?.ToObject<uint>() ?? ColorId.CustomizationDesign.Data().DefaultColor;
_config.Colors[ColorId.CustomizationDesign] = customizationColor;

View file

@ -14,6 +14,42 @@ public sealed class CustomizationService : AsyncServiceWrapper<ICustomizationMan
: 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>
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>
public bool ChangeClan(ref Customize customize, SubRace newClan)
public CustomizeFlag ChangeClan(ref Customize customize, SubRace newClan)
{
if (customize.Clan == newClan)
return false;
return 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.Clan = newClan;
// TODO Female Hrothgar
if (newRace == Race.Hrothgar)
customize.Gender = Gender.Male;
{
customize.Gender = Gender.Male;
flags |= CustomizeFlag.Gender;
}
var set = AwaitedService.GetList(customize.Clan, customize.Gender);
FixValues(set, ref customize);
return true;
return FixValues(set, ref customize) | flags;
}
/// <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)
return false;
return 0;
// TODO Female Hrothgar
if (customize.Race is Race.Hrothgar)
return false;
return 0;
if (ValidateGender(customize.Race, newGender, out newGender).Length > 0)
return false;
return 0;
customize.Gender = newGender;
var set = AwaitedService.GetList(customize.Clan, customize.Gender);
FixValues(set, ref customize);
return true;
return FixValues(set, ref customize) | CustomizeFlag.Gender;
}
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))
{
if (ValidateCustomizeValue(set, customize.Face, idx, customize[idx], out var fixedValue).Length > 0)
customize[idx] = fixedValue;
{
customize[idx] = fixedValue;
flags |= idx.ToFlag();
}
}
return flags;
}
}

View file

@ -12,11 +12,13 @@ public class FilenameService
public readonly string DesignFileSystem;
public readonly string MigrationDesignFile;
public readonly string DesignDirectory;
public readonly string AutomationFile;
public FilenameService(DalamudPluginInterface pi)
{
ConfigDirectory = pi.ConfigDirectory.FullName;
ConfigFile = pi.ConfigFile.FullName;
AutomationFile = Path.Combine(ConfigDirectory, "automation.json");
DesignFileSystem = Path.Combine(ConfigDirectory, "sort_order.json");
MigrationDesignFile = Path.Combine(ConfigDirectory, "Designs.json");
DesignDirectory = Path.Combine(ConfigDirectory, "designs");

View 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
}

View file

@ -1,9 +1,11 @@
using Dalamud.Plugin;
using Glamourer.Api;
using Glamourer.Automation;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Gui;
using Glamourer.Gui.Customization;
using Glamourer.Gui.Equipment;
using Glamourer.Gui.Tabs;
using Glamourer.Gui.Tabs.ActorTab;
using Glamourer.Gui.Tabs.DesignTab;
@ -47,6 +49,7 @@ public static class ServiceManager
.AddSingleton<BackupService>()
.AddSingleton<FrameworkManager>()
.AddSingleton<SaveService>()
.AddSingleton<PhrasingService>()
.AddSingleton<ConfigMigrationService>()
.AddSingleton<Configuration>();
@ -54,6 +57,7 @@ public static class ServiceManager
=> services.AddSingleton<VisorStateChanged>()
.AddSingleton<SlotUpdating>()
.AddSingleton<DesignChanged>()
.AddSingleton<AutomationChanged>()
.AddSingleton<StateChanged>()
.AddSingleton<WeaponLoading>()
.AddSingleton<HeadGearVisibilityChanged>()
@ -74,11 +78,15 @@ public static class ServiceManager
.AddSingleton<WeaponService>()
.AddSingleton<PenumbraService>()
.AddSingleton<ObjectManager>()
.AddSingleton<PenumbraAutoRedraw>();
.AddSingleton<PenumbraAutoRedraw>()
.AddSingleton<JobService>();
private static IServiceCollection AddDesigns(this IServiceCollection services)
=> services.AddSingleton<DesignManager>()
.AddSingleton<DesignFileSystem>();
.AddSingleton<DesignFileSystem>()
.AddSingleton<AutoDesignManager>()
.AddSingleton<AutoDesignApplier>()
.AddSingleton<FixedDesignMigrator>();
private static IServiceCollection AddState(this IServiceCollection services)
=> services.AddSingleton<StateManager>()
@ -94,6 +102,7 @@ public static class ServiceManager
.AddSingleton<MainWindow>()
.AddSingleton<GlamourerWindowSystem>()
.AddSingleton<CustomizationDrawer>()
.AddSingleton<EquipmentDrawer>()
.AddSingleton<DesignFileSystemSelector>()
.AddSingleton<DesignPanel>()
.AddSingleton<DesignTab>()

View file

@ -1,4 +1,5 @@
using Glamourer.Customization;
using System;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Structs;
@ -20,7 +21,7 @@ public class ActorState
ModelId,
}
public ActorIdentifier Identifier { get; internal init; }
public readonly ActorIdentifier Identifier;
/// <summary> This should always represent the unmodified state of the draw object. </summary>
public DesignData BaseData;
@ -33,7 +34,7 @@ public class ActorState
.Repeat(StateChanged.Source.Game, EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices + 5).ToArray();
internal ActorState(ActorIdentifier identifier)
=> Identifier = identifier;
=> Identifier = identifier.CreatePermanent();
public ref StateChanged.Source this[EquipSlot slot, bool stain]
=> ref _sources[slot.ToIndex() + (stain ? EquipFlagExtensions.NumEquipFlags / 2 : 0)];

View file

@ -1,15 +1,22 @@
using System.Linq;
using Glamourer.Customization;
using Glamourer.Interop;
using Glamourer.Interop.Penumbra;
using Glamourer.Interop.Structs;
using Glamourer.Services;
using Penumbra.Api.Enums;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
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
{
private readonly PenumbraService _penumbra;
private readonly UpdateSlotService _updateSlot;
private readonly VisorService _visor;
private readonly WeaponService _weapon;
@ -17,45 +24,63 @@ public class StateEditor
private readonly ItemManager _items;
public StateEditor(UpdateSlotService updateSlot, VisorService visor, WeaponService weapon, ChangeCustomizeService changeCustomize,
ItemManager items)
ItemManager items, PenumbraService penumbra)
{
_updateSlot = updateSlot;
_visor = visor;
_weapon = weapon;
_changeCustomize = changeCustomize;
_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)
{
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 customize = mdl.GetCustomize();
customize[idx] = value;
_changeCustomize.UpdateCustomize(mdl, customize.Data);
var mdl = actor.Model;
if (!mdl.IsHuman)
continue;
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))
{
var mdl = actor.Model;
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);
}
}
/// <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)
{
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))
_weapon.LoadWeapon(actor, EquipSlot.MainHand, weapon.Weapon());
if (slot is EquipSlot.MainHand)
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))
_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)
{
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)
{
foreach (var actor in data.Objects.Where(a => a.IsCharacter))
actor.AsCharacter->IsGPoseWet = value;
}
/// <summary> Change the hat-visibility state on actors. </summary>
public unsafe void ChangeHatState(ActorData data, bool value)
{
foreach (var actor in data.Objects.Where(a => a.IsCharacter))
actor.AsCharacter->DrawData.HideHeadgear(0, !value);
}
/// <summary> Change the weapon-visibility state on actors. </summary>
public unsafe void ChangeWeaponState(ActorData data, bool value)
{
foreach (var actor in data.Objects.Where(a => a.IsCharacter))

View file

@ -1,4 +1,5 @@
using System;
using Glamourer.Automation;
using Glamourer.Customization;
using Glamourer.Events;
using Glamourer.Interop.Penumbra;
@ -10,6 +11,11 @@ using Penumbra.GameData.Structs;
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
{
private readonly Configuration _config;
@ -22,6 +28,7 @@ public class StateListener : IDisposable
private readonly HeadGearVisibilityChanged _headGearVisibility;
private readonly VisorStateChanged _visorState;
private readonly WeaponVisibilityChanged _weaponVisibility;
private readonly AutoDesignApplier _autoDesignApplier;
public bool Enabled
{
@ -31,7 +38,7 @@ public class StateListener : IDisposable
public StateListener(StateManager manager, ItemManager items, PenumbraService penumbra, ActorService actors, Configuration config,
SlotUpdating slotUpdating, WeaponLoading weaponLoading, VisorStateChanged visorState, WeaponVisibilityChanged weaponVisibility,
HeadGearVisibilityChanged headGearVisibility)
HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier)
{
_manager = manager;
_items = items;
@ -43,6 +50,7 @@ public class StateListener : IDisposable
_visorState = visorState;
_weaponVisibility = weaponVisibility;
_headGearVisibility = headGearVisibility;
_autoDesignApplier = autoDesignApplier;
if (Enabled)
Subscribe();
@ -68,48 +76,83 @@ public class StateListener : IDisposable
Unsubscribe();
}
/// <summary> The result of updating the base state of an ActorState. </summary>
private enum UpdateState
{
/// <summary> The base state is the same as prior state. </summary>
NoChange,
/// <summary> The game requests an update to a state that does not agree with the actor state. </summary>
Transformed,
/// <summary> The base state changed compared to prior state. </summary>
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)
{
// TODO: Fixed Designs.
var actor = (Actor)actorPtr;
var identifier = actor.GetIdentifier(_actors.AwaitedService);
var modelId = *(uint*)modelPtr;
ref var customize = ref *(Customize*)customizePtr;
if (_manager.TryGetValue(identifier, out var state))
{
_autoDesignApplier.Reduce(actor, identifier, state);
switch (UpdateBaseData(actor, state, modelId, customizePtr, equipDataPtr))
{
case UpdateState.Change: break;
case UpdateState.Transformed: break;
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;
}
}
if (_config.UseRestrictedGearProtection && modelId == 0)
if (modelId == 0)
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)
{
// TODO handle hat state
var actor = _penumbra.GameObjectFromDrawObject(model);
var customize = model.GetCustomize();
var actor = _penumbra.GameObjectFromDrawObject(model);
if (actor.Identifier(_actors.AwaitedService, out var identifier)
&& _manager.TryGetValue(identifier, out var state))
ApplyEquipmentPiece(actor, state, slot, ref armor.Value);
HandleEquipSlot(actor, state, slot, ref armor.Value);
if (_config.UseRestrictedGearProtection)
(_, armor.Value) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender);
if (!_config.UseRestrictedGearProtection)
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)
{
if (!actor.Identifier(_actors.AwaitedService, out var identifier)
@ -117,134 +160,283 @@ public class StateListener : IDisposable
return;
ref var actorWeapon = ref weapon.Value;
var stateItem = state.ModelData.Item(slot);
if (actorWeapon.Set.Value != stateItem.ModelId.Value
|| actorWeapon.Type.Value != stateItem.WeaponType
|| actorWeapon.Variant != stateItem.Variant)
var baseType = state.BaseData.Item(slot).Type;
var apply = false;
switch (UpdateBaseData(actor, state, slot, actorWeapon))
{
var oldActorItem = state.BaseData.Item(slot);
if (oldActorItem.ModelId.Value == actorWeapon.Set.Value
&& oldActorItem.WeaponType.Value == actorWeapon.Type.Value
&& 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);
// Do nothing. But this usually can not happen because the hooked function also writes to game objects later.
case UpdateState.Transformed: break;
case UpdateState.Change:
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;
}
else
{
actorWeapon.Set = stateItem.ModelId;
actorWeapon.Type = stateItem.Variant;
actorWeapon.Variant = stateItem.Variant;
apply = true;
}
}
}
var stateStain = state.ModelData.Stain(slot);
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)
if (state[slot, false] is not StateChanged.Source.Fixed)
{
state.ModelData.Customize[idx] = value;
state[idx] = StateChanged.Source.Game;
state.ModelData.SetStain(slot, state.BaseData.Stain(slot));
state[slot, true] = StateChanged.Source.Game;
}
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
foreach (var slot in EquipSlotExtensions.EqdpSlots)
ApplyEquipmentPiece(actor, state, slot, ref *equipData++);
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 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);
if (changeState is UpdateState.Transformed)
switch (UpdateBaseData(actor, state, slot, armor))
{
// 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;
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
{
var modelArmor = state.ModelData.Armor(slot);
if (armor.Value == modelArmor.Value)
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);
// if base state did not change, overwrite the value with the model state one.
value.Value = state.ModelData.IsVisorToggled();
}
}
/// <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)
{
if (!_config.UseRestrictedGearProtection)
return;
var idx = 0;
var ptr = (CharacterArmor*)equipDataPtr;
for (var end = ptr + 10; ptr < end; ++ptr)
@ -274,144 +466,4 @@ public class StateListener : IDisposable
_headGearVisibility.Unsubscribe(OnHeadGearVisibilityChange);
_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();
}
}
}

View file

@ -3,17 +3,12 @@ using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Security.Cryptography;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Interop;
using Glamourer.Interop.Penumbra;
using Glamourer.Interop.Structs;
using Glamourer.Services;
using Glamourer.Structs;
using Lumina.Excel.GeneratedSheets;
using OtterGui.Log;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -30,114 +25,20 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
private readonly ObjectManager _objects;
private readonly StateEditor _editor;
private readonly PenumbraService _penumbra;
private readonly Dictionary<ActorIdentifier, ActorState> _states = new();
public StateManager(ActorService actors, ItemManager items, CustomizationService customizations, VisorService visor, StateChanged @event,
PenumbraService penumbra, ObjectManager objects, StateEditor editor)
ObjectManager objects, StateEditor editor)
{
_actors = actors;
_items = items;
_customizations = customizations;
_visor = visor;
_event = @event;
_penumbra = penumbra;
_objects = objects;
_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()
=> _states.GetEnumerator();
@ -162,15 +63,54 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
public IEnumerable<ActorState> Values
=> _states.Values;
/// <inheritdoc cref="GetOrCreate(ActorIdentifier, Actor, out ActorState?)"/>
public bool GetOrCreate(Actor actor, [NotNullWhen(true)] out ActorState? state)
=> GetOrCreate(actor.GetIdentifier(_actors.AwaitedService), actor, out state);
/// <summary> Try to obtain or create a new state for an existing actor. Returns false if no state could be created. </summary>
public 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)
{
var ret = new DesignData();
// If the given actor is not a character, just return a default character.
if (!actor.IsCharacter)
{
ret.SetDefaultEquipment(_items);
return ret;
}
// 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)
{
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 off;
// Hat visibility is only unambiguously contained in the game object.
// Set it first to know where to get head slot data from.
ret.SetHatVisible(!actor.AsCharacter->DrawData.IsHatHidden);
// Use the draw object if it is a human.
if (model.IsHuman)
{
// Customize can be obtained from the draw object.
ret.Customize = model.GetCustomize();
// We can not use the head slot data from the draw object if the hat is hidden.
var head = ret.IsHatVisible() ? model.GetArmor(EquipSlot.Head) : actor.GetArmor(EquipSlot.Head);
var headItem = _items.Identify(EquipSlot.Head, head.Set, head.Variant);
ret.SetItem(EquipSlot.Head, headItem);
ret.SetStain(EquipSlot.Head, head.Stain);
// The other slots can be used from the draw object.
foreach (var slot in EquipSlotExtensions.EqdpSlots.Skip(1))
{
var armor = model.GetArmor(slot);
@ -198,12 +147,17 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
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);
// 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));
}
else
{
// Obtain all data from the game object.
ret.Customize = actor.GetCustomize();
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var armor = actor.GetArmor(slot);
@ -212,12 +166,12 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
ret.SetStain(slot, armor.Stain);
}
ret.Customize = actor.GetCustomize();
main = actor.GetMainhand();
off = actor.GetOffhand();
main = actor.GetMainhand();
off = actor.GetOffhand();
ret.SetVisor(actor.AsCharacter->DrawData.IsVisorToggled);
}
// Set the weapons regardless of source.
var mainItem = _items.Identify(EquipSlot.MainHand, main.Set, main.Type, (byte)main.Variant);
var offItem = _items.Identify(EquipSlot.OffHand, off.Set, off.Type, (byte)off.Variant, mainItem.Type);
ret.SetItem(EquipSlot.MainHand, mainItem);
@ -225,39 +179,232 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
ret.SetItem(EquipSlot.OffHand, offItem);
ret.SetStain(EquipSlot.OffHand, off.Stain);
// Wetness can technically only be set in GPose or via external tools.
// It is only available in the game object.
ret.SetIsWet(actor.AsCharacter->IsGPoseWet);
// Weapon visibility could technically be inferred from the weapon draw objects,
// but since we use hat visibility from the game object we can also use weapon visibility from it.
ret.SetWeaponVisible(!actor.AsCharacter->DrawData.IsWeaponHidden);
return ret;
}
#region Change Values
/// <summary> Change a customization value. </summary>
public void ChangeCustomize(ActorState state, ActorData data, CustomizeIndex idx, CustomizeValue value, StateChanged.Source source,
bool force)
public void ChangeCustomize(ActorState state, CustomizeIndex idx, CustomizeValue value, StateChanged.Source source)
{
ref var s = ref state[idx];
if (s is StateChanged.Source.Fixed && source is StateChanged.Source.Game)
return;
var oldValue = state.ModelData.Customize[idx];
if (oldValue == value && !force)
return;
// Update state data.
var old = state.ModelData.Customize[idx];
state.ModelData.Customize[idx] = value;
state[idx] = source;
Glamourer.Log.Excessive(
$"Changed customize {idx.ToDefaultName()} for {state.Identifier} ({string.Join(", ", data.Objects.Select(o => $"0x{o.Address}"))}) from {oldValue.Value} to {value.Value}.");
_event.Invoke(StateChanged.Type.Customize, source, state, data, (oldValue, value, idx));
// 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 {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)
{
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):
ChangeEquip(state, slot, design.DesignData.Item(slot), StateChanged.Source.Manual);
ChangeItem(state, slot, design.DesignData.Item(slot), StateChanged.Source.Manual);
break;
case (false, true):
ChangeStain(state, slot, design.DesignData.Stain(slot), StateChanged.Source.Manual);
@ -267,6 +414,19 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
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())
ChangeHatState(state, design.DesignData.IsHatVisible(), StateChanged.Source.Manual);
if (design.DoApplyWeaponVisible())
@ -274,17 +434,29 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
if (design.DoApplyVisorToggle())
ChangeVisorState(state, design.DesignData.IsVisorToggled(), StateChanged.Source.Manual);
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)
{
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
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)
@ -292,215 +464,24 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
if (!GetOrCreate(actor, out var state))
return;
_objects.Update();
var objects = _objects.TryGetValue(state.Identifier, out var d) ? d : ActorData.Invalid;
var mdl = actor.Model;
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)
_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);
//}
}