Merge branch 'Crests'

This commit is contained in:
Ottermandias 2023-12-02 17:58:07 +01:00
commit 69dce5790b
44 changed files with 1668 additions and 1032 deletions

8
.gitmodules vendored
View file

@ -1,16 +1,16 @@
[submodule "OtterGui"]
path = OtterGui
url = git@github.com:Ottermandias/OtterGui.git
url = https://github.com/Ottermandias/OtterGui.git
branch = main
[submodule "Penumbra.GameData"]
path = Penumbra.GameData
url = git@github.com:Ottermandias/Penumbra.GameData.git
url = https://github.com/Ottermandias/Penumbra.GameData.git
branch = main
[submodule "Penumbra.String"]
path = Penumbra.String
url = git@github.com:Ottermandias/Penumbra.String.git
url = https://github.com/Ottermandias/Penumbra.String.git
branch = main
[submodule "Penumbra.Api"]
path = Penumbra.Api
url = git@github.com:Ottermandias/Penumbra.Api.git
url = https://github.com/Ottermandias/Penumbra.Api.git
branch = main

View file

@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Penumbra.GameData.Enums;
namespace Glamourer.Structs;
[Flags]
public enum CrestFlag : ushort
{
OffHand = 0x0001,
Head = 0x0002,
Body = 0x0004,
Hands = 0x0008,
Legs = 0x0010,
Feet = 0x0020,
Ears = 0x0040,
Neck = 0x0080,
Wrists = 0x0100,
RFinger = 0x0200,
LFinger = 0x0400,
MainHand = 0x0800,
}
public enum CrestType : byte
{
None,
Human,
Mainhand,
Offhand,
};
public static class CrestExtensions
{
public const CrestFlag All = (CrestFlag)(((ulong)EquipFlag.Mainhand << 1) - 1);
public const CrestFlag AllRelevant = CrestFlag.Head | CrestFlag.Body | CrestFlag.OffHand;
public static readonly IReadOnlyList<CrestFlag> AllRelevantSet = Enum.GetValues<CrestFlag>().Where(f => AllRelevant.HasFlag(f)).ToArray();
public static int ToInternalIndex(this CrestFlag flag)
=> flag switch
{
CrestFlag.Head => 0,
CrestFlag.Body => 1,
CrestFlag.OffHand => 2,
_ => -1,
};
public static (CrestType Type, byte Index) ToIndex(this CrestFlag flag)
=> flag switch
{
CrestFlag.Head => (CrestType.Human, 0),
CrestFlag.Body => (CrestType.Human, 1),
CrestFlag.Hands => (CrestType.Human, 2),
CrestFlag.Legs => (CrestType.Human, 3),
CrestFlag.Feet => (CrestType.Human, 4),
CrestFlag.Ears => (CrestType.None, 0),
CrestFlag.Neck => (CrestType.None, 0),
CrestFlag.Wrists => (CrestType.None, 0),
CrestFlag.RFinger => (CrestType.None, 0),
CrestFlag.LFinger => (CrestType.None, 0),
CrestFlag.MainHand => (CrestType.None, 0),
CrestFlag.OffHand => (CrestType.Offhand, 0),
_ => (CrestType.None, 0),
};
public static CrestFlag ToCrestFlag(this EquipSlot slot)
=> slot switch
{
EquipSlot.MainHand => CrestFlag.MainHand,
EquipSlot.OffHand => CrestFlag.OffHand,
EquipSlot.Head => CrestFlag.Head,
EquipSlot.Body => CrestFlag.Body,
EquipSlot.Hands => CrestFlag.Hands,
EquipSlot.Legs => CrestFlag.Legs,
EquipSlot.Feet => CrestFlag.Feet,
EquipSlot.Ears => CrestFlag.Ears,
EquipSlot.Neck => CrestFlag.Neck,
EquipSlot.Wrists => CrestFlag.Wrists,
EquipSlot.RFinger => CrestFlag.RFinger,
EquipSlot.LFinger => CrestFlag.LFinger,
_ => 0,
};
public static string ToLabel(this CrestFlag flag)
=> flag switch
{
CrestFlag.Head => "Head",
CrestFlag.Body => "Chest",
CrestFlag.Hands => "Gauntlets",
CrestFlag.Legs => "Pants",
CrestFlag.Feet => "Boot",
CrestFlag.Ears => "Earrings",
CrestFlag.Neck => "Necklace",
CrestFlag.Wrists => "Bracelet",
CrestFlag.RFinger => "Right Ring",
CrestFlag.LFinger => "Left Ring",
CrestFlag.MainHand => "Weapon",
CrestFlag.OffHand => "Shield",
_ => string.Empty,
};
}

View file

@ -15,18 +15,19 @@ public class AutoDesign
[Flags]
public enum Type : byte
{
Armor = 0x01,
Customizations = 0x02,
Weapons = 0x04,
Stains = 0x08,
Accessories = 0x10,
Armor = 0x01,
Customizations = 0x02,
Weapons = 0x04,
GearCustomization = 0x08,
Accessories = 0x10,
All = Armor | Accessories | Customizations | Weapons | Stains,
All = Armor | Accessories | Customizations | Weapons | GearCustomization,
}
public Design? Design;
public JobGroup Jobs;
public Type ApplicationType;
public short GearsetIndex = -1;
public string Name(bool incognito)
=> Revert ? RevertName : incognito ? Design!.Incognito : Design!.Name.Text;
@ -43,10 +44,22 @@ public class AutoDesign
Design = Design,
ApplicationType = ApplicationType,
Jobs = Jobs,
GearsetIndex = GearsetIndex,
};
public unsafe bool IsActive(Actor actor)
=> actor.IsCharacter && Jobs.Fits(actor.AsCharacter->CharacterData.ClassJob);
{
if (!actor.IsCharacter)
return false;
var ret = true;
if (GearsetIndex < 0)
ret &= Jobs.Fits(actor.AsCharacter->CharacterData.ClassJob);
else
ret &= AutoDesignApplier.CheckGearset(GearsetIndex);
return ret;
}
public JObject Serialize()
=> new()
@ -58,25 +71,29 @@ public class AutoDesign
private JObject CreateConditionObject()
{
var ret = new JObject();
if (Jobs.Id != 0)
ret["JobGroup"] = Jobs.Id;
var ret = new JObject
{
["Gearset"] = GearsetIndex,
["JobGroup"] = Jobs.Id,
};
return ret;
}
public (EquipFlag Equip, CustomizeFlag Customize, bool ApplyHat, bool ApplyVisor, bool ApplyWeapon, bool ApplyWet) ApplyWhat()
public (EquipFlag Equip, CustomizeFlag Customize, CrestFlag Crest, 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);
| (ApplicationType.HasFlag(Type.GearCustomization) ? StainFlags : 0);
var customizeFlags = ApplicationType.HasFlag(Type.Customizations) ? CustomizeFlagExtensions.All : 0;
var crestFlag = ApplicationType.HasFlag(Type.GearCustomization) ? CrestExtensions.AllRelevant : 0;
if (Revert)
return (equipFlags, customizeFlags, ApplicationType.HasFlag(Type.Armor), ApplicationType.HasFlag(Type.Armor),
return (equipFlags, customizeFlags, crestFlag, ApplicationType.HasFlag(Type.Armor), ApplicationType.HasFlag(Type.Armor),
ApplicationType.HasFlag(Type.Weapons), ApplicationType.HasFlag(Type.Customizations));
return (equipFlags & Design!.ApplyEquip, customizeFlags & Design.ApplyCustomize,
return (equipFlags & Design!.ApplyEquip, customizeFlags & Design.ApplyCustomize, crestFlag & Design.ApplyCrest,
ApplicationType.HasFlag(Type.Armor) && Design.DoApplyHatVisible(),
ApplicationType.HasFlag(Type.Armor) && Design.DoApplyVisorToggle(),
ApplicationType.HasFlag(Type.Weapons) && Design.DoApplyWeaponVisible(),

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Events;
@ -26,6 +27,7 @@ public class AutoDesignApplier : IDisposable
private readonly AutoDesignManager _manager;
private readonly StateManager _state;
private readonly JobService _jobs;
private readonly EquippedGearset _equippedGearset;
private readonly ActorService _actors;
private readonly CustomizationService _customizations;
private readonly CustomizeUnlockManager _customizeUnlocks;
@ -49,7 +51,8 @@ public class AutoDesignApplier : IDisposable
public AutoDesignApplier(Configuration config, AutoDesignManager manager, StateManager state, JobService jobs,
CustomizationService customizations, ActorService actors, ItemUnlockManager itemUnlocks, CustomizeUnlockManager customizeUnlocks,
AutomationChanged @event, ObjectManager objects, WeaponLoading weapons, HumanModelList humans, IClientState clientState)
AutomationChanged @event, ObjectManager objects, WeaponLoading weapons, HumanModelList humans, IClientState clientState,
EquippedGearset equippedGearset)
{
_config = config;
_manager = manager;
@ -64,15 +67,18 @@ public class AutoDesignApplier : IDisposable
_weapons = weapons;
_humans = humans;
_clientState = clientState;
_equippedGearset = equippedGearset;
_jobs.JobChanged += OnJobChange;
_event.Subscribe(OnAutomationChange, AutomationChanged.Priority.AutoDesignApplier);
_weapons.Subscribe(OnWeaponLoading, WeaponLoading.Priority.AutoDesignApplier);
_equippedGearset.Subscribe(OnEquippedGearset, EquippedGearset.Priority.AutoDesignApplier);
}
public void Dispose()
{
_weapons.Unsubscribe(OnWeaponLoading);
_event.Unsubscribe(OnAutomationChange);
_equippedGearset.Unsubscribe(OnEquippedGearset);
_jobs.JobChanged -= OnJobChange;
}
@ -262,6 +268,7 @@ public class AutoDesignApplier : IDisposable
{
EquipFlag totalEquipFlags = 0;
CustomizeFlag totalCustomizeFlags = 0;
CrestFlag totalCrestFlags = 0;
byte totalMetaFlags = 0;
if (set.BaseState == AutoDesignSet.Base.Game)
_state.ResetStateFixed(state);
@ -285,10 +292,11 @@ public class AutoDesignApplier : IDisposable
if (!data.IsHuman)
continue;
var (equipFlags, customizeFlags, applyHat, applyVisor, applyWeapon, applyWet) = design.ApplyWhat();
var (equipFlags, customizeFlags, crestFlags, applyHat, applyVisor, applyWeapon, applyWet) = design.ApplyWhat();
ReduceMeta(state, data, applyHat, applyVisor, applyWeapon, applyWet, ref totalMetaFlags, respectManual, source);
ReduceCustomize(state, data, customizeFlags, ref totalCustomizeFlags, respectManual, source);
ReduceEquip(state, data, equipFlags, ref totalEquipFlags, respectManual, source, fromJobChange);
ReduceCrests(state, data, crestFlags, ref totalCrestFlags, respectManual, source);
}
if (totalCustomizeFlags != 0)
@ -318,6 +326,24 @@ public class AutoDesignApplier : IDisposable
}
}
private void ReduceCrests(ActorState state, in DesignData design, CrestFlag crestFlags, ref CrestFlag totalCrestFlags, bool respectManual,
StateChanged.Source source)
{
crestFlags &= ~totalCrestFlags;
if (crestFlags == 0)
return;
foreach (var slot in CrestExtensions.AllRelevantSet)
{
if (!crestFlags.HasFlag(slot))
continue;
if (!respectManual || state[slot] is not StateChanged.Source.Manual)
_state.ChangeCrest(state, slot, design.Crest(slot), source);
totalCrestFlags |= slot;
}
}
private void ReduceEquip(ActorState state, in DesignData design, EquipFlag equipFlags, ref EquipFlag totalEquipFlags, bool respectManual,
StateChanged.Source source, bool fromJobChange)
{
@ -496,4 +522,38 @@ public class AutoDesignApplier : IDisposable
totalMetaFlags |= 0x08;
}
}
internal static int NewGearsetId = -1;
private void OnEquippedGearset(string name, int id, int prior, byte _, byte job)
{
if (!_config.EnableAutoDesigns)
return;
var (player, data) = _objects.PlayerData;
if (!player.IsValid)
return;
if (!GetPlayerSet(player, out var set) || !_state.TryGetValue(player, out var state))
return;
var respectManual = prior == id;
NewGearsetId = id;
Reduce(data.Objects[0], state, set, respectManual, job != state.LastJob);
NewGearsetId = -1;
foreach (var actor in data.Objects)
_state.ReapplyState(actor);
}
public static unsafe bool CheckGearset(short check)
{
if (NewGearsetId != -1)
return check == NewGearsetId;
var module = RaptureGearsetModule.Instance();
if (module == null)
return false;
return check == module->CurrentGearsetIndex;
}
}

View file

@ -306,6 +306,7 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
return;
var design = set.Designs[which];
if (design.Jobs.Id == jobs.Id)
return;
@ -316,6 +317,22 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
_event.Invoke(AutomationChanged.Type.ChangedConditions, set, (which, old, jobs));
}
public void ChangeGearsetCondition(AutoDesignSet set, int which, short index)
{
if (which >= set.Designs.Count || which < 0)
return;
var design = set.Designs[which];
if (design.GearsetIndex == index)
return;
var old = design.GearsetIndex;
design.GearsetIndex = index;
Save();
Glamourer.Log.Debug($"Changed gearset condition from {old} to {index} for associated design {which + 1} in design set.");
_event.Invoke(AutomationChanged.Type.ChangedConditions, set, (which, old, index));
}
public void ChangeApplicationType(AutoDesignSet set, int which, AutoDesign.Type type)
{
if (which >= set.Designs.Count || which < 0)
@ -338,10 +355,8 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
public void Save(StreamWriter writer)
{
using var j = new JsonTextWriter(writer)
{
Formatting = Formatting.Indented,
};
using var j = new JsonTextWriter(writer);
j.Formatting = Formatting.Indented;
Serialize().WriteTo(j);
}
@ -456,13 +471,16 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
{
if (designIdentifier.Length == 0)
{
Glamourer.Messager.NotificationMessage($"Error parsing automatically applied design for set {setName}: No design specified.", NotificationType.Warning);
Glamourer.Messager.NotificationMessage($"Error parsing automatically applied design for set {setName}: No design specified.",
NotificationType.Warning);
return null;
}
if (!Guid.TryParse(designIdentifier, out var guid))
{
Glamourer.Messager.NotificationMessage($"Error parsing automatically applied design for set {setName}: {designIdentifier} is not a valid GUID.", NotificationType.Warning);
Glamourer.Messager.NotificationMessage(
$"Error parsing automatically applied design for set {setName}: {designIdentifier} is not a valid GUID.",
NotificationType.Warning);
return null;
}
@ -471,7 +489,8 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
if (design == null)
{
Glamourer.Messager.NotificationMessage(
$"Error parsing automatically applied design for set {setName}: The specified design {guid} does not exist.", NotificationType.Warning);
$"Error parsing automatically applied design for set {setName}: The specified design {guid} does not exist.",
NotificationType.Warning);
return null;
}
}
@ -483,24 +502,31 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
Design = design,
ApplicationType = applicationType & AutoDesign.Type.All,
};
return ParseConditions(setName, jObj, ret) ? ret : null;
}
private bool ParseConditions(string setName, JObject jObj, AutoDesign ret)
{
var conditions = jObj["Conditions"];
if (conditions == null)
return ret;
return true;
var jobs = conditions["JobGroup"]?.ToObject<int>() ?? -1;
if (jobs >= 0)
{
if (!_jobs.JobGroups.TryGetValue((ushort)jobs, out var jobGroup))
{
Glamourer.Messager.NotificationMessage($"Error parsing automatically applied design for set {setName}: The job condition {jobs} does not exist.", NotificationType.Warning);
return null;
Glamourer.Messager.NotificationMessage(
$"Error parsing automatically applied design for set {setName}: The job condition {jobs} does not exist.",
NotificationType.Warning);
return false;
}
ret.Jobs = jobGroup;
}
return ret;
ret.GearsetIndex = conditions["Gearset"]?.ToObject<short>() ?? -1;
return true;
}
private void Save()

View file

@ -3,8 +3,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Interface.Internal.Notifications;
using Glamourer.Customization;
using Glamourer.Gui;
using Glamourer.Interop.Penumbra;
using Glamourer.Services;
using Newtonsoft.Json;
@ -28,8 +26,8 @@ public sealed class Design : DesignBase, ISavable
internal Design(Design other)
: base(other)
{
Tags = Tags.ToArray();
Description = Description;
Tags = other.Tags.ToArray();
Description = other.Description;
AssociatedMods = new SortedList<Mod, ModSettings>(other.AssociatedMods);
}
@ -69,8 +67,7 @@ public sealed class Design : DesignBase, ISavable
["Equipment"] = SerializeEquipment(),
["Customize"] = SerializeCustomize(),
["Mods"] = SerializeMods(),
}
;
};
return ret;
}

View file

@ -1,6 +1,4 @@
using System;
using System.Linq;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.Internal.Notifications;
using Glamourer.Customization;
using Glamourer.Services;
using Glamourer.Structs;
@ -9,6 +7,8 @@ using OtterGui.Classes;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using System;
using System.Linq;
namespace Glamourer.Designs;
@ -39,7 +39,6 @@ public class DesignBase
ApplyEquip = equipFlags & EquipFlagExtensions.All;
_designFlags = 0;
CustomizationSet = SetCustomizationSet(customize);
}
internal DesignBase(DesignBase clone)
@ -83,6 +82,7 @@ public class DesignBase
=> _applyCustomize;
internal EquipFlag ApplyEquip = EquipFlagExtensions.All;
internal CrestFlag ApplyCrest = CrestExtensions.AllRelevant;
private DesignFlags _designFlags = DesignFlags.ApplyHatVisible | DesignFlags.ApplyVisorState | DesignFlags.ApplyWeaponVisible;
public bool SetCustomize(CustomizationService customizationService, Customize customize)
@ -169,6 +169,9 @@ public class DesignBase
public bool DoApplyCustomize(CustomizeIndex idx)
=> ApplyCustomize.HasFlag(idx.ToFlag());
public bool DoApplyCrest(CrestFlag slot)
=> ApplyCrest.HasFlag(slot);
internal bool SetApplyEquip(EquipSlot slot, bool value)
{
var newValue = value ? ApplyEquip | slot.ToFlag() : ApplyEquip & ~slot.ToFlag();
@ -199,28 +202,42 @@ public class DesignBase
return true;
}
internal FlagRestrictionResetter TemporarilyRestrictApplication(EquipFlag equipFlags, CustomizeFlag customizeFlags)
=> new(this, equipFlags, customizeFlags);
internal bool SetApplyCrest(CrestFlag slot, bool value)
{
var newValue = value ? ApplyCrest | slot : ApplyCrest & ~slot;
if (newValue == ApplyCrest)
return false;
ApplyCrest = newValue;
return true;
}
internal FlagRestrictionResetter TemporarilyRestrictApplication(EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags)
=> new(this, equipFlags, customizeFlags, crestFlags);
internal readonly struct FlagRestrictionResetter : IDisposable
{
private readonly DesignBase _design;
private readonly EquipFlag _oldEquipFlags;
private readonly CustomizeFlag _oldCustomizeFlags;
private readonly CrestFlag _oldCrestFlags;
public FlagRestrictionResetter(DesignBase d, EquipFlag equipFlags, CustomizeFlag customizeFlags)
public FlagRestrictionResetter(DesignBase d, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags)
{
_design = d;
_oldEquipFlags = d.ApplyEquip;
_oldCustomizeFlags = d.ApplyCustomizeRaw;
_oldCrestFlags = d.ApplyCrest;
d.ApplyEquip &= equipFlags;
d.ApplyCustomize &= customizeFlags;
d.ApplyCrest &= crestFlags;
}
public void Dispose()
{
_design.ApplyEquip = _oldEquipFlags;
_design.ApplyCustomize = _oldCustomizeFlags;
_design.ApplyCrest = _oldCrestFlags;
}
}
@ -246,13 +263,15 @@ public class DesignBase
protected JObject SerializeEquipment()
{
static JObject Serialize(CustomItemId id, StainId stain, bool apply, bool applyStain)
static JObject Serialize(CustomItemId id, StainId stain, bool crest, bool apply, bool applyStain, bool applyCrest)
=> new()
{
["ItemId"] = id.Id,
["Stain"] = stain.Id,
["Crest"] = crest,
["Apply"] = apply,
["ApplyStain"] = applyStain,
["ApplyCrest"] = applyCrest,
};
var ret = new JObject();
@ -260,9 +279,11 @@ public class DesignBase
{
foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand))
{
var item = _designData.Item(slot);
var stain = _designData.Stain(slot);
ret[slot.ToString()] = Serialize(item.Id, stain, DoApplyEquip(slot), DoApplyStain(slot));
var item = _designData.Item(slot);
var stain = _designData.Stain(slot);
var crestSlot = slot.ToCrestFlag();
var crest = _designData.Crest(crestSlot);
ret[slot.ToString()] = Serialize(item.Id, stain, crest, DoApplyEquip(slot), DoApplyStain(slot), DoApplyCrest(crestSlot));
}
ret["Hat"] = new QuadBool(_designData.IsHatVisible(), DoApplyHatVisible()).ToJObject("Show", "Apply");
@ -345,13 +366,15 @@ public class DesignBase
return;
}
static (CustomItemId, StainId, bool, bool) ParseItem(EquipSlot slot, JToken? item)
static (CustomItemId, StainId, bool, bool, bool, bool) ParseItem(EquipSlot slot, JToken? item)
{
var id = item?["ItemId"]?.ToObject<ulong>() ?? ItemManager.NothingId(slot).Id;
var stain = (StainId)(item?["Stain"]?.ToObject<byte>() ?? 0);
var crest = item?["Crest"]?.ToObject<bool>() ?? false;
var apply = item?["Apply"]?.ToObject<bool>() ?? false;
var applyStain = item?["ApplyStain"]?.ToObject<bool>() ?? false;
return (id, stain, apply, applyStain);
var applyCrest = item?["ApplyCrest"]?.ToObject<bool>() ?? false;
return (id, stain, crest, apply, applyStain, applyCrest);
}
void PrintWarning(string msg)
@ -362,21 +385,25 @@ public class DesignBase
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var (id, stain, apply, applyStain) = ParseItem(slot, equip[slot.ToString()]);
var (id, stain, crest, apply, applyStain, applyCrest) = ParseItem(slot, equip[slot.ToString()]);
PrintWarning(items.ValidateItem(slot, id, out var item, allowUnknown));
PrintWarning(items.ValidateStain(stain, out stain, allowUnknown));
var crestSlot = slot.ToCrestFlag();
design._designData.SetItem(slot, item);
design._designData.SetStain(slot, stain);
design._designData.SetCrest(crestSlot, crest);
design.SetApplyEquip(slot, apply);
design.SetApplyStain(slot, applyStain);
design.SetApplyCrest(crestSlot, applyCrest);
}
{
var (id, stain, apply, applyStain) = ParseItem(EquipSlot.MainHand, equip[EquipSlot.MainHand.ToString()]);
var (id, stain, crest, apply, applyStain, applyCrest) = ParseItem(EquipSlot.MainHand, equip[EquipSlot.MainHand.ToString()]);
if (id == ItemManager.NothingId(EquipSlot.MainHand))
id = items.DefaultSword.ItemId;
var (idOff, stainOff, applyOff, applyStainOff) = ParseItem(EquipSlot.OffHand, equip[EquipSlot.OffHand.ToString()]);
var (idOff, stainOff, crestOff, applyOff, applyStainOff, applyCrestOff) =
ParseItem(EquipSlot.OffHand, equip[EquipSlot.OffHand.ToString()]);
if (id == ItemManager.NothingId(EquipSlot.OffHand))
id = ItemManager.NothingId(FullEquipType.Shield);
@ -387,10 +414,14 @@ public class DesignBase
design._designData.SetItem(EquipSlot.OffHand, off);
design._designData.SetStain(EquipSlot.MainHand, stain);
design._designData.SetStain(EquipSlot.OffHand, stainOff);
design._designData.SetCrest(CrestFlag.MainHand, crest);
design._designData.SetCrest(CrestFlag.OffHand, crestOff);
design.SetApplyEquip(EquipSlot.MainHand, apply);
design.SetApplyEquip(EquipSlot.OffHand, applyOff);
design.SetApplyStain(EquipSlot.MainHand, applyStain);
design.SetApplyStain(EquipSlot.OffHand, applyStainOff);
design.SetApplyCrest(CrestFlag.MainHand, applyCrest);
design.SetApplyCrest(CrestFlag.OffHand, applyCrestOff);
}
var metaValue = QuadBool.FromJObject(equip["Hat"], "Show", "Apply", QuadBool.NullFalse);
design.SetApplyHatVisible(metaValue.Enabled);

View file

@ -13,22 +13,9 @@ using Penumbra.GameData.Enums;
namespace Glamourer.Designs;
public class DesignConverter
public class DesignConverter(ItemManager _items, DesignManager _designs, CustomizationService _customize, HumanModelList _humans)
{
public const byte Version = 5;
private readonly ItemManager _items;
private readonly DesignManager _designs;
private readonly CustomizationService _customize;
private readonly HumanModelList _humans;
public DesignConverter(ItemManager items, DesignManager designs, CustomizationService customize, HumanModelList humans)
{
_items = items;
_designs = designs;
_customize = customize;
_humans = humans;
}
public const byte Version = 6;
public JObject ShareJObject(DesignBase design)
=> design.JsonSerialize();
@ -36,32 +23,33 @@ public class DesignConverter
public JObject ShareJObject(Design design)
=> design.JsonSerialize();
public JObject ShareJObject(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags)
public JObject ShareJObject(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags)
{
var design = Convert(state, equipFlags, customizeFlags);
var design = Convert(state, equipFlags, customizeFlags, crestFlags);
return ShareJObject(design);
}
public string ShareBase64(Design design)
=> ShareBackwardCompatible(ShareJObject(design), design);
=> ShareBase64(ShareJObject(design));
public string ShareBase64(DesignBase design)
=> ShareBackwardCompatible(ShareJObject(design), design);
=> ShareBase64(ShareJObject(design));
public string ShareBase64(ActorState state)
=> ShareBase64(state, EquipFlagExtensions.All, CustomizeFlagExtensions.All);
=> ShareBase64(state, EquipFlagExtensions.All, CustomizeFlagExtensions.All, CrestExtensions.All);
public string ShareBase64(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags)
public string ShareBase64(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags)
{
var design = Convert(state, equipFlags, customizeFlags);
return ShareBackwardCompatible(ShareJObject(design), design);
var design = Convert(state, equipFlags, customizeFlags, crestFlags);
return ShareBase64(ShareJObject(design));
}
public DesignBase Convert(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags)
public DesignBase Convert(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags)
{
var design = _designs.CreateTemporary();
design.ApplyEquip = equipFlags & EquipFlagExtensions.All;
design.ApplyCustomize = customizeFlags;
design.ApplyCustomize = customizeFlags & CustomizeFlagExtensions.AllRelevant;
design.ApplyCrest = crestFlags & CrestExtensions.All;
design.SetApplyHatVisible(design.DoApplyEquip(EquipSlot.Head));
design.SetApplyVisorToggle(design.DoApplyEquip(EquipSlot.Head));
design.SetApplyWeaponVisible(design.DoApplyEquip(EquipSlot.MainHand) || design.DoApplyEquip(EquipSlot.OffHand));
@ -123,6 +111,7 @@ public class DesignConverter
: DesignBase.LoadDesignBase(_customize, _items, jObj2);
break;
}
default: throw new Exception($"Unknown Version {bytes[0]}.");
}
}
@ -138,6 +127,7 @@ public class DesignConverter
if (!equip)
{
ret.ApplyEquip = 0;
ret.ApplyCrest = 0;
ret.SetApplyHatVisible(false);
ret.SetApplyWeaponVisible(false);
ret.SetApplyVisorToggle(false);
@ -146,23 +136,10 @@ public class DesignConverter
return ret;
}
private static string ShareBase64(JObject jObj)
private static string ShareBase64(JObject jObject)
{
var json = jObj.ToString(Formatting.None);
var json = jObject.ToString(Formatting.None);
var compressed = json.Compress(Version);
return System.Convert.ToBase64String(compressed);
}
private static string ShareBackwardCompatible(JObject jObject, DesignBase design)
{
var oldBase64 = DesignBase64Migration.CreateOldBase64(design.DesignData, design.ApplyEquip, design.ApplyCustomizeRaw,
design.DoApplyHatVisible(), design.DoApplyVisorToggle(), design.DoApplyWeaponVisible(), design.WriteProtected(), 1f);
var oldBytes = System.Convert.FromBase64String(oldBase64);
var json = jObject.ToString(Formatting.None);
var compressed = json.Compress(Version);
var bytes = new byte[oldBytes.Length + compressed.Length];
oldBytes.CopyTo(bytes, 0);
compressed.CopyTo(bytes, oldBytes.Length);
return System.Convert.ToBase64String(bytes);
}
}

View file

@ -2,6 +2,7 @@
using System.Runtime.CompilerServices;
using Glamourer.Customization;
using Glamourer.Services;
using Glamourer.Structs;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -29,6 +30,7 @@ public unsafe struct DesignData
private fixed byte _equipmentBytes[48];
public Customize Customize = Customize.Default;
public uint ModelId;
public CrestFlag CrestVisibility;
private WeaponType _secondaryMainhand;
private WeaponType _secondaryOffhand;
private FullEquipType _typeMainhand;
@ -59,6 +61,10 @@ public unsafe struct DesignData
return index > 11 ? (StainId)0 : _equipmentBytes[4 * index + 3];
}
public readonly bool Crest(CrestFlag slot)
=> CrestVisibility.HasFlag(slot);
public FullEquipType MainhandType
=> _typeMainhand;
@ -173,6 +179,16 @@ public unsafe struct DesignData
_ => false,
};
public bool SetCrest(CrestFlag slot, bool visible)
{
var newValue = visible ? CrestVisibility | slot : CrestVisibility & ~slot;
if (newValue == CrestVisibility)
return false;
CrestVisibility = newValue;
return true;
}
public readonly bool IsWet()
=> (_states & 0x01) == 0x01;
@ -228,12 +244,15 @@ public unsafe struct DesignData
{
SetItem(slot, ItemManager.NothingItem(slot));
SetStain(slot, 0);
SetCrest(slot.ToCrestFlag(), false);
}
SetItem(EquipSlot.MainHand, items.DefaultSword);
SetStain(EquipSlot.MainHand, 0);
SetCrest(CrestFlag.MainHand, false);
SetItem(EquipSlot.OffHand, ItemManager.NothingItem(FullEquipType.Shield));
SetStain(EquipSlot.OffHand, 0);
SetCrest(CrestFlag.OffHand, false);
}

View file

@ -8,6 +8,7 @@ using Glamourer.Events;
using Glamourer.Interop.Penumbra;
using Glamourer.Services;
using Glamourer.State;
using Glamourer.Structs;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
@ -345,7 +346,7 @@ public class DesignManager
/// <summary> Change a non-weapon equipment piece. </summary>
public void ChangeEquip(Design design, EquipSlot slot, EquipItem item)
{
if (!_items.IsItemValid(slot, item.ItemId, out item))
if (!_items.IsItemValid(slot, item.Id, out item))
return;
var old = design.DesignData.Item(slot);
@ -446,6 +447,31 @@ public class DesignManager
_event.Invoke(DesignChanged.Type.ApplyStain, design, slot);
}
/// <summary> Change the crest visibility for any equipment piece. </summary>
public void ChangeCrest(Design design, CrestFlag slot, bool crest)
{
var oldCrest = design.DesignData.Crest(slot);
if (!design.GetDesignDataRef().SetCrest(slot, crest))
return;
design.LastEdit = DateTimeOffset.UtcNow;
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Set crest visibility of {slot} equipment piece to {crest}.");
_event.Invoke(DesignChanged.Type.Crest, design, (oldCrest, crest, slot));
}
/// <summary> Change whether to apply a specific crest visibility. </summary>
public void ChangeApplyCrest(Design design, CrestFlag slot, bool value)
{
if (!design.SetApplyCrest(slot, value))
return;
design.LastEdit = DateTimeOffset.UtcNow;
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Set applying of crest visibility of {slot} equipment piece to {value}.");
_event.Invoke(DesignChanged.Type.ApplyCrest, design, slot);
}
/// <summary> Change the bool value of one of the meta flags. </summary>
public void ChangeMeta(Design design, ActorState.MetaIndex metaIndex, bool value)
{
@ -515,6 +541,12 @@ public class DesignManager
if (other.DoApplyStain(slot))
ChangeStain(design, slot, other.DesignData.Stain(slot));
}
foreach (var slot in Enum.GetValues<CrestFlag>())
{
if (other.DoApplyCrest(slot))
ChangeCrest(design, slot, other.DesignData.Crest(slot));
}
}
if (other.DoApplyEquip(EquipSlot.MainHand))

View file

@ -66,7 +66,8 @@ public class EphemeralConfig : ISavable
public void Save(StreamWriter writer)
{
using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
using var jWriter = new JsonTextWriter(writer);
jWriter.Formatting = Formatting.Indented;
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
serializer.Serialize(jWriter, this);
}

View file

@ -62,6 +62,9 @@ public sealed class DesignChanged : EventWrapper<Action<DesignChanged.Type, Desi
/// <summary> An existing design had a stain changed. Data is the old stain id, the new stain id and the slot [(StainId, StainId, EquipSlot)]. </summary>
Stain,
/// <summary> An existing design had a crest visibility changed. Data is the old crest visibility, the new crest visibility and the slot [(bool, bool, EquipSlot)]. </summary>
Crest,
/// <summary> An existing design changed whether a specific customization is applied. Data is the type of customization [CustomizeIndex]. </summary>
ApplyCustomize,
@ -71,6 +74,9 @@ public sealed class DesignChanged : EventWrapper<Action<DesignChanged.Type, Desi
/// <summary> An existing design changed whether a specific stain is applied. Data is the slot of the equipment [EquipSlot]. </summary>
ApplyStain,
/// <summary> An existing design changed whether a specific crest visibility is applied. Data is the slot of the equipment [EquipSlot]. </summary>
ApplyCrest,
/// <summary> An existing design changed its write protection status. Data is the new value [bool]. </summary>
WriteProtection,

View file

@ -0,0 +1,30 @@
using System;
using OtterGui.Classes;
namespace Glamourer.Events;
/// <summary>
/// Triggered when the player equips a gear set.
/// <list type="number">
/// <item>Parameter is the name of the gear set. </item>
/// <item>Parameter is the id of the gear set. </item>
/// <item>Parameter is the id of the prior gear set. </item>
/// <item>Parameter is the id of the associated glamour. </item>
/// <item>Parameter is the job id of the associated job. </item>
/// </list>
/// </summary>
public sealed class EquippedGearset : EventWrapper<Action<string, int, int, byte, byte>, EquippedGearset.Priority>
{
public enum Priority
{
/// <seealso cref="Automation.AutoDesignApplier.OnEquippedGearset"/>
AutoDesignApplier = 0,
}
public EquippedGearset()
: base(nameof(EquippedGearset))
{ }
public void Invoke(string name, int id, int lastId, byte glamour, byte jobId)
=> Invoke(this, name, id, lastId, glamour, jobId);
}

View file

@ -37,6 +37,9 @@ public sealed class StateChanged : EventWrapper<Action<StateChanged.Type, StateC
/// <summary> A characters saved state had a stain changed. Data is the old stain id, the new stain id and the slot [(StainId, StainId, EquipSlot)]. </summary>
Stain,
/// <summary> A characters saved state had a crest visibility changed. Data is the old crest visibility, the new crest visibility and the slot [(bool, bool, EquipSlot)]. </summary>
Crest,
/// <summary> A characters saved state had a design applied. This means everything may have changed. Data is the applied design. [DesignBase] </summary>
Design,

View file

@ -194,14 +194,14 @@ public partial class CustomizationDrawer
{
switch (UiHelpers.DrawMetaToggle(_currentIndex.ToDefaultName(), tmp, _currentApply, out var newValue, out var newApply, _locked))
{
case DataChange.Item:
case (true, false):
_customize.Set(idx, newValue ? CustomizeValue.Max : CustomizeValue.Zero);
Changed |= _currentFlag;
break;
case DataChange.ApplyItem:
case (false, true):
ChangeApply = newApply ? ChangeApply | _currentFlag : ChangeApply & ~_currentFlag;
break;
case DataChange.Item | DataChange.ApplyItem:
case (true, true):
ChangeApply = newApply ? ChangeApply | _currentFlag : ChangeApply & ~_currentFlag;
_customize.Set(idx, newValue ? CustomizeValue.Max : CustomizeValue.Zero);
Changed |= _currentFlag;

View file

@ -14,18 +14,16 @@ using CustomizeData = Penumbra.GameData.Structs.CustomizeData;
namespace Glamourer.Gui.Customization;
public partial class CustomizationDrawer : IDisposable
public partial class CustomizationDrawer(DalamudPluginInterface pi, CustomizationService _service, CodeService _codes, Configuration _config)
: IDisposable
{
private readonly CodeService _codes;
private readonly Configuration _config;
private readonly Vector4 _redTint = new(0.6f, 0.3f, 0.3f, 1f);
private readonly IDalamudTextureWrap? _legacyTattoo;
private readonly Vector4 _redTint = new(0.6f, 0.3f, 0.3f, 1f);
private readonly IDalamudTextureWrap? _legacyTattoo = GetLegacyTattooIcon(pi);
private Exception? _terminate;
private Customize _customize;
private CustomizationSet _set = null!;
private Customize _customize = Customize.Default;
private CustomizationSet _set = null!;
public Customize Customize
=> _customize;
@ -46,21 +44,8 @@ public partial class CustomizationDrawer : IDisposable
private float _raceSelectorWidth;
private bool _withApply;
private readonly CustomizationService _service;
public CustomizationDrawer(DalamudPluginInterface pi, CustomizationService service, CodeService codes, Configuration config)
{
_service = service;
_codes = codes;
_config = config;
_legacyTattoo = GetLegacyTattooIcon(pi);
_customize = Customize.Default;
}
public void Dispose()
{
_legacyTattoo?.Dispose();
}
=> _legacyTattoo?.Dispose();
public bool Draw(Customize current, bool locked, bool lockedRedraw)
{
@ -125,12 +110,6 @@ public partial class CustomizationDrawer : IDisposable
Changed |= _currentFlag;
}
public bool DrawWetnessState(bool currentValue, out bool newValue, bool locked)
=> UiHelpers.DrawCheckbox("Force Wetness", "Force the character to be wet or not.", currentValue, out newValue, locked);
public DataChange DrawWetnessState(bool currentValue, bool currentApply, out bool newValue, out bool newApply, bool locked)
=> UiHelpers.DrawMetaToggle("Force Wetness", currentValue, currentApply, out newValue, out newApply, locked);
private bool DrawInternal()
{
using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, _spacing);
@ -199,13 +178,13 @@ public partial class CustomizationDrawer : IDisposable
private void UpdateSizes()
{
_spacing = ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemInnerSpacing.X };
_iconSize = new Vector2(ImGui.GetTextLineHeight() * 2 + _spacing.Y + 2 * ImGui.GetStyle().FramePadding.Y);
_framedIconSize = _iconSize + 2 * ImGui.GetStyle().FramePadding;
_inputIntSize = 2 * _framedIconSize.X + 1 * _spacing.X;
_spacing = ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemInnerSpacing.X };
_iconSize = new Vector2(ImGui.GetTextLineHeight() * 2 + _spacing.Y + 2 * ImGui.GetStyle().FramePadding.Y);
_framedIconSize = _iconSize + 2 * ImGui.GetStyle().FramePadding;
_inputIntSize = 2 * _framedIconSize.X + 1 * _spacing.X;
_inputIntSizeNoButtons = _inputIntSize - 2 * _spacing.X - 2 * ImGui.GetFrameHeight();
_comboSelectorSize = 4 * _framedIconSize.X + 3 * _spacing.X;
_raceSelectorWidth = _inputIntSize + _comboSelectorSize - _framedIconSize.X;
_comboSelectorSize = 4 * _framedIconSize.X + 3 * _spacing.X;
_raceSelectorWidth = _inputIntSize + _comboSelectorSize - _framedIconSize.X;
}
private static IDalamudTextureWrap? GetLegacyTattooIcon(DalamudPluginInterface pi)

View file

@ -22,8 +22,8 @@ public class DesignQuickBar : Window, IDisposable
{
private ImGuiWindowFlags GetFlags
=> _config.Ephemeral.LockDesignQuickBar
? ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoMove
: ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoFocusOnAppearing;
? ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoMove
: ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoFocusOnAppearing;
private readonly Configuration _config;
private readonly DesignCombo _designCombo;
@ -37,7 +37,7 @@ public class DesignQuickBar : Window, IDisposable
public DesignQuickBar(Configuration config, DesignCombo designCombo, StateManager stateManager, IKeyState keyState,
ObjectManager objects, AutoDesignApplier autoDesignApplier)
: base("Glamourer Quick Bar", ImGuiWindowFlags.NoDecoration)
: base("Glamourer Quick Bar", ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking)
{
_config = config;
_designCombo = designCombo;
@ -159,8 +159,8 @@ public class DesignQuickBar : Window, IDisposable
return;
}
var (applyGear, applyCustomize) = UiHelpers.ConvertKeysToFlags();
using var _ = design!.TemporarilyRestrictApplication(applyGear, applyCustomize);
var (applyGear, applyCustomize, applyCrest) = UiHelpers.ConvertKeysToFlags();
using var _ = design!.TemporarilyRestrictApplication(applyGear, applyCustomize, applyCrest);
_stateManager.ApplyDesign(design, state, StateChanged.Source.Manual);
}

View file

@ -0,0 +1,49 @@
using System;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.State;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Equipment;
public ref struct EquipDrawData(EquipSlot slot, in DesignData designData)
{
public readonly EquipSlot Slot = slot;
public bool Locked;
public bool DisplayApplication;
public Action<EquipItem> ItemSetter = null!;
public Action<StainId> StainSetter = null!;
public Action<bool> ApplySetter = null!;
public Action<bool> ApplyStainSetter = null!;
public EquipItem CurrentItem = designData.Item(slot);
public StainId CurrentStain = designData.Stain(slot);
public bool CurrentApply;
public bool CurrentApplyStain;
public readonly Gender CurrentGender = designData.Customize.Gender;
public readonly Race CurrentRace = designData.Customize.Race;
public static EquipDrawData FromDesign(DesignManager manager, Design design, EquipSlot slot)
=> new(slot, design.DesignData)
{
ItemSetter = i => manager.ChangeEquip(design, slot, i),
StainSetter = i => manager.ChangeStain(design, slot, i),
ApplySetter = b => manager.ChangeApplyEquip(design, slot, b),
ApplyStainSetter = b => manager.ChangeApplyStain(design, slot, b),
CurrentApply = design.DoApplyEquip(slot),
CurrentApplyStain = design.DoApplyStain(slot),
Locked = design.WriteProtected(),
DisplayApplication = true,
};
public static EquipDrawData FromState(StateManager manager, ActorState state, EquipSlot slot)
=> new(slot, state.ModelData)
{
ItemSetter = i => manager.ChangeItem(state, slot, i, StateChanged.Source.Manual),
StainSetter = i => manager.ChangeStain(state, slot, i, StateChanged.Source.Manual),
Locked = state.IsLocked,
DisplayApplication = false,
};
}

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Plugin.Services;
using Glamourer.Services;
@ -22,6 +23,9 @@ public sealed class ItemCombo : FilterComboCache<EquipItem>
private ItemId _currentItem;
private float _innerWidth;
public SetId CustomSetId { get; private set; }
public Variant CustomVariant { get; private set; }
public ItemCombo(IDataManager gameData, ItemManager items, EquipSlot slot, Logger log, FavoriteManager favorites)
: base(() => GetItems(favorites, items, slot), log)
{
@ -50,8 +54,9 @@ public sealed class ItemCombo : FilterComboCache<EquipItem>
public bool Draw(string previewName, ItemId previewIdx, float width, float innerWidth)
{
_innerWidth = innerWidth;
_currentItem = previewIdx;
_innerWidth = innerWidth;
_currentItem = previewIdx;
CustomVariant = 0;
return Draw($"##{Label}", previewName, string.Empty, width, ImGui.GetTextLineHeightWithSpacing());
}
@ -117,4 +122,18 @@ public sealed class ItemCombo : FilterComboCache<EquipItem>
enumerable = enumerable.Append(ItemManager.SmallClothesItem(slot));
return enumerable.OrderByDescending(favorites.Contains).ThenBy(i => i.Name).Prepend(nothing).ToList();
}
protected override void OnClosePopup()
{
// If holding control while the popup closes, try to parse the input as a full pair of set id and variant, and set a custom item for that.
if (!ImGui.GetIO().KeyCtrl)
return;
var split = Filter.Text.Split('-', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (split.Length != 2 || !ushort.TryParse(split[0], out var setId) || !byte.TryParse(split[1], out var variant))
return;
CustomSetId = setId;
CustomVariant = variant;
}
}

View file

@ -15,6 +15,7 @@ using Glamourer.Interop;
using Glamourer.Interop.Structs;
using Glamourer.Services;
using Glamourer.State;
using Glamourer.Structs;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;
@ -24,21 +25,11 @@ using Penumbra.GameData.Enums;
namespace Glamourer.Gui.Tabs.ActorTab;
public class ActorPanel
public class ActorPanel(ActorSelector _selector, StateManager _stateManager, CustomizationDrawer _customizationDrawer,
EquipmentDrawer _equipmentDrawer, IdentifierService _identification, AutoDesignApplier _autoDesignApplier,
Configuration _config, DesignConverter _converter, ObjectManager _objects, DesignManager _designManager, ImportService _importService,
ICondition _conditions)
{
private readonly ActorSelector _selector;
private readonly StateManager _stateManager;
private readonly CustomizationDrawer _customizationDrawer;
private readonly EquipmentDrawer _equipmentDrawer;
private readonly IdentifierService _identification;
private readonly AutoDesignApplier _autoDesignApplier;
private readonly Configuration _config;
private readonly DesignConverter _converter;
private readonly ObjectManager _objects;
private readonly DesignManager _designManager;
private readonly ImportService _importService;
private readonly ICondition _conditions;
private ActorIdentifier _identifier;
private string _actorName = string.Empty;
private Actor _actor = Actor.Null;
@ -46,29 +37,10 @@ public class ActorPanel
private ActorState? _state;
private bool _lockedRedraw;
public ActorPanel(ActorSelector selector, StateManager stateManager, CustomizationDrawer customizationDrawer,
EquipmentDrawer equipmentDrawer, IdentifierService identification, AutoDesignApplier autoDesignApplier,
Configuration config, DesignConverter converter, ObjectManager objects, DesignManager designManager, ImportService importService,
ICondition conditions)
{
_selector = selector;
_stateManager = stateManager;
_customizationDrawer = customizationDrawer;
_equipmentDrawer = equipmentDrawer;
_identification = identification;
_autoDesignApplier = autoDesignApplier;
_config = config;
_converter = converter;
_objects = objects;
_designManager = designManager;
_importService = importService;
_conditions = conditions;
}
private CustomizeFlag CustomizeApplicationFlags
=> _lockedRedraw ? CustomizeFlagExtensions.AllRelevant & ~CustomizeFlagExtensions.RedrawRequired : CustomizeFlagExtensions.AllRelevant;
public unsafe void Draw()
public void Draw()
{
using var group = ImRaii.Group();
(_identifier, _data) = _selector.Selection;
@ -161,8 +133,7 @@ public class ActorPanel
if (_customizationDrawer.Draw(_state!.ModelData.Customize, _state.IsLocked, _lockedRedraw))
_stateManager.ChangeCustomize(_state, _customizationDrawer.Customize, _customizationDrawer.Changed, StateChanged.Source.Manual);
if (_customizationDrawer.DrawWetnessState(_state!.ModelData.IsWet(), out var newWetness, _state.IsLocked))
_stateManager.ChangeWetness(_state, newWetness, StateChanged.Source.Manual);
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(ActorState.MetaIndex.Wetness, _stateManager, _state));
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
}
@ -176,63 +147,42 @@ public class ActorPanel
var usedAllStain = _equipmentDrawer.DrawAllStain(out var newAllStain, _state!.IsLocked);
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var changes = _equipmentDrawer.DrawEquip(slot, _state!.ModelData, out var newArmor, out var newStain, null, out _, out _,
_state.IsLocked);
var data = EquipDrawData.FromState(_stateManager, _state!, slot);
_equipmentDrawer.DrawEquip(data);
if (usedAllStain)
{
changes |= DataChange.Stain;
newStain = newAllStain;
}
switch (changes)
{
case DataChange.Item:
_stateManager.ChangeItem(_state, slot, newArmor, StateChanged.Source.Manual);
break;
case DataChange.Stain:
_stateManager.ChangeStain(_state, slot, newStain, StateChanged.Source.Manual);
break;
case DataChange.Item | DataChange.Stain:
_stateManager.ChangeEquip(_state, slot, newArmor, newStain, StateChanged.Source.Manual);
break;
}
_stateManager.ChangeStain(_state, slot, newAllStain, StateChanged.Source.Manual);
}
var weaponChanges = _equipmentDrawer.DrawWeapons(_state!.ModelData, out var newMainhand, out var newOffhand, out var newMainhandStain,
out var newOffhandStain, null, GameMain.IsInGPose(), out _, out _, out _, out _, _state.IsLocked);
if (usedAllStain)
var mainhand = EquipDrawData.FromState(_stateManager, _state, EquipSlot.MainHand);
var offhand = EquipDrawData.FromState(_stateManager, _state, EquipSlot.OffHand);
_equipmentDrawer.DrawWeapons(mainhand, offhand, GameMain.IsInGPose());
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
DrawEquipmentMetaToggles();
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
}
private void DrawEquipmentMetaToggles()
{
using (var _ = ImRaii.Group())
{
weaponChanges |= DataChange.Stain | DataChange.Stain2;
newMainhandStain = newAllStain;
newOffhandStain = newAllStain;
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(ActorState.MetaIndex.HatState, _stateManager, _state!));
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromState(CrestFlag.Head, _stateManager, _state!));
}
if (weaponChanges.HasFlag(DataChange.Item))
if (weaponChanges.HasFlag(DataChange.Stain))
_stateManager.ChangeEquip(_state, EquipSlot.MainHand, newMainhand, newMainhandStain, StateChanged.Source.Manual);
else
_stateManager.ChangeItem(_state, EquipSlot.MainHand, newMainhand, StateChanged.Source.Manual);
else if (weaponChanges.HasFlag(DataChange.Stain))
_stateManager.ChangeStain(_state, EquipSlot.MainHand, newMainhandStain, StateChanged.Source.Manual);
if (weaponChanges.HasFlag(DataChange.Item2))
if (weaponChanges.HasFlag(DataChange.Stain2))
_stateManager.ChangeEquip(_state, EquipSlot.OffHand, newOffhand, newOffhandStain, StateChanged.Source.Manual);
else
_stateManager.ChangeItem(_state, EquipSlot.OffHand, newOffhand, StateChanged.Source.Manual);
else if (weaponChanges.HasFlag(DataChange.Stain2))
_stateManager.ChangeStain(_state, EquipSlot.OffHand, newOffhandStain, StateChanged.Source.Manual);
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
if (EquipmentDrawer.DrawHatState(_state!.ModelData.IsHatVisible(), out var newHatState, _state!.IsLocked))
_stateManager.ChangeHatState(_state, newHatState, StateChanged.Source.Manual);
ImGui.SameLine();
if (EquipmentDrawer.DrawVisorState(_state!.ModelData.IsVisorToggled(), out var newVisorState, _state!.IsLocked))
_stateManager.ChangeVisorState(_state, newVisorState, StateChanged.Source.Manual);
using (var _ = ImRaii.Group())
{
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(ActorState.MetaIndex.VisorState, _stateManager, _state!));
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromState(CrestFlag.Body, _stateManager, _state!));
}
ImGui.SameLine();
if (EquipmentDrawer.DrawWeaponState(_state!.ModelData.IsWeaponVisible(), out var newWeaponState, _state!.IsLocked))
_stateManager.ChangeWeaponState(_state, newWeaponState, StateChanged.Source.Manual);
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
using (var _ = ImRaii.Group())
{
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(ActorState.MetaIndex.WeaponState, _stateManager, _state!));
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromState(CrestFlag.OffHand, _stateManager, _state!));
}
}
private void DrawMonsterPanel()
@ -346,8 +296,8 @@ public class ActorPanel
{
ImGui.OpenPopup("Save as Design");
_newName = _state!.Identifier.ToName();
var (applyGear, applyCustomize) = UiHelpers.ConvertKeysToFlags();
_newDesign = _converter.Convert(_state, applyGear, applyCustomize);
var (applyGear, applyCustomize, applyCrest) = UiHelpers.ConvertKeysToFlags();
_newDesign = _converter.Convert(_state, applyGear, applyCustomize, applyCrest);
}
private void SaveDesignDrawPopup()
@ -382,8 +332,8 @@ public class ActorPanel
{
try
{
var (applyGear, applyCustomize) = UiHelpers.ConvertKeysToFlags();
var text = _converter.ShareBase64(_state!, applyGear, applyCustomize);
var (applyGear, applyCustomize, applyCrest) = UiHelpers.ConvertKeysToFlags();
var text = _converter.ShareBase64(_state!, applyGear, applyCustomize, applyCrest);
ImGui.SetClipboardText(text);
}
catch (Exception ex)
@ -422,9 +372,9 @@ public class ActorPanel
!data.Valid || id == _identifier || _state!.ModelData.ModelId != 0))
return;
var (applyGear, applyCustomize) = UiHelpers.ConvertKeysToFlags();
var (applyGear, applyCustomize, applyCrest) = UiHelpers.ConvertKeysToFlags();
if (_stateManager.GetOrCreate(id, data.Objects[0], out var state))
_stateManager.ApplyDesign(_converter.Convert(_state!, applyGear, applyCustomize), state,
_stateManager.ApplyDesign(_converter.Convert(_state!, applyGear, applyCustomize, applyCrest), state,
StateChanged.Source.Manual);
}
@ -440,9 +390,9 @@ public class ActorPanel
!data.Valid || id == _identifier || _state!.ModelData.ModelId != 0))
return;
var (applyGear, applyCustomize) = UiHelpers.ConvertKeysToFlags();
var (applyGear, applyCustomize, applyCrest) = UiHelpers.ConvertKeysToFlags();
if (_stateManager.GetOrCreate(id, data.Objects[0], out var state))
_stateManager.ApplyDesign(_converter.Convert(_state!, applyGear, applyCustomize), state,
_stateManager.ApplyDesign(_converter.Convert(_state!, applyGear, applyCustomize, applyCrest), state,
StateChanged.Source.Manual);
}
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
@ -29,17 +30,18 @@ public class SetPanel
private readonly CustomizeUnlockManager _customizeUnlocks;
private readonly CustomizationService _customizations;
private readonly Configuration _config;
private readonly RevertDesignCombo _designCombo;
private readonly JobGroupCombo _jobGroupCombo;
private readonly IdentifierDrawer _identifierDrawer;
private readonly Configuration _config;
private readonly RevertDesignCombo _designCombo;
private readonly JobGroupCombo _jobGroupCombo;
private readonly IdentifierDrawer _identifierDrawer;
private string? _tempName;
private int _dragIndex = -1;
private Action? _endAction;
public SetPanel(SetSelector selector, AutoDesignManager manager, JobService jobs, ItemUnlockManager itemUnlocks, RevertDesignCombo designCombo,
public SetPanel(SetSelector selector, AutoDesignManager manager, JobService jobs, ItemUnlockManager itemUnlocks,
RevertDesignCombo designCombo,
CustomizeUnlockManager customizeUnlocks, CustomizationService customizations, IdentifierDrawer identifierDrawer, Configuration config)
{
_selector = selector;
@ -216,11 +218,11 @@ public class SetPanel
ImGui.TableNextColumn();
DrawApplicationTypeBoxes(Selection, design, idx, singleRow);
ImGui.TableNextColumn();
_jobGroupCombo.Draw(Selection, design, idx);
DrawConditions(design, idx);
}
else
{
_jobGroupCombo.Draw(Selection, design, idx);
DrawConditions(design, idx);
ImGui.TableNextColumn();
DrawApplicationTypeBoxes(Selection, design, idx, singleRow);
}
@ -244,6 +246,38 @@ public class SetPanel
_endAction = null;
}
private int _tmpGearset = int.MaxValue;
private void DrawConditions(AutoDesign design, int idx)
{
var usingGearset = design.GearsetIndex >= 0;
if (ImGui.Button($"{(usingGearset ? "Gearset:" : "Jobs:")}##usingGearset"))
{
usingGearset = !usingGearset;
_manager.ChangeGearsetCondition(Selection, idx, (short)(usingGearset ? 0 : -1));
}
ImGuiUtil.HoverTooltip("Click to switch between Job and Gearset restrictions.");
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
if (usingGearset)
{
var set = 1 + (_tmpGearset == int.MaxValue ? design.GearsetIndex : _tmpGearset);
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
if (ImGui.InputInt("##whichGearset", ref set, 0, 0))
_tmpGearset = Math.Clamp(set, 1, 100);
if (ImGui.IsItemDeactivatedAfterEdit())
{
_manager.ChangeGearsetCondition(Selection, idx, (short)(_tmpGearset - 1));
_tmpGearset = int.MaxValue;
}
}
else
{
_jobGroupCombo.Draw(Selection, design, idx);
}
}
private void DrawWarnings(AutoDesign design, int idx)
{
if (design.Revert)
@ -252,7 +286,7 @@ public class SetPanel
var size = new Vector2(ImGui.GetFrameHeight());
size.X += ImGuiHelpers.GlobalScale;
var (equipFlags, customizeFlags, _, _, _, _) = design.ApplyWhat();
var (equipFlags, customizeFlags, _, _, _, _, _) = design.ApplyWhat();
var sb = new StringBuilder();
foreach (var slot in EquipSlotExtensions.EqdpSlots.Append(EquipSlot.MainHand).Append(EquipSlot.OffHand))
{
@ -423,7 +457,7 @@ public class SetPanel
"Apply all customization changes that are enabled in this design and that are valid in a fixed design and for the given race and gender."),
(AutoDesign.Type.Armor, "Apply all armor piece changes that are enabled in this design and that are valid in a fixed design."),
(AutoDesign.Type.Accessories, "Apply all accessory changes that are enabled in this design and that are valid in a fixed design."),
(AutoDesign.Type.Stains, "Apply all dye changes that are enabled in this design."),
(AutoDesign.Type.GearCustomization, "Apply all dye and crest changes that are enabled in this design."),
(AutoDesign.Type.Weapons, "Apply all weapon changes that are enabled in this design and that are valid with the current weapon worn."),
};

View file

@ -4,6 +4,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Text;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Interface;
@ -21,6 +22,7 @@ using Glamourer.Interop.Penumbra;
using Glamourer.Interop.Structs;
using Glamourer.Services;
using Glamourer.State;
using Glamourer.Structs;
using Glamourer.Unlocks;
using Glamourer.Utility;
using ImGuiNET;
@ -43,6 +45,7 @@ public unsafe class DebugTab : ITab
private readonly VisorService _visorService;
private readonly ChangeCustomizeService _changeCustomizeService;
private readonly UpdateSlotService _updateSlotService;
private readonly CrestService _crestService;
private readonly WeaponService _weaponService;
private readonly MetaService _metaService;
private readonly InventoryService _inventoryService;
@ -50,7 +53,7 @@ public unsafe class DebugTab : ITab
private readonly ObjectManager _objectManager;
private readonly GlamourerIpc _ipc;
private readonly CodeService _code;
private readonly ImportService _importService;
private readonly ImportService _importService;
private readonly ItemManager _items;
private readonly ActorService _actors;
@ -82,7 +85,7 @@ public unsafe class DebugTab : ITab
PenumbraChangedItemTooltip penumbraTooltip, MetaService metaService, GlamourerIpc ipc, DalamudPluginInterface pluginInterface,
AutoDesignManager autoDesignManager, JobService jobs, CodeService code, CustomizeUnlockManager customizeUnlocks,
ItemUnlockManager itemUnlocks, DesignConverter designConverter, ImportService importService, InventoryService inventoryService,
HumanModelList humans, FunModule funModule)
HumanModelList humans, FunModule funModule, CrestService crestService)
{
_changeCustomizeService = changeCustomizeService;
_visorService = visorService;
@ -107,10 +110,11 @@ public unsafe class DebugTab : ITab
_customizeUnlocks = customizeUnlocks;
_itemUnlocks = itemUnlocks;
_designConverter = designConverter;
_importService = importService;
_importService = importService;
_inventoryService = inventoryService;
_humans = humans;
_funModule = funModule;
_crestService = crestService;
}
public ReadOnlySpan<byte> Label
@ -200,6 +204,7 @@ public unsafe class DebugTab : ITab
DrawWetness(actor, model);
DrawEquip(actor, model);
DrawCustomize(actor, model);
DrawCrests(actor, model);
}
private string _objectFilter = string.Empty;
@ -477,6 +482,32 @@ public unsafe class DebugTab : ITab
}
}
private void DrawCrests(Actor actor, Model model)
{
using var id = ImRaii.PushId("Crests");
CrestFlag whichToggle = 0;
CrestFlag totalModelFlags = 0;
foreach (var crestFlag in CrestExtensions.AllRelevantSet)
{
id.Push((int)crestFlag);
var modelCrest = CrestService.GetModelCrest(actor, crestFlag);
if (modelCrest)
totalModelFlags |= crestFlag;
ImGuiUtil.DrawTableColumn($"{crestFlag.ToLabel()} Crest");
ImGuiUtil.DrawTableColumn(actor.IsCharacter ? actor.GetCrest(crestFlag).ToString() : "No Character");
ImGuiUtil.DrawTableColumn(modelCrest.ToString());
ImGui.TableNextColumn();
if (model.IsHuman && ImGui.SmallButton("Toggle"))
whichToggle = crestFlag;
id.Pop();
}
if (whichToggle != 0)
_crestService.UpdateCrests(actor, totalModelFlags ^ whichToggle);
}
#endregion
#region Penumbra

View file

@ -118,6 +118,7 @@ public sealed class DesignFileSystemSelector : FileSystemSelector<Design, Design
case DesignChanged.Type.ApplyCustomize:
case DesignChanged.Type.ApplyEquip:
case DesignChanged.Type.ApplyStain:
case DesignChanged.Type.ApplyCrest:
case DesignChanged.Type.Customize:
case DesignChanged.Type.Equip:
case DesignChanged.Type.ChangedColor:

View file

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Xml.Linq;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Internal.Notifications;
@ -95,45 +94,15 @@ public class DesignPanel(DesignFileSystemSelector _selector, CustomizationDrawer
var usedAllStain = _equipmentDrawer.DrawAllStain(out var newAllStain, _selector.Selected!.WriteProtected());
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var changes = _equipmentDrawer.DrawEquip(slot, _selector.Selected!.DesignData, out var newArmor, out var newStain,
_selector.Selected.ApplyEquip, out var newApply, out var newApplyStain, _selector.Selected!.WriteProtected());
if (changes.HasFlag(DataChange.Item))
_manager.ChangeEquip(_selector.Selected, slot, newArmor);
if (changes.HasFlag(DataChange.Stain))
_manager.ChangeStain(_selector.Selected, slot, newStain);
else if (usedAllStain)
var data = EquipDrawData.FromDesign(_manager, _selector.Selected!, slot);
_equipmentDrawer.DrawEquip(data);
if (usedAllStain)
_manager.ChangeStain(_selector.Selected, slot, newAllStain);
if (changes.HasFlag(DataChange.ApplyItem))
_manager.ChangeApplyEquip(_selector.Selected, slot, newApply);
if (changes.HasFlag(DataChange.ApplyStain))
_manager.ChangeApplyStain(_selector.Selected, slot, newApplyStain);
}
var weaponChanges = _equipmentDrawer.DrawWeapons(_selector.Selected!.DesignData, out var newMainhand, out var newOffhand,
out var newMainhandStain, out var newOffhandStain, _selector.Selected.ApplyEquip, true, out var applyMain, out var applyMainStain,
out var applyOff, out var applyOffStain, _selector.Selected!.WriteProtected());
if (weaponChanges.HasFlag(DataChange.Item))
_manager.ChangeWeapon(_selector.Selected, EquipSlot.MainHand, newMainhand);
if (weaponChanges.HasFlag(DataChange.Stain))
_manager.ChangeStain(_selector.Selected, EquipSlot.MainHand, newMainhandStain);
else if (usedAllStain)
_manager.ChangeStain(_selector.Selected, EquipSlot.MainHand, newAllStain);
if (weaponChanges.HasFlag(DataChange.ApplyItem))
_manager.ChangeApplyEquip(_selector.Selected, EquipSlot.MainHand, applyMain);
if (weaponChanges.HasFlag(DataChange.ApplyStain))
_manager.ChangeApplyStain(_selector.Selected, EquipSlot.MainHand, applyMainStain);
if (weaponChanges.HasFlag(DataChange.Item2))
_manager.ChangeWeapon(_selector.Selected, EquipSlot.OffHand, newOffhand);
if (weaponChanges.HasFlag(DataChange.Stain2))
_manager.ChangeStain(_selector.Selected, EquipSlot.OffHand, newOffhandStain);
else if (usedAllStain)
_manager.ChangeStain(_selector.Selected, EquipSlot.OffHand, newAllStain);
if (weaponChanges.HasFlag(DataChange.ApplyItem2))
_manager.ChangeApplyEquip(_selector.Selected, EquipSlot.OffHand, applyOff);
if (weaponChanges.HasFlag(DataChange.ApplyStain2))
_manager.ChangeApplyStain(_selector.Selected, EquipSlot.OffHand, applyOffStain);
var mainhand = EquipDrawData.FromDesign(_manager, _selector.Selected!, EquipSlot.MainHand);
var offhand = EquipDrawData.FromDesign(_manager, _selector.Selected!, EquipSlot.OffHand);
_equipmentDrawer.DrawWeapons(mainhand, offhand, true);
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
DrawEquipmentMetaToggles();
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
@ -141,22 +110,25 @@ public class DesignPanel(DesignFileSystemSelector _selector, CustomizationDrawer
private void DrawEquipmentMetaToggles()
{
var hatChanges = EquipmentDrawer.DrawHatState(_selector.Selected!.DesignData.IsHatVisible(),
_selector.Selected.DoApplyHatVisible(),
out var newHatState, out var newHatApply, _selector.Selected.WriteProtected());
ApplyChanges(ActorState.MetaIndex.HatState, hatChanges, newHatState, newHatApply);
using (var _ = ImRaii.Group())
{
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromDesign(ActorState.MetaIndex.HatState, _manager, _selector.Selected!));
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromDesign(CrestFlag.Head, _manager, _selector.Selected!));
}
ImGui.SameLine();
var visorChanges = EquipmentDrawer.DrawVisorState(_selector.Selected!.DesignData.IsVisorToggled(),
_selector.Selected.DoApplyVisorToggle(),
out var newVisorState, out var newVisorApply, _selector.Selected.WriteProtected());
ApplyChanges(ActorState.MetaIndex.VisorState, visorChanges, newVisorState, newVisorApply);
using (var _ = ImRaii.Group())
{
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromDesign(ActorState.MetaIndex.VisorState, _manager, _selector.Selected!));
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromDesign(CrestFlag.Body, _manager, _selector.Selected!));
}
ImGui.SameLine();
var weaponChanges = EquipmentDrawer.DrawWeaponState(_selector.Selected!.DesignData.IsWeaponVisible(),
_selector.Selected.DoApplyWeaponVisible(),
out var newWeaponState, out var newWeaponApply, _selector.Selected.WriteProtected());
ApplyChanges(ActorState.MetaIndex.WeaponState, weaponChanges, newWeaponState, newWeaponApply);
using (var _ = ImRaii.Group())
{
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromDesign(ActorState.MetaIndex.WeaponState, _manager, _selector.Selected!));
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromDesign(CrestFlag.OffHand, _manager, _selector.Selected!));
}
}
private void DrawCustomize()
@ -178,12 +150,53 @@ public class DesignPanel(DesignFileSystemSelector _selector, CustomizationDrawer
_manager.ChangeCustomize(_selector.Selected, idx, _customizationDrawer.Customize[idx]);
}
var wetnessChanges = _customizationDrawer.DrawWetnessState(_selector.Selected!.DesignData.IsWet(),
_selector.Selected!.DoApplyWetness(), out var newWetnessState, out var newWetnessApply, _selector.Selected!.WriteProtected());
ApplyChanges(ActorState.MetaIndex.Wetness, wetnessChanges, newWetnessState, newWetnessApply);
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromDesign(ActorState.MetaIndex.Wetness, _manager, _selector.Selected!));
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
}
private void DrawCustomizeApplication()
{
var set = _selector.Selected!.CustomizationSet;
var available = set.SettingAvailable | CustomizeFlag.Clan | CustomizeFlag.Gender;
var flags = _selector.Selected!.ApplyCustomize == 0 ? 0 : (_selector.Selected!.ApplyCustomize & available) == available ? 3 : 1;
if (ImGui.CheckboxFlags("Apply All Customizations", ref flags, 3))
{
var newFlags = flags == 3;
_manager.ChangeApplyCustomize(_selector.Selected!, CustomizeIndex.Clan, newFlags);
_manager.ChangeApplyCustomize(_selector.Selected!, CustomizeIndex.Gender, newFlags);
foreach (var index in CustomizationExtensions.AllBasic)
_manager.ChangeApplyCustomize(_selector.Selected!, index, newFlags);
}
var applyClan = _selector.Selected!.DoApplyCustomize(CustomizeIndex.Clan);
if (ImGui.Checkbox($"Apply {CustomizeIndex.Clan.ToDefaultName()}", ref applyClan))
_manager.ChangeApplyCustomize(_selector.Selected!, CustomizeIndex.Clan, applyClan);
var applyGender = _selector.Selected!.DoApplyCustomize(CustomizeIndex.Gender);
if (ImGui.Checkbox($"Apply {CustomizeIndex.Gender.ToDefaultName()}", ref applyGender))
_manager.ChangeApplyCustomize(_selector.Selected!, CustomizeIndex.Gender, applyGender);
foreach (var index in CustomizationExtensions.All.Where(set.IsAvailable))
{
var apply = _selector.Selected!.DoApplyCustomize(index);
if (ImGui.Checkbox($"Apply {set.Option(index)}", ref apply))
_manager.ChangeApplyCustomize(_selector.Selected!, index, apply);
}
}
private void DrawCrestApplication()
{
var flags = (uint)_selector.Selected!.ApplyCrest;
var bigChange = ImGui.CheckboxFlags("Apply All Crests", ref flags, (uint)CrestExtensions.AllRelevant);
foreach (var flag in CrestExtensions.AllRelevantSet)
{
var apply = bigChange ? ((CrestFlag)flags & flag) == flag : _selector.Selected!.DoApplyCrest(flag);
if (ImGui.Checkbox($"Apply {flag.ToLabel()} Crest", ref apply) || bigChange)
_manager.ChangeApplyCrest(_selector.Selected!, flag, apply);
}
}
private void DrawApplicationRules()
{
if (!ImGui.CollapsingHeader("Application Rules"))
@ -191,33 +204,9 @@ public class DesignPanel(DesignFileSystemSelector _selector, CustomizationDrawer
using (var _ = ImRaii.Group())
{
var set = _selector.Selected!.CustomizationSet;
var available = set.SettingAvailable | CustomizeFlag.Clan | CustomizeFlag.Gender;
var flags = _selector.Selected!.ApplyCustomize == 0 ? 0 : (_selector.Selected!.ApplyCustomize & available) == available ? 3 : 1;
if (ImGui.CheckboxFlags("Apply All Customizations", ref flags, 3))
{
var newFlags = flags == 3;
_manager.ChangeApplyCustomize(_selector.Selected!, CustomizeIndex.Clan, newFlags);
_manager.ChangeApplyCustomize(_selector.Selected!, CustomizeIndex.Gender, newFlags);
foreach (var index in CustomizationExtensions.AllBasic)
_manager.ChangeApplyCustomize(_selector.Selected!, index, newFlags);
}
var applyClan = _selector.Selected!.DoApplyCustomize(CustomizeIndex.Clan);
if (ImGui.Checkbox($"Apply {CustomizeIndex.Clan.ToDefaultName()}", ref applyClan))
_manager.ChangeApplyCustomize(_selector.Selected!, CustomizeIndex.Clan, applyClan);
var applyGender = _selector.Selected!.DoApplyCustomize(CustomizeIndex.Gender);
if (ImGui.Checkbox($"Apply {CustomizeIndex.Gender.ToDefaultName()}", ref applyGender))
_manager.ChangeApplyCustomize(_selector.Selected!, CustomizeIndex.Gender, applyGender);
foreach (var index in CustomizationExtensions.All.Where(set.IsAvailable))
{
var apply = _selector.Selected!.DoApplyCustomize(index);
if (ImGui.Checkbox($"Apply {set.Option(index)}", ref apply))
_manager.ChangeApplyCustomize(_selector.Selected!, index, apply);
}
DrawCustomizeApplication();
ImGui.NewLine();
DrawCrestApplication();
}
ImGui.SameLine(ImGui.GetContentRegionAvail().X / 2);
@ -306,12 +295,14 @@ public class DesignPanel(DesignFileSystemSelector _selector, CustomizationDrawer
_manager.ChangeCustomize(_selector.Selected!, CustomizeIndex.Gender, dat.Customize[CustomizeIndex.Gender]);
foreach (var idx in CustomizationExtensions.AllBasic)
_manager.ChangeCustomize(_selector.Selected!, idx, dat.Customize[idx]);
Glamourer.Messager.NotificationMessage($"Applied games .dat file {dat.Description} customizations to {_selector.Selected.Name}.", NotificationType.Success, false);
Glamourer.Messager.NotificationMessage(
$"Applied games .dat file {dat.Description} customizations to {_selector.Selected.Name}.", NotificationType.Success, false);
}
else if (_importService.CreateCharaTarget(out var designBase, out var name))
{
_manager.ApplyDesign(_selector.Selected!, designBase);
Glamourer.Messager.NotificationMessage($"Applied Anamnesis .chara file {name} to {_selector.Selected.Name}.", NotificationType.Success, false);
Glamourer.Messager.NotificationMessage($"Applied Anamnesis .chara file {name} to {_selector.Selected.Name}.",
NotificationType.Success, false);
}
}
@ -397,8 +388,8 @@ public class DesignPanel(DesignFileSystemSelector _selector, CustomizationDrawer
if (_state.GetOrCreate(id, data.Objects[0], out var state))
{
var (applyGear, applyCustomize) = UiHelpers.ConvertKeysToFlags();
using var _ = _selector.Selected!.TemporarilyRestrictApplication(applyGear, applyCustomize);
var (applyGear, applyCustomize, applyCrest) = UiHelpers.ConvertKeysToFlags();
using var _ = _selector.Selected!.TemporarilyRestrictApplication(applyGear, applyCustomize, applyCrest);
_state.ApplyDesign(_selector.Selected!, state, StateChanged.Source.Manual);
}
}
@ -416,8 +407,8 @@ public class DesignPanel(DesignFileSystemSelector _selector, CustomizationDrawer
if (_state.GetOrCreate(id, data.Objects[0], out var state))
{
var (applyGear, applyCustomize) = UiHelpers.ConvertKeysToFlags();
using var _ = _selector.Selected!.TemporarilyRestrictApplication(applyGear, applyCustomize);
var (applyGear, applyCustomize, applyCrest) = UiHelpers.ConvertKeysToFlags();
using var _ = _selector.Selected!.TemporarilyRestrictApplication(applyGear, applyCustomize, applyCrest);
_state.ApplyDesign(_selector.Selected!, state, StateChanged.Source.Manual);
}
}
@ -441,23 +432,6 @@ public class DesignPanel(DesignFileSystemSelector _selector, CustomizationDrawer
_fileDialog.Draw();
}
private void ApplyChanges(ActorState.MetaIndex index, DataChange change, bool value, bool apply)
{
switch (change)
{
case DataChange.Item:
_manager.ChangeMeta(_selector.Selected!, index, value);
break;
case DataChange.ApplyItem:
_manager.ChangeApplyMeta(_selector.Selected!, index, apply);
break;
case DataChange.Item | DataChange.ApplyItem:
_manager.ChangeApplyMeta(_selector.Selected!, index, apply);
_manager.ChangeMeta(_selector.Selected!, index, value);
break;
}
}
private static unsafe string GetUserPath()
=> Framework.Instance()->UserPath;
}

View file

@ -0,0 +1,104 @@
using System;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.State;
using Glamourer.Structs;
using Penumbra.GameData.Enums;
namespace Glamourer.Gui;
public ref struct ToggleDrawData
{
public bool Locked;
public bool DisplayApplication;
public bool CurrentValue;
public bool CurrentApply;
public Action<bool> SetValue = null!;
public Action<bool> SetApply = null!;
public string Label = string.Empty;
public string Tooltip = string.Empty;
public ToggleDrawData()
{ }
public static ToggleDrawData FromDesign(ActorState.MetaIndex index, DesignManager manager, Design design)
{
var (label, value, apply, setValue, setApply) = index switch
{
ActorState.MetaIndex.HatState => ("Hat Visible", design.DesignData.IsHatVisible(), design.DoApplyHatVisible(),
(Action<bool>)(b => manager.ChangeMeta(design, index, b)), (Action<bool>)(b => manager.ChangeApplyMeta(design, index, b))),
ActorState.MetaIndex.VisorState => ("Visor Toggled", design.DesignData.IsVisorToggled(), design.DoApplyVisorToggle(),
b => manager.ChangeMeta(design, index, b), b => manager.ChangeApplyMeta(design, index, b)),
ActorState.MetaIndex.WeaponState => ("Weapon Visible", design.DesignData.IsWeaponVisible(), design.DoApplyWeaponVisible(),
b => manager.ChangeMeta(design, index, b), b => manager.ChangeApplyMeta(design, index, b)),
ActorState.MetaIndex.Wetness => ("Force Wetness", design.DesignData.IsWet(), design.DoApplyWetness(),
b => manager.ChangeMeta(design, index, b), b => manager.ChangeApplyMeta(design, index, b)),
_ => throw new Exception("Unsupported meta index."),
};
return new ToggleDrawData
{
Label = label,
Tooltip = string.Empty,
Locked = design.WriteProtected(),
DisplayApplication = true,
CurrentValue = value,
CurrentApply = apply,
SetValue = setValue,
SetApply = setApply,
};
}
public static ToggleDrawData CrestFromDesign(CrestFlag slot, DesignManager manager, Design design)
=> new()
{
Label = $"{slot.ToLabel()} Crest",
Tooltip = string.Empty,
Locked = design.WriteProtected(),
DisplayApplication = true,
CurrentValue = design.DesignData.Crest(slot),
CurrentApply = design.DoApplyCrest(slot),
SetValue = v => manager.ChangeCrest(design, slot, v),
SetApply = v => manager.ChangeApplyCrest(design, slot, v),
};
public static ToggleDrawData CrestFromState(CrestFlag slot, StateManager manager, ActorState state)
=> new()
{
Label = $"{slot.ToLabel()} Crest",
Tooltip = "Hide or show your free company crest on this piece of gear.",
Locked = state.IsLocked,
CurrentValue = state.ModelData.Crest(slot),
SetValue = v => manager.ChangeCrest(state, slot, v, StateChanged.Source.Manual),
};
public static ToggleDrawData FromState(ActorState.MetaIndex index, StateManager manager, ActorState state)
{
var (label, tooltip, value, setValue) = index switch
{
ActorState.MetaIndex.HatState => ("Hat Visible", "Hide or show the characters head gear.", state.ModelData.IsHatVisible(),
(Action<bool>)(b => manager.ChangeHatState(state, b, StateChanged.Source.Manual))),
ActorState.MetaIndex.VisorState => ("Visor Toggled", "Toggle the visor state of the characters head gear.",
state.ModelData.IsVisorToggled(),
b => manager.ChangeVisorState(state, b, StateChanged.Source.Manual)),
ActorState.MetaIndex.WeaponState => ("Weapon Visible", "Hide or show the characters weapons when not drawn.",
state.ModelData.IsWeaponVisible(),
b => manager.ChangeWeaponState(state, b, StateChanged.Source.Manual)),
ActorState.MetaIndex.Wetness => ("Force Wetness", "Force the character to be wet or not.", state.ModelData.IsWet(),
b => manager.ChangeWetness(state, b, StateChanged.Source.Manual)),
_ => throw new Exception("Unsupported meta index."),
};
return new ToggleDrawData
{
Label = label,
Tooltip = tooltip,
Locked = state.IsLocked,
CurrentValue = value,
SetValue = setValue,
};
}
}

View file

@ -1,4 +1,3 @@
using System;
using System.Numerics;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
@ -9,27 +8,13 @@ using Glamourer.Unlocks;
using ImGuiNET;
using Lumina.Misc;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui;
[Flags]
public enum DataChange : byte
{
None = 0x00,
Item = 0x01,
Stain = 0x02,
ApplyItem = 0x04,
ApplyStain = 0x08,
Item2 = 0x10,
Stain2 = 0x20,
ApplyItem2 = 0x40,
ApplyStain2 = 0x80,
}
public static class UiHelpers
{
/// <summary> Open a combo popup with another method than the combo itself. </summary>
@ -71,14 +56,15 @@ public static class UiHelpers
return ret;
}
public static DataChange DrawMetaToggle(string label, bool currentValue, bool currentApply, out bool newValue,
public static (bool, bool) DrawMetaToggle(string label, bool currentValue, bool currentApply, out bool newValue,
out bool newApply, bool locked)
{
var flags = (sbyte)(currentApply ? currentValue ? 1 : -1 : 0);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing);
using (var disabled = ImRaii.Disabled(locked))
{
if (new TristateCheckbox(ColorId.TriStateCross.Value(), ColorId.TriStateCheck.Value(), ColorId.TriStateNeutral.Value()).Draw("##" + label, flags, out flags))
if (new TristateCheckbox(ColorId.TriStateCross.Value(), ColorId.TriStateCheck.Value(), ColorId.TriStateNeutral.Value()).Draw(
"##" + label, flags, out flags))
{
(newValue, newApply) = flags switch
{
@ -99,22 +85,16 @@ public static class UiHelpers
ImGui.SameLine();
ImGui.TextUnformatted(label);
return (currentApply != newApply, currentValue != newValue) switch
{
(true, true) => DataChange.ApplyItem | DataChange.Item,
(true, false) => DataChange.ApplyItem,
(false, true) => DataChange.Item,
_ => DataChange.None,
};
return (currentValue != newValue, currentApply != newApply);
}
public static (EquipFlag, CustomizeFlag) ConvertKeysToFlags()
public static (EquipFlag, CustomizeFlag, CrestFlag) ConvertKeysToFlags()
=> (ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift) switch
{
(false, false) => (EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant),
(true, true) => (EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant),
(true, false) => (EquipFlagExtensions.All, (CustomizeFlag)0),
(false, true) => ((EquipFlag)0, CustomizeFlagExtensions.AllRelevant),
(false, false) => (EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant, CrestExtensions.All),
(true, true) => (EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant, CrestExtensions.All),
(true, false) => (EquipFlagExtensions.All, (CustomizeFlag)0, CrestExtensions.All),
(false, true) => ((EquipFlag)0, CustomizeFlagExtensions.AllRelevant, 0),
};
public static (bool, bool) ConvertKeysToBool()

View file

@ -23,7 +23,7 @@ public unsafe class ChangeCustomizeService : EventWrapper<Action<Model, Ref<Cust
private readonly delegate* unmanaged[Stdcall]<Human*, byte*, bool, bool> _original;
/// <summary> Check whether we in a manual customize update, in which case we need to not toggle certain flags. </summary>
public static readonly ThreadLocal<bool> InUpdate = new(() => false);
public static readonly InMethodChecker InUpdate = new();
public enum Priority
{
@ -70,9 +70,8 @@ public unsafe class ChangeCustomizeService : EventWrapper<Action<Model, Ref<Cust
return false;
Glamourer.Log.Verbose($"[ChangeCustomize] Invoked on 0x{model.Address:X} with {customize}.");
InUpdate.Value = true;
var ret = _original(model.AsHuman, customize.Data, true);
InUpdate.Value = false;
using var _ = InUpdate.EnterMethod();
var ret = _original(model.AsHuman, customize.Data, true);
return ret;
}
@ -81,7 +80,7 @@ public unsafe class ChangeCustomizeService : EventWrapper<Action<Model, Ref<Cust
private bool ChangeCustomizeDetour(Human* human, byte* data, byte skipEquipment)
{
if (!InUpdate.Value)
if (!InUpdate.InMethod)
{
var customize = new Ref<Customize>(new Customize(*(CustomizeData*)data));
Invoke(this, (Model)human, customize);

View file

@ -0,0 +1,111 @@
using System;
using Glamourer.Designs;
using Glamourer.Services;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Interop.CharaFile;
public sealed class CmaFile
{
public string Name = string.Empty;
public DesignData Data = new();
public static CmaFile? ParseData(ItemManager items, string data, string? name = null)
{
try
{
var jObj = JObject.Parse(data);
var ret = new CmaFile();
ret.Data.SetDefaultEquipment(items);
ParseMainHand(items, jObj, ref ret.Data);
ParseOffHand(items, jObj, ref ret.Data);
ret.Name = jObj["Description"]?.ToObject<string>() ?? name ?? "New Design";
ParseEquipment(items, jObj, ref ret.Data);
ParseCustomization(jObj, ref ret.Data);
return ret;
}
catch
{
return null;
}
}
private static unsafe void ParseCustomization(JObject jObj, ref DesignData data)
{
var bytes = jObj["CharacterBytes"]?.ToObject<string>() ?? string.Empty;
if (bytes.Length is not 26 * 3 - 1)
return;
bytes = bytes.Replace(" ", string.Empty);
var byteData = Convert.FromHexString(bytes);
fixed (byte* ptr = byteData)
{
data.Customize.Data.Read(ptr);
}
}
private static unsafe void ParseEquipment(ItemManager items, JObject jObj, ref DesignData data)
{
var bytes = jObj["EquipmentBytes"]?.ToObject<string>() ?? string.Empty;
bytes = bytes.Replace(" ", string.Empty);
var byteData = Convert.FromHexString(bytes);
fixed (byte* ptr = byteData)
{
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var idx = slot.ToIndex();
if (idx * 4 + 3 >= byteData.Length)
continue;
var armor = ((CharacterArmor*)ptr)[idx];
var item = items.Identify(slot, armor.Set, armor.Variant);
data.SetItem(slot, item);
data.SetStain(slot, armor.Stain);
}
data.Customize.Data.Read(ptr);
}
}
private static void ParseMainHand(ItemManager items, JObject jObj, ref DesignData data)
{
var mainhand = jObj["MainHand"];
if (mainhand == null)
{
data.SetItem(EquipSlot.MainHand, items.DefaultSword);
data.SetStain(EquipSlot.MainHand, 0);
return;
}
var set = mainhand["Item1"]?.ToObject<ushort>() ?? items.DefaultSword.ModelId;
var type = mainhand["Item2"]?.ToObject<ushort>() ?? items.DefaultSword.WeaponType;
var variant = mainhand["Item3"]?.ToObject<byte>() ?? items.DefaultSword.Variant;
var stain = mainhand["Item4"]?.ToObject<byte>() ?? 0;
var item = items.Identify(EquipSlot.MainHand, set, type, variant);
data.SetItem(EquipSlot.MainHand, item.Valid ? item : items.DefaultSword);
data.SetStain(EquipSlot.MainHand, stain);
}
private static void ParseOffHand(ItemManager items, JObject jObj, ref DesignData data)
{
var offhand = jObj["OffHand"];
var defaultOffhand = items.GetDefaultOffhand(data.Item(EquipSlot.MainHand));
if (offhand == null)
{
data.SetItem(EquipSlot.MainHand, defaultOffhand);
data.SetStain(EquipSlot.MainHand, defaultOffhand.ModelId.Id == 0 ? 0 : data.Stain(EquipSlot.MainHand));
return;
}
var set = offhand["Item1"]?.ToObject<ushort>() ?? items.DefaultSword.ModelId;
var type = offhand["Item2"]?.ToObject<ushort>() ?? items.DefaultSword.WeaponType;
var variant = offhand["Item3"]?.ToObject<byte>() ?? items.DefaultSword.Variant;
var stain = offhand["Item4"]?.ToObject<byte>() ?? 0;
var item = items.Identify(EquipSlot.OffHand, set, type, variant, data.MainhandType);
data.SetItem(EquipSlot.OffHand, item.Valid ? item : defaultOffhand);
data.SetStain(EquipSlot.OffHand, defaultOffhand.ModelId.Id == 0 ? 0 : (StainId)stain);
}
}

View file

@ -0,0 +1,155 @@
using System;
using System.Linq;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Interop.Structs;
using Glamourer.Structs;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
namespace Glamourer.Interop;
/// <summary>
/// Triggered when the crest visibility is updated on a model.
/// <list type="number">
/// <item>Parameter is the model with an update. </item>
/// <item>Parameter is the equipment slot changed. </item>
/// <item>Parameter is the whether the crest will be shown. </item>
/// </list>
/// </summary>
public sealed unsafe class CrestService : EventWrapper<Action<Actor, CrestFlag, Ref<bool>>, CrestService.Priority>
{
public enum Priority
{
/// <seealso cref="State.StateListener.OnCrestChange"/>
StateListener = 0,
}
public CrestService(IGameInteropProvider interop)
: base(nameof(CrestService))
{
interop.InitializeFromAttributes(this);
_humanSetFreeCompanyCrestVisibleOnSlot =
interop.HookFromAddress<SetCrestDelegateIntern>(_humanVTable[96], HumanSetFreeCompanyCrestVisibleOnSlotDetour);
_weaponSetFreeCompanyCrestVisibleOnSlot =
interop.HookFromAddress<SetCrestDelegateIntern>(_weaponVTable[96], WeaponSetFreeCompanyCrestVisibleOnSlotDetour);
_humanSetFreeCompanyCrestVisibleOnSlot.Enable();
_weaponSetFreeCompanyCrestVisibleOnSlot.Enable();
_crestChangeHook.Enable();
}
public void UpdateCrests(Actor gameObject, CrestFlag flags)
{
if (!gameObject.IsCharacter)
return;
flags &= CrestExtensions.AllRelevant;
var currentCrests = gameObject.CrestBitfield;
using var update = _inUpdate.EnterMethod();
_crestChangeHook.Original(gameObject.AsCharacter, (byte) flags);
gameObject.CrestBitfield = currentCrests;
}
public delegate void DrawObjectCrestUpdateDelegate(Model drawObject, CrestFlag slot, ref bool value);
public event DrawObjectCrestUpdateDelegate? ModelCrestSetup;
protected override void Dispose(bool _)
{
_humanSetFreeCompanyCrestVisibleOnSlot.Dispose();
_weaponSetFreeCompanyCrestVisibleOnSlot.Dispose();
_crestChangeHook.Dispose();
}
private delegate void CrestChangeDelegate(Character* character, byte crestFlags);
[Signature("E8 ?? ?? ?? ?? 48 8B 55 ?? 49 8B CE E8", DetourName = nameof(CrestChangeDetour))]
private readonly Hook<CrestChangeDelegate> _crestChangeHook = null!;
private void CrestChangeDetour(Character* character, byte crestFlags)
{
var actor = (Actor)character;
foreach (var slot in CrestExtensions.AllRelevantSet)
{
var newValue = new Ref<bool>(((CrestFlag)crestFlags).HasFlag(slot));
Invoke(this, actor, slot, newValue);
crestFlags = (byte)(newValue.Value ? crestFlags | (byte)slot : crestFlags & (byte)~slot);
}
Glamourer.Log.Information(
$"Called CrestChange on {(ulong)character:X} with {crestFlags:X} and prior flags {((Actor)character).CrestBitfield}.");
using var _ = _inUpdate.EnterMethod();
_crestChangeHook.Original(character, crestFlags);
}
public static bool GetModelCrest(Actor gameObject, CrestFlag slot)
{
if (!gameObject.IsCharacter)
return false;
var (type, index) = slot.ToIndex();
switch (type)
{
case CrestType.Human:
{
var model = gameObject.Model;
if (!model.IsHuman)
return false;
var getter = (delegate* unmanaged<Human*, byte, byte>)((nint*)model.AsCharacterBase->VTable)[95];
return getter(model.AsHuman, index) != 0;
}
case CrestType.Offhand:
{
var model = (Model)gameObject.AsCharacter->DrawData.Weapon(DrawDataContainer.WeaponSlot.OffHand).DrawObject;
if (!model.IsWeapon)
return false;
var getter = (delegate* unmanaged<Weapon*, byte, byte>)((nint*)model.AsCharacterBase->VTable)[95];
return getter(model.AsWeapon, index) != 0;
}
}
return false;
}
private readonly InMethodChecker _inUpdate = new();
private delegate void SetCrestDelegateIntern(DrawObject* drawObject, byte slot, byte visible);
[Signature(global::Penumbra.GameData.Sigs.HumanVTable, ScanType = ScanType.StaticAddress)]
private readonly nint* _humanVTable = null!;
[Signature(global::Penumbra.GameData.Sigs.WeaponVTable, ScanType = ScanType.StaticAddress)]
private readonly nint* _weaponVTable = null!;
private readonly Hook<SetCrestDelegateIntern> _humanSetFreeCompanyCrestVisibleOnSlot;
private readonly Hook<SetCrestDelegateIntern> _weaponSetFreeCompanyCrestVisibleOnSlot;
private void HumanSetFreeCompanyCrestVisibleOnSlotDetour(DrawObject* drawObject, byte slotIdx, byte visible)
{
var rVisible = visible != 0;
var inUpdate = _inUpdate.InMethod;
var slot = (CrestFlag)((ushort)CrestFlag.Head << slotIdx);
if (!inUpdate)
ModelCrestSetup?.Invoke(drawObject, slot, ref rVisible);
Glamourer.Log.Excessive(
$"[Human.SetFreeCompanyCrestVisibleOnSlot] Called with 0x{(ulong)drawObject:X} for slot {slot} with {rVisible} (original: {visible != 0}, in update: {inUpdate}).");
_humanSetFreeCompanyCrestVisibleOnSlot.Original(drawObject, slotIdx, rVisible ? (byte)1 : (byte)0);
}
private void WeaponSetFreeCompanyCrestVisibleOnSlotDetour(DrawObject* drawObject, byte slotIdx, byte visible)
{
var rVisible = visible != 0;
var inUpdate = _inUpdate.InMethod;
if (!inUpdate && slotIdx == 0)
ModelCrestSetup?.Invoke(drawObject, CrestFlag.OffHand, ref rVisible);
Glamourer.Log.Excessive(
$"[Weapon.SetFreeCompanyCrestVisibleOnSlot] Called with 0x{(ulong)drawObject:X} with {rVisible} (original: {visible != 0}, in update: {inUpdate}).");
_weaponSetFreeCompanyCrestVisibleOnSlot.Original(drawObject, slotIdx, rVisible ? (byte)1 : (byte)0);
}
}

View file

@ -6,7 +6,9 @@ using Dalamud.Interface.DragDrop;
using Dalamud.Interface.Internal.Notifications;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Interop.CharaFile;
using Glamourer.Services;
using Glamourer.Structs;
using ImGuiNET;
using OtterGui.Classes;
@ -22,9 +24,9 @@ public class ImportService(CustomizationService _customizations, IDragDropManage
});
public void CreateCharaSource()
=> _dragDropManager.CreateImGuiSource("CharaDragger", m => m.Files.Count == 1 && m.Extensions.Contains(".chara"), m =>
=> _dragDropManager.CreateImGuiSource("CharaDragger", m => m.Files.Count == 1 && m.Extensions.Contains(".chara") || m.Extensions.Contains(".cma"), m =>
{
ImGui.TextUnformatted($"Dragging {Path.GetFileName(m.Files[0])} to import Anamnesis data for Glamourer...");
ImGui.TextUnformatted($"Dragging {Path.GetFileName(m.Files[0])} to import Anamnesis/CMTool data for Glamourer...");
return true;
});
@ -48,7 +50,7 @@ public class ImportService(CustomizationService _customizations, IDragDropManage
return false;
}
return LoadChara(files[0], out design, out name);
return Path.GetExtension(files[0]) is ".chara" ? LoadChara(files[0], out design, out name) : LoadCma(files[0], out design, out name);
}
public bool LoadChara(string path, [NotNullWhen(true)] out DesignBase? design, out string name)
@ -81,6 +83,36 @@ public class ImportService(CustomizationService _customizations, IDragDropManage
return true;
}
public bool LoadCma(string path, [NotNullWhen(true)] out DesignBase? design, out string name)
{
if (!File.Exists(path))
{
design = null;
name = string.Empty;
return false;
}
try
{
var text = File.ReadAllText(path);
var file = CmaFile.ParseData(_items, text, Path.GetFileNameWithoutExtension(path));
if (file == null)
throw new Exception();
name = file.Name;
design = new DesignBase(_customizations, file.Data, EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant);
}
catch (Exception ex)
{
Glamourer.Messager.NotificationMessage(ex, $"Could not read .cma file {path}.", NotificationType.Error);
design = null;
name = string.Empty;
return false;
}
return true;
}
public bool LoadDat(string path, out DatCharacterFile file)
{
if (!File.Exists(path))

View file

@ -7,17 +7,20 @@ using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using Glamourer.Events;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.String;
namespace Glamourer.Interop;
public unsafe class InventoryService : IDisposable
{
private readonly MovedEquipment _event;
private readonly MovedEquipment _movedItemsEvent;
private readonly EquippedGearset _gearsetEvent;
private readonly List<(EquipSlot, uint, StainId)> _itemList = new(12);
public InventoryService(MovedEquipment @event, IGameInteropProvider interop)
public InventoryService(MovedEquipment movedItemsEvent, IGameInteropProvider interop, EquippedGearset gearsetEvent)
{
_event = @event;
_movedItemsEvent = movedItemsEvent;
_gearsetEvent = gearsetEvent;
_moveItemHook = interop.HookFromAddress<MoveItemDelegate>((nint)InventoryManager.MemberFunctionPointers.MoveItemSlot, MoveItemDetour);
_equipGearsetHook =
@ -39,7 +42,10 @@ public unsafe class InventoryService : IDisposable
private int EquipGearSetDetour(RaptureGearsetModule* module, int gearsetId, byte glamourPlateId)
{
var ret = _equipGearsetHook.Original(module, gearsetId, glamourPlateId);
var prior = module->CurrentGearsetIndex;
var ret = _equipGearsetHook.Original(module, gearsetId, glamourPlateId);
var set = module->GetGearset(gearsetId);
_gearsetEvent.Invoke(new ByteString(set->Name).ToString(), gearsetId, prior, glamourPlateId, set->ClassJob);
Glamourer.Log.Excessive($"[InventoryService] Applied gear set {gearsetId} with glamour plate {glamourPlateId} (Returned {ret})");
if (ret == 0)
{
@ -64,7 +70,7 @@ public unsafe class InventoryService : IDisposable
else if (item.GlamourId != 0)
_itemList.Add((slot, item.GlamourId, item.Stain));
else
_itemList.Add((slot, item.ItemID, item.Stain));
_itemList.Add((slot, FixId(item.ItemID), item.Stain));
}
var plate = MirageManager.Instance()->GlamourPlatesSpan[glamourPlateId - 1];
@ -90,7 +96,7 @@ public unsafe class InventoryService : IDisposable
else if (item.GlamourId != 0)
_itemList.Add((slot, item.GlamourId, item.Stain));
else
_itemList.Add((slot, item.ItemID, item.Stain));
_itemList.Add((slot, FixId(item.ItemID), item.Stain));
}
Add(EquipSlot.MainHand, ref entry->MainHand);
@ -107,12 +113,15 @@ public unsafe class InventoryService : IDisposable
Add(EquipSlot.LFinger, ref entry->RingLeft);
}
_event.Invoke(_itemList.ToArray());
_movedItemsEvent.Invoke(_itemList.ToArray());
}
return ret;
}
private static uint FixId(uint itemId)
=> itemId % 50000;
private delegate int MoveItemDelegate(InventoryManager* manager, InventoryType sourceContainer, ushort sourceSlot,
InventoryType targetContainer, ushort targetSlot, byte unk);
@ -127,18 +136,18 @@ public unsafe class InventoryService : IDisposable
{
if (InvokeSource(sourceContainer, sourceSlot, out var source))
if (InvokeTarget(manager, targetContainer, targetSlot, out var target))
_event.Invoke(new[]
_movedItemsEvent.Invoke(new[]
{
source,
target,
});
else
_event.Invoke(new[]
_movedItemsEvent.Invoke(new[]
{
source,
});
else if (InvokeTarget(manager, targetContainer, targetSlot, out var target))
_event.Invoke(new[]
_movedItemsEvent.Invoke(new[]
{
target,
});

View file

@ -3,6 +3,7 @@ using System;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Glamourer.Customization;
using Glamourer.Structs;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.String;
@ -106,6 +107,9 @@ public readonly unsafe struct Actor : IEquatable<Actor>
public CharacterArmor GetArmor(EquipSlot slot)
=> ((CharacterArmor*)&AsCharacter->DrawData.Head)[slot.ToIndex()];
public bool GetCrest(CrestFlag slot)
=> CrestBitfield.HasFlag(slot);
public CharacterWeapon GetMainhand()
=> new(AsCharacter->DrawData.Weapon(DrawDataContainer.WeaponSlot.MainHand).ModelId.Value);
@ -115,6 +119,10 @@ public readonly unsafe struct Actor : IEquatable<Actor>
public Customize GetCustomize()
=> *(Customize*)&AsCharacter->DrawData.CustomizeData;
// TODO remove this when available in ClientStructs
internal ref CrestFlag CrestBitfield
=> ref *(CrestFlag*)((byte*)Address + 0x1BBB);
public override string ToString()
=> $"0x{Address:X}";
}

View file

@ -21,9 +21,7 @@ public unsafe class UpdateSlotService : IDisposable
}
public void Dispose()
{
_flagSlotForUpdateHook.Dispose();
}
=> _flagSlotForUpdateHook.Dispose();
public void UpdateSlot(Model drawObject, EquipSlot slot, CharacterArmor data)
{

View file

@ -5,6 +5,7 @@ using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using Glamourer.Events;
using Glamourer.Interop.Structs;
using Glamourer.Structs;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -13,6 +14,7 @@ namespace Glamourer.Interop;
public unsafe class WeaponService : IDisposable
{
private readonly WeaponLoading _event;
private readonly CrestService _crestService;
private readonly ThreadLocal<bool> _inUpdate = new(() => false);
@ -20,9 +22,10 @@ public unsafe class WeaponService : IDisposable
_original;
public WeaponService(WeaponLoading @event, IGameInteropProvider interop)
public WeaponService(WeaponLoading @event, IGameInteropProvider interop, CrestService crestService)
{
_event = @event;
_event = @event;
_crestService = crestService;
_loadWeaponHook =
interop.HookFromAddress<LoadWeaponDelegate>((nint)DrawDataContainer.MemberFunctionPointers.LoadWeapon, LoadWeaponDetour);
_original =
@ -64,8 +67,12 @@ public unsafe class WeaponService : IDisposable
// First call the regular function.
if (equipSlot is not EquipSlot.Unknown)
_event.Invoke(actor, equipSlot, ref tmpWeapon);
// Sage hack for weapons appearing in animations?
else if (weaponValue == actor.GetMainhand().Value)
_event.Invoke(actor, EquipSlot.MainHand, ref tmpWeapon);
_loadWeaponHook.Original(drawData, slot, weapon.Value, redrawOnEquality, unk2, skipGameObject, unk4);
if (tmpWeapon.Value != weapon.Value)
{
if (tmpWeapon.Set.Id == 0)

View file

@ -165,7 +165,7 @@ public class CommandService : IDisposable
.AddInitialPurple("Customizations, ")
.AddInitialPurple("Equipment, ")
.AddInitialPurple("Accessories, ")
.AddInitialPurple("Dyes and ")
.AddInitialPurple("Dyes & Crests and ")
.AddInitialPurple("Weapons, where ").AddPurple("CEADW")
.AddText(" means everything should be toggled on, and no value means nothing should be toggled on.")
.BuiltString);
@ -268,7 +268,7 @@ public class CommandService : IDisposable
applicationFlags |= AutoDesign.Type.Accessories;
break;
case 'd':
applicationFlags |= AutoDesign.Type.Stains;
applicationFlags |= AutoDesign.Type.GearCustomization;
break;
case 'w':
applicationFlags |= AutoDesign.Type.Weapons;
@ -472,7 +472,7 @@ public class CommandService : IDisposable
&& _stateManager.GetOrCreate(identifier, data.Objects[0], out state)))
continue;
var design = _converter.Convert(state, EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant);
var design = _converter.Convert(state, EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant, CrestExtensions.All);
_designManager.CreateClone(design, split[0], true);
return true;
}

View file

@ -66,7 +66,7 @@ public class ItemManager : IDisposable
public static EquipItem SmallClothesItem(EquipSlot slot)
=> new(SmallClothesNpc, SmallclothesId(slot), 0, SmallClothesNpcModel, 0, 1, slot.ToEquipType(), 0, 0, 0);
public EquipItem Resolve(EquipSlot slot, ItemId itemId)
public EquipItem Resolve(EquipSlot slot, CustomItemId itemId)
{
slot = slot.ToSlot();
if (itemId == NothingId(slot))
@ -74,7 +74,7 @@ public class ItemManager : IDisposable
if (itemId == SmallclothesId(slot))
return SmallClothesItem(slot);
if (!ItemService.AwaitedService.TryGetValue(itemId, slot, out var item))
if (!itemId.IsItem || !ItemService.AwaitedService.TryGetValue(itemId.Item, slot, out var item))
return EquipItem.FromId(itemId);
if (item.Type.ToSlot() != slot)
@ -151,7 +151,7 @@ public class ItemManager : IDisposable
/// <summary> Returns whether an item id represents a valid item for a slot and gives the item. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public bool IsItemValid(EquipSlot slot, ItemId itemId, out EquipItem item)
public bool IsItemValid(EquipSlot slot, CustomItemId itemId, out EquipItem item)
{
item = Resolve(slot, itemId);
return item.Valid;

View file

@ -73,6 +73,7 @@ public static class ServiceManager
.AddSingleton<ObjectUnlocked>()
.AddSingleton<TabSelected>()
.AddSingleton<MovedEquipment>()
.AddSingleton<EquippedGearset>()
.AddSingleton<GPoseService>()
.AddSingleton<PenumbraReloaded>();
@ -97,6 +98,7 @@ public static class ServiceManager
.AddSingleton<CustomizeUnlockManager>()
.AddSingleton<ItemUnlockManager>()
.AddSingleton<ImportService>()
.AddSingleton<CrestService>()
.AddSingleton<InventoryService>()
.AddSingleton<ContextMenuService>()
.AddSingleton<ScalingService>();

View file

@ -80,7 +80,8 @@ public class ActorState
/// <summary> This contains whether a change to the base data was made by the game, the user via manual input or through automatic application. </summary>
private readonly StateChanged.Source[] _sources = Enumerable
.Repeat(StateChanged.Source.Game, EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices + 5).ToArray();
.Repeat(StateChanged.Source.Game,
EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices + 5 + CrestExtensions.AllRelevantSet.Count).ToArray();
internal ActorState(ActorIdentifier identifier)
=> Identifier = identifier.CreatePermanent();
@ -88,6 +89,9 @@ public class ActorState
public ref StateChanged.Source this[EquipSlot slot, bool stain]
=> ref _sources[slot.ToIndex() + (stain ? EquipFlagExtensions.NumEquipFlags / 2 : 0)];
public ref StateChanged.Source this[CrestFlag slot]
=> ref _sources[EquipFlagExtensions.NumEquipFlags + CustomizationExtensions.NumIndices + 5 + slot.ToInternalIndex()];
public ref StateChanged.Source this[CustomizeIndex type]
=> ref _sources[EquipFlagExtensions.NumEquipFlags + (int)type];

View file

@ -1,13 +1,12 @@
using System.Linq;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization;
using Glamourer.Events;
using Glamourer.Interop;
using Glamourer.Interop.Penumbra;
using Glamourer.Interop.Structs;
using Glamourer.Services;
using Glamourer.Structs;
using Penumbra.Api.Enums;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -17,30 +16,9 @@ namespace Glamourer.State;
/// 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 StateApplier
public class StateApplier(UpdateSlotService _updateSlot, VisorService _visor, WeaponService _weapon, ChangeCustomizeService _changeCustomize,
ItemManager _items, PenumbraService _penumbra, MetaService _metaService, ObjectManager _objects, CrestService _crests)
{
private readonly PenumbraService _penumbra;
private readonly UpdateSlotService _updateSlot;
private readonly VisorService _visor;
private readonly WeaponService _weapon;
private readonly MetaService _metaService;
private readonly ChangeCustomizeService _changeCustomize;
private readonly ItemManager _items;
private readonly ObjectManager _objects;
public StateApplier(UpdateSlotService updateSlot, VisorService visor, WeaponService weapon, ChangeCustomizeService changeCustomize,
ItemManager items, PenumbraService penumbra, MetaService metaService, ObjectManager objects)
{
_updateSlot = updateSlot;
_visor = visor;
_weapon = weapon;
_changeCustomize = changeCustomize;
_items = items;
_penumbra = penumbra;
_metaService = metaService;
_objects = objects;
}
/// <summary> Simply force a redraw regardless of conditions. </summary>
public void ForceRedraw(ActorData data)
{
@ -279,6 +257,22 @@ public class StateApplier
return data;
}
/// <summary> Change the crest state on actors. </summary>
public void ChangeCrests(ActorData data, CrestFlag flags)
{
foreach (var actor in data.Objects.Where(a => a.IsCharacter))
_crests.UpdateCrests(actor, flags);
}
/// <inheritdoc cref="ChangeCrests(ActorData, CrestFlag)"/>
public ActorData ChangeCrests(ActorState state, bool apply)
{
var data = GetData(state);
if (apply)
ChangeCrests(data, state.ModelData.CrestVisibility);
return data;
}
private ActorData GetData(ActorState state)
{
_objects.Update();

View file

@ -4,6 +4,7 @@ using Dalamud.Plugin.Services;
using Glamourer.Customization;
using Glamourer.Events;
using Glamourer.Services;
using Glamourer.Structs;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -197,6 +198,18 @@ public class StateEditor
return true;
}
/// <summary> Change the crest of an equipment piece. </summary>
public bool ChangeCrest(ActorState state, CrestFlag slot, bool crest, StateChanged.Source source, out bool oldCrest, uint key = 0)
{
oldCrest = state.ModelData.Crest(slot);
if (!state.CanUnlock(key))
return false;
state.ModelData.SetCrest(slot, crest);
state[slot] = source;
return true;
}
public bool ChangeMetaState(ActorState state, ActorState.MetaIndex index, bool value, StateChanged.Source source, out bool oldValue,
uint key = 0)
{

View file

@ -13,6 +13,7 @@ using Penumbra.GameData.Structs;
using System;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using Glamourer.Structs;
namespace Glamourer.State;
@ -42,6 +43,7 @@ public class StateListener : IDisposable
private readonly MovedEquipment _movedEquipment;
private readonly GPoseService _gPose;
private readonly ChangeCustomizeService _changeCustomizeService;
private readonly CrestService _crestService;
private readonly ICondition _condition;
private ActorIdentifier _creatingIdentifier = ActorIdentifier.Invalid;
@ -52,7 +54,7 @@ public class StateListener : IDisposable
SlotUpdating slotUpdating, WeaponLoading weaponLoading, VisorStateChanged visorState, WeaponVisibilityChanged weaponVisibility,
HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier, FunModule funModule, HumanModelList humans,
StateApplier applier, MovedEquipment movedEquipment, ObjectManager objects, GPoseService gPose,
ChangeCustomizeService changeCustomizeService, CustomizationService customizations, ICondition condition)
ChangeCustomizeService changeCustomizeService, CustomizationService customizations, ICondition condition, CrestService crestService)
{
_manager = manager;
_items = items;
@ -74,6 +76,7 @@ public class StateListener : IDisposable
_changeCustomizeService = changeCustomizeService;
_customizations = customizations;
_condition = condition;
_crestService = crestService;
Subscribe();
}
@ -405,6 +408,58 @@ public class StateListener : IDisposable
}
}
private void OnCrestChange(Actor actor, CrestFlag slot, Ref<bool> value)
{
if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart)
return;
if (!actor.Identifier(_actors.AwaitedService, out var identifier)
|| !_manager.TryGetValue(identifier, out var state))
return;
switch (UpdateBaseCrest(actor, state, slot, value.Value))
{
case UpdateState.Change:
if (state[slot] is not StateChanged.Source.Fixed and not StateChanged.Source.Ipc)
_manager.ChangeCrest(state, slot, state.BaseData.Crest(slot), StateChanged.Source.Game);
else
value.Value = state.ModelData.Crest(slot);
break;
case UpdateState.NoChange:
case UpdateState.HatHack:
value.Value = state.ModelData.Crest(slot);
break;
case UpdateState.Transformed: break;
}
}
private void OnModelCrestSetup(Model model, CrestFlag slot, ref bool value)
{
var actor = _penumbra.GameObjectFromDrawObject(model);
if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart)
return;
if (!actor.Identifier(_actors.AwaitedService, out var identifier)
|| !_manager.TryGetValue(identifier, out var state))
return;
value = state.ModelData.Crest(slot);
}
private static UpdateState UpdateBaseCrest(Actor actor, ActorState state, CrestFlag slot, bool visible)
{
if (actor.IsTransformed)
return UpdateState.Transformed;
if (state.BaseData.Crest(slot) != visible)
{
state.BaseData.SetCrest(slot, visible);
return UpdateState.Change;
}
return UpdateState.NoChange;
}
/// <summary> Update base data for a single changed weapon slot. </summary>
private unsafe UpdateState UpdateBaseData(Actor actor, ActorState state, EquipSlot slot, CharacterWeapon weapon)
{
@ -490,7 +545,7 @@ public class StateListener : IDisposable
private void OnVisorChange(Model model, Ref<bool> value)
{
// Skip updates when in customize update.
if (ChangeCustomizeService.InUpdate.IsValueCreated && ChangeCustomizeService.InUpdate.Value)
if (ChangeCustomizeService.InUpdate.InMethod)
return;
// Find appropriate actor and state.
@ -616,6 +671,8 @@ public class StateListener : IDisposable
_headGearVisibility.Subscribe(OnHeadGearVisibilityChange, HeadGearVisibilityChanged.Priority.StateListener);
_weaponVisibility.Subscribe(OnWeaponVisibilityChange, WeaponVisibilityChanged.Priority.StateListener);
_changeCustomizeService.Subscribe(OnCustomizeChange, ChangeCustomizeService.Priority.StateListener);
_crestService.Subscribe(OnCrestChange, CrestService.Priority.StateListener);
_crestService.ModelCrestSetup += OnModelCrestSetup;
}
private void Unsubscribe()
@ -629,6 +686,8 @@ public class StateListener : IDisposable
_headGearVisibility.Unsubscribe(OnHeadGearVisibilityChange);
_weaponVisibility.Unsubscribe(OnWeaponVisibilityChange);
_changeCustomizeService.Unsubscribe(OnCustomizeChange);
_crestService.Unsubscribe(OnCrestChange);
_crestService.ModelCrestSetup -= OnModelCrestSetup;
}
private void OnCreatedCharacterBase(nint gameObject, string _, nint drawObject)
@ -639,8 +698,9 @@ public class StateListener : IDisposable
if (_creatingState == null)
return;
_applier.ChangeHatState(new ActorData(gameObject, _creatingIdentifier.ToName()), _creatingState.ModelData.IsHatVisible());
_applier.ChangeWeaponState(new ActorData(gameObject, _creatingIdentifier.ToName()), _creatingState.ModelData.IsWeaponVisible());
_applier.ChangeWetness(new ActorData(gameObject, _creatingIdentifier.ToName()), _creatingState.ModelData.IsWet());
var data = new ActorData(gameObject, _creatingIdentifier.ToName());
_applier.ChangeHatState(data, _creatingState.ModelData.IsHatVisible());
_applier.ChangeWeaponState(data, _creatingState.ModelData.IsWeaponVisible());
_applier.ChangeWetness(data, _creatingState.ModelData.IsWet());
}
}

View file

@ -10,6 +10,7 @@ using Glamourer.Events;
using Glamourer.Interop;
using Glamourer.Interop.Structs;
using Glamourer.Services;
using Glamourer.Structs;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
@ -17,32 +18,12 @@ using Penumbra.GameData.Structs;
namespace Glamourer.State;
public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
public class StateManager(ActorService _actors, ItemManager _items, StateChanged _event, StateApplier _applier, StateEditor _editor,
HumanModelList _humans, ICondition _condition, IClientState _clientState)
: IReadOnlyDictionary<ActorIdentifier, ActorState>
{
private readonly ActorService _actors;
private readonly ItemManager _items;
private readonly HumanModelList _humans;
private readonly StateChanged _event;
private readonly StateApplier _applier;
private readonly StateEditor _editor;
private readonly ICondition _condition;
private readonly IClientState _clientState;
private readonly Dictionary<ActorIdentifier, ActorState> _states = new();
public StateManager(ActorService actors, ItemManager items, StateChanged @event, StateApplier applier, StateEditor editor,
HumanModelList humans, ICondition condition, IClientState clientState)
{
_actors = actors;
_items = items;
_event = @event;
_applier = applier;
_editor = editor;
_humans = humans;
_condition = condition;
_clientState = clientState;
}
public IEnumerator<KeyValuePair<ActorIdentifier, ActorState>> GetEnumerator()
=> _states.GetEnumerator();
@ -83,7 +64,7 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
// and the draw objects data for the model data (where possible).
state = new ActorState(identifier)
{
ModelData = FromActor(actor, true, false),
ModelData = FromActor(actor, true, false),
BaseData = FromActor(actor, false, false),
LastJob = (byte)(actor.IsCharacter ? actor.AsCharacter->CharacterData.ClassJob : 0),
LastTerritory = _clientState.TerritoryType,
@ -162,6 +143,9 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
// Visor state is a flag on the game object, but we can see the actual state on the draw object.
ret.SetVisor(VisorService.GetVisorState(model));
foreach (var slot in CrestExtensions.AllRelevantSet)
ret.SetCrest(slot, CrestService.GetModelCrest(actor, slot));
}
else
{
@ -180,6 +164,9 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
off = actor.GetOffhand();
FistWeaponHack(ref ret, ref main, ref off);
ret.SetVisor(actor.AsCharacter->DrawData.IsVisorToggled);
foreach (var slot in CrestExtensions.AllRelevantSet)
ret.SetCrest(slot, actor.GetCrest(slot));
}
// Set the weapons regardless of source.
@ -206,7 +193,7 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
if (mainhand.Set.Id is < 1601 or >= 1651)
return;
var gauntlets = _items.Identify(EquipSlot.Hands, offhand.Set, (Variant) offhand.Type.Id);
var gauntlets = _items.Identify(EquipSlot.Hands, offhand.Set, (Variant)offhand.Type.Id);
offhand.Set = (SetId)(mainhand.Set.Id + 50);
offhand.Variant = mainhand.Variant;
offhand.Type = mainhand.Type;
@ -304,6 +291,18 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
_event.Invoke(StateChanged.Type.Stain, source, state, actors, (old, stain, slot));
}
/// <summary> Change the crest of an equipment piece. </summary>
public void ChangeCrest(ActorState state, CrestFlag slot, bool crest, StateChanged.Source source, uint key = 0)
{
if (!_editor.ChangeCrest(state, slot, crest, source, out var old, key))
return;
var actors = _applier.ChangeCrests(state, source is StateChanged.Source.Manual or StateChanged.Source.Ipc);
Glamourer.Log.Verbose(
$"Set {slot.ToLabel()} crest in state {state.Identifier.Incognito(null)} from {old} to {crest}. [Affecting {actors.ToLazyString("nothing")}.]");
_event.Invoke(StateChanged.Type.Crest, source, state, actors, (old, crest, slot));
}
/// <summary> Change hat visibility. </summary>
public void ChangeHatState(ActorState state, bool value, StateChanged.Source source, uint key = 0)
{
@ -356,19 +355,8 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
public void ApplyDesign(DesignBase design, ActorState state, StateChanged.Source source, uint key = 0)
{
void HandleEquip(EquipSlot slot, bool applyPiece, bool applyStain)
{
var unused = (applyPiece, applyStain) switch
{
(false, false) => false,
(true, false) => _editor.ChangeItem(state, slot, design.DesignData.Item(slot), source, out _, key),
(false, true) => _editor.ChangeStain(state, slot, design.DesignData.Stain(slot), source, out _, key),
(true, true) => _editor.ChangeEquip(state, slot, design.DesignData.Item(slot), design.DesignData.Stain(slot), source, out _,
out _, key),
};
}
if (!_editor.ChangeModelId(state, design.DesignData.ModelId, design.DesignData.Customize, design.GetDesignDataRef().GetEquipmentPtr(), source,
if (!_editor.ChangeModelId(state, design.DesignData.ModelId, design.DesignData.Customize, design.GetDesignDataRef().GetEquipmentPtr(),
source,
out var oldModelId, key))
return;
@ -393,12 +381,28 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
foreach (var slot in EquipSlotExtensions.FullSlots)
HandleEquip(slot, design.DoApplyEquip(slot), design.DoApplyStain(slot));
foreach (var slot in CrestExtensions.AllRelevantSet.Where(design.DoApplyCrest))
_editor.ChangeCrest(state, slot, design.DesignData.Crest(slot), source, out _, key);
}
var actors = ApplyAll(state, redraw, false);
Glamourer.Log.Verbose(
$"Applied design to {state.Identifier.Incognito(null)}. [Affecting {actors.ToLazyString("nothing")}.]");
_event.Invoke(StateChanged.Type.Design, state[ActorState.MetaIndex.Wetness], state, actors, design);
return;
void HandleEquip(EquipSlot slot, bool applyPiece, bool applyStain)
{
var unused = (applyPiece, applyStain) switch
{
(false, false) => false,
(true, false) => _editor.ChangeItem(state, slot, design.DesignData.Item(slot), source, out _, key),
(false, true) => _editor.ChangeStain(state, slot, design.DesignData.Stain(slot), source, out _, key),
(true, true) => _editor.ChangeEquip(state, slot, design.DesignData.Item(slot), design.DesignData.Stain(slot), source, out _,
out _, key),
};
}
}
private ActorData ApplyAll(ActorState state, bool redraw, bool withLock)
@ -430,6 +434,7 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
_applier.ChangeHatState(actors, state.ModelData.IsHatVisible());
_applier.ChangeWeaponState(actors, state.ModelData.IsWeaponVisible());
_applier.ChangeVisor(actors, state.ModelData.IsVisorToggled());
_applier.ChangeCrests(actors, state.ModelData.CrestVisibility);
}
return actors;
@ -457,6 +462,9 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
foreach (var type in Enum.GetValues<ActorState.MetaIndex>())
state[type] = StateChanged.Source.Game;
foreach (var slot in CrestExtensions.AllRelevantSet)
state[slot] = StateChanged.Source.Game;
var actors = ActorData.Invalid;
if (source is StateChanged.Source.Manual or StateChanged.Source.Ipc)
actors = ApplyAll(state, redraw, true);
@ -491,6 +499,15 @@ public class StateManager : IReadOnlyDictionary<ActorIdentifier, ActorState>
}
}
foreach (var slot in CrestExtensions.AllRelevantSet)
{
if (state[slot] is StateChanged.Source.Fixed)
{
state[slot] = StateChanged.Source.Game;
state.ModelData.SetCrest(slot, state.BaseData.Crest(slot));
}
}
if (state[ActorState.MetaIndex.HatState] is StateChanged.Source.Fixed)
{
state[ActorState.MetaIndex.HatState] = StateChanged.Source.Game;

@ -1 +1 @@
Subproject commit f55733a96fdc9f82c9bbf8272ca6366079aa8e32
Subproject commit f624aca526bd1f36364d63443ed1c6e83499b8b7