diff --git a/Glamourer.Api b/Glamourer.Api
index 4aaece3..ca00339 160000
--- a/Glamourer.Api
+++ b/Glamourer.Api
@@ -1 +1 @@
-Subproject commit 4aaece34289d64363bc32aaa8fe52c8e7d3dce32
+Subproject commit ca003395306791b9e595683c47824b4718385311
diff --git a/Glamourer/Api/ApiHelpers.cs b/Glamourer/Api/ApiHelpers.cs
index cf67912..a54a0ec 100644
--- a/Glamourer/Api/ApiHelpers.cs
+++ b/Glamourer/Api/ApiHelpers.cs
@@ -35,7 +35,8 @@ public class ApiHelpers(ObjectManager objects, StateManager stateManager, ActorM
state = null;
return GlamourerApiEc.ActorNotFound;
}
- stateManager.TryGetValue(identifier, out state);
+
+ stateManager.TryGetValue(identifier, out state);
return GlamourerApiEc.Success;
}
@@ -54,12 +55,10 @@ public class ApiHelpers(ObjectManager objects, StateManager stateManager, ActorM
internal static DesignBase.FlagRestrictionResetter Restrict(DesignBase design, ApplyFlag flags)
=> (flags & (ApplyFlag.Equipment | ApplyFlag.Customization)) switch
{
- ApplyFlag.Equipment => design.TemporarilyRestrictApplication(EquipFlagExtensions.All, 0, CrestExtensions.All, 0),
- ApplyFlag.Customization => design.TemporarilyRestrictApplication(0, CustomizeFlagExtensions.All, 0,
- CustomizeParameterExtensions.All),
- ApplyFlag.Equipment | ApplyFlag.Customization => design.TemporarilyRestrictApplication(EquipFlagExtensions.All,
- CustomizeFlagExtensions.All, CrestExtensions.All, CustomizeParameterExtensions.All),
- _ => design.TemporarilyRestrictApplication(0, 0, 0, 0),
+ ApplyFlag.Equipment => design.TemporarilyRestrictApplication(ApplicationCollection.Equipment),
+ ApplyFlag.Customization => design.TemporarilyRestrictApplication(ApplicationCollection.Customizations),
+ ApplyFlag.Equipment | ApplyFlag.Customization => design.TemporarilyRestrictApplication(ApplicationCollection.All),
+ _ => design.TemporarilyRestrictApplication(ApplicationCollection.None),
};
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
diff --git a/Glamourer/Automation/ApplicationType.cs b/Glamourer/Automation/ApplicationType.cs
index 12dac50..3d409cb 100644
--- a/Glamourer/Automation/ApplicationType.cs
+++ b/Glamourer/Automation/ApplicationType.cs
@@ -28,8 +28,7 @@ public static class ApplicationTypeExtensions
(ApplicationType.Weapons, "Apply all weapon changes that are enabled in this design and that are valid with the current weapon worn."),
];
- public static (EquipFlag Equip, CustomizeFlag Customize, CrestFlag Crest, CustomizeParameterFlag Parameters, MetaFlag Meta) ApplyWhat(
- this ApplicationType type, IDesignStandIn designStandIn)
+ public static ApplicationCollection Collection(this ApplicationType type)
{
var equipFlags = (type.HasFlag(ApplicationType.Weapons) ? WeaponFlags : 0)
| (type.HasFlag(ApplicationType.Armor) ? ArmorFlags : 0)
@@ -37,18 +36,18 @@ public static class ApplicationTypeExtensions
| (type.HasFlag(ApplicationType.GearCustomization) ? StainFlags : 0);
var customizeFlags = type.HasFlag(ApplicationType.Customizations) ? CustomizeFlagExtensions.All : 0;
var parameterFlags = type.HasFlag(ApplicationType.Customizations) ? CustomizeParameterExtensions.All : 0;
- var crestFlag = type.HasFlag(ApplicationType.GearCustomization) ? CrestExtensions.AllRelevant : 0;
- var metaFlag = (type.HasFlag(ApplicationType.Armor) ? MetaFlag.HatState | MetaFlag.VisorState : 0)
+ var crestFlags = type.HasFlag(ApplicationType.GearCustomization) ? CrestExtensions.AllRelevant : 0;
+ var metaFlags = (type.HasFlag(ApplicationType.Armor) ? MetaFlag.HatState | MetaFlag.VisorState : 0)
| (type.HasFlag(ApplicationType.Weapons) ? MetaFlag.WeaponState : 0)
| (type.HasFlag(ApplicationType.Customizations) ? MetaFlag.Wetness : 0);
+ var bonusFlags = type.HasFlag(ApplicationType.Armor) ? BonusExtensions.All : 0;
- if (designStandIn is not DesignBase design)
- return (equipFlags, customizeFlags, crestFlag, parameterFlags, metaFlag);
-
- return (equipFlags & design!.ApplyEquip, customizeFlags & design.ApplyCustomize, crestFlag & design.ApplyCrest,
- parameterFlags & design.ApplyParameters, metaFlag & design.ApplyMeta);
+ return new ApplicationCollection(equipFlags, bonusFlags, customizeFlags, crestFlags, parameterFlags, metaFlags);
}
+ public static ApplicationCollection ApplyWhat(this ApplicationType type, IDesignStandIn designStandIn)
+ => designStandIn is not DesignBase design ? type.Collection() : type.Collection().Restrict(design.Application);
+
public const EquipFlag WeaponFlags = EquipFlag.Mainhand | EquipFlag.Offhand;
public const EquipFlag ArmorFlags = EquipFlag.Head | EquipFlag.Body | EquipFlag.Hands | EquipFlag.Legs | EquipFlag.Feet;
public const EquipFlag AccessoryFlags = EquipFlag.Ears | EquipFlag.Neck | EquipFlag.Wrist | EquipFlag.RFinger | EquipFlag.LFinger;
diff --git a/Glamourer/Automation/AutoDesign.cs b/Glamourer/Automation/AutoDesign.cs
index 9fc8ca7..e31fb16 100644
--- a/Glamourer/Automation/AutoDesign.cs
+++ b/Glamourer/Automation/AutoDesign.cs
@@ -61,6 +61,6 @@ public class AutoDesign
return ret;
}
- public (EquipFlag Equip, CustomizeFlag Customize, CrestFlag Crest, CustomizeParameterFlag Parameters, MetaFlag Meta) ApplyWhat()
+ public ApplicationCollection ApplyWhat()
=> Type.ApplyWhat(Design);
}
diff --git a/Glamourer/Designs/ApplicationCollection.cs b/Glamourer/Designs/ApplicationCollection.cs
new file mode 100644
index 0000000..b31ff2e
--- /dev/null
+++ b/Glamourer/Designs/ApplicationCollection.cs
@@ -0,0 +1,61 @@
+using Glamourer.GameData;
+using ImGuiNET;
+using Penumbra.GameData.Enums;
+
+namespace Glamourer.Designs;
+
+public record struct ApplicationCollection(
+ EquipFlag Equip,
+ BonusItemFlag BonusItem,
+ CustomizeFlag Customize,
+ CrestFlag Crest,
+ CustomizeParameterFlag Parameters,
+ MetaFlag Meta)
+{
+ public static readonly ApplicationCollection All = new(EquipFlagExtensions.All, BonusExtensions.All,
+ CustomizeFlagExtensions.AllRelevant, CrestExtensions.AllRelevant, CustomizeParameterExtensions.All, MetaExtensions.All);
+
+ public static readonly ApplicationCollection None = new(0, 0, 0, 0, 0, 0);
+
+ public static readonly ApplicationCollection Equipment = new(EquipFlagExtensions.All, BonusExtensions.All,
+ 0, CrestExtensions.AllRelevant, 0, MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState);
+
+ public static readonly ApplicationCollection Customizations = new(0, 0, CustomizeFlagExtensions.AllRelevant, 0,
+ CustomizeParameterExtensions.All, MetaFlag.Wetness);
+
+ public static readonly ApplicationCollection Default = new(EquipFlagExtensions.All, BonusExtensions.All,
+ CustomizeFlagExtensions.AllRelevant, CrestExtensions.AllRelevant, 0, MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState);
+
+ public static ApplicationCollection FromKeys()
+ => (ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift) switch
+ {
+ (false, false) => All,
+ (true, true) => All,
+ (true, false) => Equipment,
+ (false, true) => Customizations,
+ };
+
+ public void RemoveEquip()
+ {
+ Equip = 0;
+ BonusItem = 0;
+ Crest = 0;
+ Meta &= ~(MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState);
+ }
+
+ public void RemoveCustomize()
+ {
+ Customize = 0;
+ Parameters = 0;
+ Meta &= MetaFlag.Wetness;
+ }
+
+ public ApplicationCollection Restrict(ApplicationCollection old)
+ => new(old.Equip & Equip, old.BonusItem & BonusItem, old.Customize & Customize, old.Crest & Crest,
+ old.Parameters & Parameters, old.Meta & Meta);
+
+ public ApplicationCollection CloneSecure()
+ => new(Equip & EquipFlagExtensions.All, BonusItem & BonusExtensions.All,
+ (Customize & CustomizeFlagExtensions.AllRelevant) | CustomizeFlag.BodyType, Crest & CrestExtensions.AllRelevant,
+ Parameters & CustomizeParameterExtensions.All, Meta & MetaExtensions.All);
+}
diff --git a/Glamourer/Designs/ApplicationRules.cs b/Glamourer/Designs/ApplicationRules.cs
index c15b26a..3c5fed2 100644
--- a/Glamourer/Designs/ApplicationRules.cs
+++ b/Glamourer/Designs/ApplicationRules.cs
@@ -5,16 +5,9 @@ using Penumbra.GameData.Enums;
namespace Glamourer.Designs;
-public readonly struct ApplicationRules(
- EquipFlag equip,
- CustomizeFlag customize,
- CrestFlag crest,
- CustomizeParameterFlag parameters,
- MetaFlag meta,
- bool materials)
+public readonly struct ApplicationRules(ApplicationCollection application, bool materials)
{
- public static readonly ApplicationRules All = new(EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant,
- CrestExtensions.AllRelevant, CustomizeParameterExtensions.All, MetaExtensions.All, true);
+ public static readonly ApplicationRules All = new(ApplicationCollection.All, true);
public static ApplicationRules FromModifiers(ActorState state)
=> FromModifiers(state, ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift);
@@ -23,54 +16,43 @@ public readonly struct ApplicationRules(
=> NpcFromModifiers(ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift);
public static ApplicationRules AllButParameters(ActorState state)
- => new(All.Equip, All.Customize, All.Crest, ComputeParameters(state.ModelData, state.BaseData, All.Parameters), All.Meta, true);
+ => new(ApplicationCollection.All with { Parameters = ComputeParameters(state.ModelData, state.BaseData, All.Parameters) }, true);
public static ApplicationRules AllWithConfig(Configuration config)
- => new(All.Equip, All.Customize, All.Crest, config.UseAdvancedParameters ? All.Parameters : 0, All.Meta, config.UseAdvancedDyes);
+ => new(ApplicationCollection.All with { Parameters = config.UseAdvancedParameters ? All.Parameters : 0 }, config.UseAdvancedDyes);
public static ApplicationRules NpcFromModifiers(bool ctrl, bool shift)
- => new(ctrl || !shift ? EquipFlagExtensions.All : 0,
- !ctrl || shift ? CustomizeFlagExtensions.AllRelevant : 0,
- 0,
- 0,
- ctrl || !shift ? MetaFlag.VisorState : 0, false);
+ {
+ var equip = ctrl || !shift ? EquipFlagExtensions.All : 0;
+ var customize = !ctrl || shift ? CustomizeFlagExtensions.AllRelevant : 0;
+ var visor = equip != 0 ? MetaFlag.VisorState : 0;
+ return new ApplicationRules(new ApplicationCollection(equip, 0, customize, 0, 0, visor), false);
+ }
public static ApplicationRules FromModifiers(ActorState state, bool ctrl, bool shift)
{
var equip = ctrl || !shift ? EquipFlagExtensions.All : 0;
var customize = !ctrl || shift ? CustomizeFlagExtensions.AllRelevant : 0;
+ var bonus = equip == 0 ? 0 : BonusExtensions.All;
var crest = equip == 0 ? 0 : CrestExtensions.AllRelevant;
var parameters = customize == 0 ? 0 : CustomizeParameterExtensions.All;
var meta = state.ModelData.IsWet() ? MetaFlag.Wetness : 0;
if (equip != 0)
meta |= MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState;
- return new ApplicationRules(equip, customize, crest, ComputeParameters(state.ModelData, state.BaseData, parameters), meta, equip != 0);
+ var collection = new ApplicationCollection(equip, bonus, customize, crest,
+ ComputeParameters(state.ModelData, state.BaseData, parameters), meta);
+ return new ApplicationRules(collection, equip != 0);
}
public void Apply(DesignBase design)
- {
- design.ApplyEquip = Equip;
- design.ApplyCustomize = Customize;
- design.ApplyCrest = Crest;
- design.ApplyParameters = Parameters;
- design.ApplyMeta = Meta;
- }
+ => design.Application = application;
public EquipFlag Equip
- => equip & EquipFlagExtensions.All;
-
- public CustomizeFlag Customize
- => customize & CustomizeFlagExtensions.AllRelevant;
-
- public CrestFlag Crest
- => crest & CrestExtensions.AllRelevant;
+ => application.Equip & EquipFlagExtensions.All;
public CustomizeParameterFlag Parameters
- => parameters & CustomizeParameterExtensions.All;
-
- public MetaFlag Meta
- => meta & MetaExtensions.All;
+ => application.Parameters & CustomizeParameterExtensions.All;
public bool Materials
=> materials;
diff --git a/Glamourer/Designs/DesignBase.cs b/Glamourer/Designs/DesignBase.cs
index 3791442..be12737 100644
--- a/Glamourer/Designs/DesignBase.cs
+++ b/Glamourer/Designs/DesignBase.cs
@@ -42,23 +42,19 @@ public class DesignBase
/// Used when importing .cma or .chara files.
internal DesignBase(CustomizeService customize, in DesignData designData, EquipFlag equipFlags, CustomizeFlag customizeFlags)
{
- _designData = designData;
- ApplyCustomize = customizeFlags & CustomizeFlagExtensions.AllRelevant;
- ApplyEquip = equipFlags & EquipFlagExtensions.All;
- ApplyMeta = 0;
- CustomizeSet = SetCustomizationSet(customize);
+ _designData = designData;
+ ApplyCustomize = customizeFlags & CustomizeFlagExtensions.AllRelevant;
+ Application.Equip = equipFlags & EquipFlagExtensions.All;
+ Application.Meta = 0;
+ CustomizeSet = SetCustomizationSet(customize);
}
internal DesignBase(DesignBase clone)
{
- _designData = clone._designData;
- _materials = clone._materials.Clone();
- CustomizeSet = clone.CustomizeSet;
- ApplyCustomize = clone.ApplyCustomizeRaw;
- ApplyEquip = clone.ApplyEquip & EquipFlagExtensions.All;
- ApplyParameters = clone.ApplyParameters & CustomizeParameterExtensions.All;
- ApplyCrest = clone.ApplyCrest & CrestExtensions.All;
- ApplyMeta = clone.ApplyMeta & MetaExtensions.All;
+ _designData = clone._designData;
+ _materials = clone._materials.Clone();
+ CustomizeSet = clone.CustomizeSet;
+ Application = clone.Application.CloneSecure();
}
/// Ensure that the customization set is updated when the design data changes.
@@ -70,27 +66,20 @@ public class DesignBase
#region Application Data
- private CustomizeFlag _applyCustomize = CustomizeFlagExtensions.AllRelevant;
- public CustomizeSet CustomizeSet { get; private set; }
+ public CustomizeSet CustomizeSet { get; private set; }
- public CustomizeParameterFlag ApplyParameters { get; internal set; }
+ public ApplicationCollection Application = ApplicationCollection.Default;
internal CustomizeFlag ApplyCustomize
{
- get => _applyCustomize.FixApplication(CustomizeSet);
- set => _applyCustomize = (value & CustomizeFlagExtensions.AllRelevant) | CustomizeFlag.BodyType;
+ get => Application.Customize.FixApplication(CustomizeSet);
+ set => Application.Customize = (value & CustomizeFlagExtensions.AllRelevant) | CustomizeFlag.BodyType;
}
internal CustomizeFlag ApplyCustomizeExcludingBodyType
- => _applyCustomize.FixApplication(CustomizeSet) & ~CustomizeFlag.BodyType;
+ => Application.Customize.FixApplication(CustomizeSet) & ~CustomizeFlag.BodyType;
- internal CustomizeFlag ApplyCustomizeRaw
- => _applyCustomize;
-
- internal EquipFlag ApplyEquip = EquipFlagExtensions.All;
- internal CrestFlag ApplyCrest = CrestExtensions.AllRelevant;
- internal MetaFlag ApplyMeta = MetaFlag.HatState | MetaFlag.VisorState | MetaFlag.WeaponState;
- private bool _writeProtected;
+ private bool _writeProtected;
public bool SetCustomize(CustomizeService customizeService, CustomizeArray customize)
{
@@ -103,18 +92,18 @@ public class DesignBase
}
public bool DoApplyMeta(MetaIndex index)
- => ApplyMeta.HasFlag(index.ToFlag());
+ => Application.Meta.HasFlag(index.ToFlag());
public bool WriteProtected()
=> _writeProtected;
public bool SetApplyMeta(MetaIndex index, bool value)
{
- var newFlag = value ? ApplyMeta | index.ToFlag() : ApplyMeta & ~index.ToFlag();
- if (newFlag == ApplyMeta)
+ var newFlag = value ? Application.Meta | index.ToFlag() : Application.Meta & ~index.ToFlag();
+ if (newFlag == Application.Meta)
return false;
- ApplyMeta = newFlag;
+ Application.Meta = newFlag;
return true;
}
@@ -128,103 +117,100 @@ public class DesignBase
}
public bool DoApplyEquip(EquipSlot slot)
- => ApplyEquip.HasFlag(slot.ToFlag());
+ => Application.Equip.HasFlag(slot.ToFlag());
public bool DoApplyStain(EquipSlot slot)
- => ApplyEquip.HasFlag(slot.ToStainFlag());
+ => Application.Equip.HasFlag(slot.ToStainFlag());
public bool DoApplyCustomize(CustomizeIndex idx)
- => ApplyCustomize.HasFlag(idx.ToFlag());
+ => Application.Customize.HasFlag(idx.ToFlag());
public bool DoApplyCrest(CrestFlag slot)
- => ApplyCrest.HasFlag(slot);
+ => Application.Crest.HasFlag(slot);
public bool DoApplyParameter(CustomizeParameterFlag flag)
- => ApplyParameters.HasFlag(flag);
+ => Application.Parameters.HasFlag(flag);
+
+ public bool DoApplyBonusItem(BonusItemFlag slot)
+ => Application.BonusItem.HasFlag(slot);
internal bool SetApplyEquip(EquipSlot slot, bool value)
{
- var newValue = value ? ApplyEquip | slot.ToFlag() : ApplyEquip & ~slot.ToFlag();
- if (newValue == ApplyEquip)
+ var newValue = value ? Application.Equip | slot.ToFlag() : Application.Equip & ~slot.ToFlag();
+ if (newValue == Application.Equip)
return false;
- ApplyEquip = newValue;
+ Application.Equip = newValue;
+ return true;
+ }
+
+ internal bool SetApplyBonusItem(BonusItemFlag slot, bool value)
+ {
+ var newValue = value ? Application.BonusItem | slot : Application.BonusItem & ~slot;
+ if (newValue == Application.BonusItem)
+ return false;
+
+ Application.BonusItem = newValue;
return true;
}
internal bool SetApplyStain(EquipSlot slot, bool value)
{
- var newValue = value ? ApplyEquip | slot.ToStainFlag() : ApplyEquip & ~slot.ToStainFlag();
- if (newValue == ApplyEquip)
+ var newValue = value ? Application.Equip | slot.ToStainFlag() : Application.Equip & ~slot.ToStainFlag();
+ if (newValue == Application.Equip)
return false;
- ApplyEquip = newValue;
+ Application.Equip = newValue;
return true;
}
internal bool SetApplyCustomize(CustomizeIndex idx, bool value)
{
- var newValue = value ? _applyCustomize | idx.ToFlag() : _applyCustomize & ~idx.ToFlag();
- if (newValue == _applyCustomize)
+ var newValue = value ? Application.Customize | idx.ToFlag() : Application.Customize & ~idx.ToFlag();
+ if (newValue == Application.Customize)
return false;
- _applyCustomize = newValue;
+ Application.Customize = newValue;
return true;
}
internal bool SetApplyCrest(CrestFlag slot, bool value)
{
- var newValue = value ? ApplyCrest | slot : ApplyCrest & ~slot;
- if (newValue == ApplyCrest)
+ var newValue = value ? Application.Crest | slot : Application.Crest & ~slot;
+ if (newValue == Application.Crest)
return false;
- ApplyCrest = newValue;
+ Application.Crest = newValue;
return true;
}
internal bool SetApplyParameter(CustomizeParameterFlag flag, bool value)
{
- var newValue = value ? ApplyParameters | flag : ApplyParameters & ~flag;
- if (newValue == ApplyParameters)
+ var newValue = value ? Application.Parameters | flag : Application.Parameters & ~flag;
+ if (newValue == Application.Parameters)
return false;
- ApplyParameters = newValue;
+ Application.Parameters = newValue;
return true;
}
- internal FlagRestrictionResetter TemporarilyRestrictApplication(EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags,
- CustomizeParameterFlag parameterFlags)
- => new(this, equipFlags, customizeFlags, crestFlags, parameterFlags);
+ internal FlagRestrictionResetter TemporarilyRestrictApplication(ApplicationCollection restrictions)
+ => new(this, restrictions);
internal readonly struct FlagRestrictionResetter : IDisposable
{
- private readonly DesignBase _design;
- private readonly EquipFlag _oldEquipFlags;
- private readonly CustomizeFlag _oldCustomizeFlags;
- private readonly CrestFlag _oldCrestFlags;
- private readonly CustomizeParameterFlag _oldParameterFlags;
+ private readonly DesignBase _design;
+ private readonly ApplicationCollection _oldFlags;
- public FlagRestrictionResetter(DesignBase d, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags,
- CustomizeParameterFlag parameterFlags)
+ public FlagRestrictionResetter(DesignBase d, ApplicationCollection restrictions)
{
- _design = d;
- _oldEquipFlags = d.ApplyEquip;
- _oldCustomizeFlags = d.ApplyCustomizeRaw;
- _oldCrestFlags = d.ApplyCrest;
- _oldParameterFlags = d.ApplyParameters;
- d.ApplyEquip &= equipFlags;
- d.ApplyCustomize &= customizeFlags;
- d.ApplyCrest &= crestFlags;
- d.ApplyParameters &= parameterFlags;
+ _design = d;
+ _oldFlags = d.Application;
+ _design.Application = restrictions.Restrict(_oldFlags);
}
public void Dispose()
- {
- _design.ApplyEquip = _oldEquipFlags;
- _design.ApplyCustomize = _oldCustomizeFlags;
- _design.ApplyCrest = _oldCrestFlags;
- _design.ApplyParameters = _oldParameterFlags;
- }
+ => _design.Application = _oldFlags;
}
private CustomizeSet SetCustomizationSet(CustomizeService customize)
@@ -285,6 +271,22 @@ public class DesignBase
});
}
+ protected JObject SerializeBonusItems()
+ {
+ var ret = new JObject();
+ foreach (var slot in BonusExtensions.AllFlags)
+ {
+ var item = _designData.BonusItem(slot);
+ ret[slot.ToString()] = new JObject()
+ {
+ ["BonusId"] = item.ModelId.Id,
+ ["Apply"] = DoApplyBonusItem(slot),
+ };
+ }
+
+ return ret;
+ }
+
protected JObject SerializeCustomize()
{
var ret = new JObject()
@@ -299,7 +301,7 @@ public class DesignBase
ret[idx.ToString()] = new JObject()
{
["Value"] = customize[idx].Value,
- ["Apply"] = ApplyCustomizeRaw.HasFlag(idx.ToFlag()),
+ ["Apply"] = Application.Customize.HasFlag(idx.ToFlag()),
};
}
else
@@ -382,7 +384,7 @@ public class DesignBase
{
var k = uint.Parse(key.Name, NumberStyles.HexNumber);
var v = value.ToObject();
- if (!MaterialValueIndex.FromKey(k, out var idx))
+ if (!MaterialValueIndex.FromKey(k, out _))
{
Glamourer.Messager.NotificationMessage($"Invalid material value key {k} for design {name}, skipped.",
NotificationType.Warning);
@@ -429,7 +431,7 @@ public class DesignBase
{
if (parameters == null)
{
- design.ApplyParameters = 0;
+ design.Application.Parameters = 0;
design.GetDesignDataRef().Parameters = default;
return;
}
@@ -490,7 +492,7 @@ public class DesignBase
return true;
}
- design.ApplyParameters &= ~flag;
+ design.Application.Parameters &= ~flag;
design.GetDesignDataRef().Parameters[flag] = CustomizeParameterValue.Zero;
return false;
}
@@ -669,11 +671,12 @@ public class DesignBase
{
_designData = DesignBase64Migration.MigrateBase64(items, humans, base64, out var equipFlags, out var customizeFlags,
out var writeProtected, out var applyMeta);
- ApplyEquip = equipFlags;
- ApplyCustomize = customizeFlags;
- ApplyParameters = 0;
- ApplyCrest = 0;
- ApplyMeta = applyMeta;
+ Application.Equip = equipFlags;
+ ApplyCustomize = customizeFlags;
+ Application.Parameters = 0;
+ Application.Crest = 0;
+ Application.Meta = applyMeta;
+ Application.BonusItem = 0;
SetWriteProtected(writeProtected);
CustomizeSet = SetCustomizationSet(customize);
}
diff --git a/Glamourer/Designs/DesignColors.cs b/Glamourer/Designs/DesignColors.cs
index 5577c2c..96592bf 100644
--- a/Glamourer/Designs/DesignColors.cs
+++ b/Glamourer/Designs/DesignColors.cs
@@ -270,7 +270,7 @@ public class DesignColors : ISavable, IReadOnlyDictionary
public static uint AutoColor(DesignBase design)
{
var customize = design.ApplyCustomizeExcludingBodyType == 0;
- var equip = design.ApplyEquip == 0;
+ var equip = design.Application.Equip == 0;
return (customize, equip) switch
{
(true, true) => ColorId.StateDesign.Value(),
diff --git a/Glamourer/Designs/DesignConverter.cs b/Glamourer/Designs/DesignConverter.cs
index b1b5c61..21172fe 100644
--- a/Glamourer/Designs/DesignConverter.cs
+++ b/Glamourer/Designs/DesignConverter.cs
@@ -72,16 +72,11 @@ public class DesignConverter(
? Design.LoadDesign(_customize, _items, _linkLoader, jObject)
: DesignBase.LoadDesignBase(_customize, _items, jObject);
- ret.SetApplyMeta(MetaIndex.Wetness, customize);
if (!customize)
- ret.ApplyCustomize = 0;
+ ret.Application.RemoveCustomize();
if (!equip)
- {
- ret.ApplyEquip = 0;
- ret.ApplyCrest = 0;
- ret.ApplyMeta &= ~(MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState);
- }
+ ret.Application.RemoveEquip();
return ret;
}
@@ -155,16 +150,11 @@ public class DesignConverter(
return null;
}
- ret.SetApplyMeta(MetaIndex.Wetness, customize);
if (!customize)
- ret.ApplyCustomize = 0;
+ ret.Application.RemoveCustomize();
if (!equip)
- {
- ret.ApplyEquip = 0;
- ret.ApplyCrest = 0;
- ret.ApplyMeta &= ~(MetaFlag.HatState | MetaFlag.WeaponState | MetaFlag.VisorState);
- }
+ ret.Application.RemoveEquip();
return ret;
}
diff --git a/Glamourer/Designs/DesignData.cs b/Glamourer/Designs/DesignData.cs
index a762a84..7fb2f72 100644
--- a/Glamourer/Designs/DesignData.cs
+++ b/Glamourer/Designs/DesignData.cs
@@ -9,24 +9,31 @@ namespace Glamourer.Designs;
public unsafe struct DesignData
{
- public const int EquipmentByteSize = 10 * CharacterArmor.Size;
+ public const int NumEquipment = 10;
+ public const int EquipmentByteSize = NumEquipment * CharacterArmor.Size;
+ public const int NumBonusItems = 1;
+ public const int NumWeapons = 2;
- private string _nameHead = string.Empty;
- private string _nameBody = string.Empty;
- private string _nameHands = string.Empty;
- private string _nameLegs = string.Empty;
- private string _nameFeet = string.Empty;
- private string _nameEars = string.Empty;
- private string _nameNeck = string.Empty;
- private string _nameWrists = string.Empty;
- private string _nameRFinger = string.Empty;
- private string _nameLFinger = string.Empty;
- private string _nameMainhand = string.Empty;
- private string _nameOffhand = string.Empty;
- private string _nameFaceWear = string.Empty;
- private fixed uint _itemIds[12];
- private fixed uint _iconIds[12];
- private fixed byte _equipmentBytes[EquipmentByteSize + 16];
+ private string _nameHead = string.Empty;
+ private string _nameBody = string.Empty;
+ private string _nameHands = string.Empty;
+ private string _nameLegs = string.Empty;
+ private string _nameFeet = string.Empty;
+ private string _nameEars = string.Empty;
+ private string _nameNeck = string.Empty;
+ private string _nameWrists = string.Empty;
+ private string _nameRFinger = string.Empty;
+ private string _nameLFinger = string.Empty;
+ private string _nameMainhand = string.Empty;
+ private string _nameOffhand = string.Empty;
+ private string _nameGlasses = string.Empty;
+
+ private fixed uint _itemIds[NumEquipment + NumWeapons];
+ private fixed uint _iconIds[NumEquipment + NumWeapons + NumBonusItems];
+ private fixed byte _equipmentBytes[EquipmentByteSize + NumWeapons * CharacterWeapon.Size];
+ private fixed ushort _bonusIds[NumBonusItems];
+ private fixed ushort _bonusModelIds[NumBonusItems];
+ private fixed byte _bonusVariants[NumBonusItems];
public CustomizeParameterData Parameters;
public CustomizeArray Customize = CustomizeArray.Default;
public uint ModelId;
@@ -52,7 +59,7 @@ public unsafe struct DesignData
|| name.IsContained(_nameLFinger)
|| name.IsContained(_nameMainhand)
|| name.IsContained(_nameOffhand)
- || name.IsContained(_nameFaceWear);
+ || name.IsContained(_nameGlasses);
public readonly StainIds Stain(EquipSlot slot)
{
@@ -101,6 +108,15 @@ public unsafe struct DesignData
}
}
+ public readonly BonusItem BonusItem(BonusItemFlag slot)
+ => slot switch
+ {
+ // @formatter:off
+ BonusItemFlag.Glasses => new BonusItem(_nameGlasses, _iconIds[12], _bonusIds[0], _bonusModelIds[0], _bonusVariants[0], BonusItemFlag.Glasses),
+ _ => Penumbra.GameData.Structs.BonusItem.Empty(slot),
+ // @formatter:on
+ };
+
public readonly CharacterArmor Armor(EquipSlot slot)
{
fixed (byte* ptr = _equipmentBytes)
@@ -134,7 +150,7 @@ public unsafe struct DesignData
public bool SetItem(EquipSlot slot, EquipItem item)
{
var index = slot.ToIndex();
- if (index > 11)
+ if (index > NumEquipment + NumWeapons)
return false;
_itemIds[index] = item.ItemId.Id;
@@ -173,6 +189,25 @@ public unsafe struct DesignData
return true;
}
+ public bool SetBonusItem(BonusItemFlag slot, BonusItem item)
+ {
+ var index = slot.ToIndex();
+ if (index > NumBonusItems)
+ return false;
+
+ _iconIds[NumEquipment + NumWeapons + index] = item.Icon.Id;
+ _bonusIds[index] = item.Id.Id;
+ _bonusModelIds[index] = item.ModelId.Id;
+ _bonusVariants[index] = item.Variant.Id;
+ switch (index)
+ {
+ case 0:
+ _nameGlasses = item.Name;
+ return true;
+ default: return false;
+ }
+ }
+
public bool SetStain(EquipSlot slot, StainIds stains)
=> slot.ToIndex() switch
{
@@ -313,17 +348,17 @@ public unsafe struct DesignData
MemoryUtility.MemSet(ptr, 0, 10 * 2);
}
- _nameHead = string.Empty;
- _nameBody = string.Empty;
- _nameHands = string.Empty;
- _nameLegs = string.Empty;
- _nameFeet = string.Empty;
- _nameEars = string.Empty;
- _nameNeck = string.Empty;
- _nameWrists = string.Empty;
- _nameRFinger = string.Empty;
- _nameLFinger = string.Empty;
- _nameFaceWear = string.Empty;
+ _nameHead = string.Empty;
+ _nameBody = string.Empty;
+ _nameHands = string.Empty;
+ _nameLegs = string.Empty;
+ _nameFeet = string.Empty;
+ _nameEars = string.Empty;
+ _nameNeck = string.Empty;
+ _nameWrists = string.Empty;
+ _nameRFinger = string.Empty;
+ _nameLFinger = string.Empty;
+ _nameGlasses = string.Empty;
return true;
}
diff --git a/Glamourer/Designs/DesignEditor.cs b/Glamourer/Designs/DesignEditor.cs
index 32e38ac..08cb241 100644
--- a/Glamourer/Designs/DesignEditor.cs
+++ b/Glamourer/Designs/DesignEditor.cs
@@ -165,6 +165,11 @@ public class DesignEditor(
}
}
+ public void ChangeBonusItem(object data, BonusItemFlag slot, BonusItem item, ApplySettings settings = default)
+ {
+
+ }
+
///
public void ChangeStains(object data, EquipSlot slot, StainIds stains, ApplySettings _ = default)
{
diff --git a/Glamourer/Designs/DesignManager.cs b/Glamourer/Designs/DesignManager.cs
index 7bd949c..725d562 100644
--- a/Glamourer/Designs/DesignManager.cs
+++ b/Glamourer/Designs/DesignManager.cs
@@ -347,6 +347,18 @@ public sealed class DesignManager : DesignEditor
DesignChanged.Invoke(DesignChanged.Type.ApplyEquip, design, slot);
}
+ /// Change whether to apply a specific equipment piece.
+ public void ChangeApplyBonusItem(Design design, BonusItemFlag slot, bool value)
+ {
+ if (!design.SetApplyBonusItem(slot, value))
+ return;
+
+ design.LastEdit = DateTimeOffset.UtcNow;
+ SaveService.QueueSave(design);
+ Glamourer.Log.Debug($"Set applying of {slot} bonus item to {value}.");
+ DesignChanged.Invoke(DesignChanged.Type.ApplyBonus, design, slot);
+ }
+
/// Change whether to apply a specific stain.
public void ChangeApplyStain(Design design, EquipSlot slot, bool value)
{
diff --git a/Glamourer/Designs/IDesignEditor.cs b/Glamourer/Designs/IDesignEditor.cs
index cd51cf2..9eefa63 100644
--- a/Glamourer/Designs/IDesignEditor.cs
+++ b/Glamourer/Designs/IDesignEditor.cs
@@ -64,6 +64,9 @@ public interface IDesignEditor
public void ChangeItem(object data, EquipSlot slot, EquipItem item, ApplySettings settings = default)
=> ChangeEquip(data, slot, item, null, settings);
+ /// Change a bonus item.
+ public void ChangeBonusItem(object data, BonusItemFlag slot, BonusItem item, ApplySettings settings = default);
+
/// Change the stain for any equipment piece.
public void ChangeStains(object data, EquipSlot slot, StainIds stains, ApplySettings settings = default)
=> ChangeEquip(data, slot, null, stains, settings);
diff --git a/Glamourer/Designs/Links/DesignMerger.cs b/Glamourer/Designs/Links/DesignMerger.cs
index 558377a..9832ead 100644
--- a/Glamourer/Designs/Links/DesignMerger.cs
+++ b/Glamourer/Designs/Links/DesignMerger.cs
@@ -42,14 +42,15 @@ public class DesignMerger(
if (!data.IsHuman)
continue;
- var (equipFlags, customizeFlags, crestFlags, parameterFlags, applyMeta) = type.ApplyWhat(design);
- ReduceMeta(data, applyMeta, ret, source);
- ReduceCustomize(data, customizeFlags, ref fixFlags, ret, source, respectOwnership, startBodyType);
- ReduceEquip(data, equipFlags, ret, source, respectOwnership);
- ReduceMainhands(data, jobs, equipFlags, ret, source, respectOwnership);
- ReduceOffhands(data, jobs, equipFlags, ret, source, respectOwnership);
- ReduceCrests(data, crestFlags, ret, source);
- ReduceParameters(data, parameterFlags, ret, source);
+ var collection = type.ApplyWhat(design);
+ ReduceMeta(data, collection.Meta, ret, source);
+ ReduceCustomize(data, collection.Customize, ref fixFlags, ret, source, respectOwnership, startBodyType);
+ ReduceEquip(data, collection.Equip, ret, source, respectOwnership);
+ ReduceBonusItems(data, collection.BonusItem, ret, source, respectOwnership);
+ ReduceMainhands(data, jobs, collection.Equip, ret, source, respectOwnership);
+ ReduceOffhands(data, jobs, collection.Equip, ret, source, respectOwnership);
+ ReduceCrests(data, collection.Crest, ret, source);
+ ReduceParameters(data, collection.Parameters, ret, source);
ReduceMods(design as Design, ret, modAssociations);
if (type.HasFlag(ApplicationType.GearCustomization))
ReduceMaterials(design, ret);
@@ -83,7 +84,7 @@ public class DesignMerger(
private static void ReduceMeta(in DesignData design, MetaFlag applyMeta, MergedDesign ret, StateSource source)
{
- applyMeta &= ~ret.Design.ApplyMeta;
+ applyMeta &= ~ret.Design.Application.Meta;
if (applyMeta == 0)
return;
@@ -100,7 +101,7 @@ public class DesignMerger(
private static void ReduceCrests(in DesignData design, CrestFlag crestFlags, MergedDesign ret, StateSource source)
{
- crestFlags &= ~ret.Design.ApplyCrest;
+ crestFlags &= ~ret.Design.Application.Crest;
if (crestFlags == 0)
return;
@@ -118,7 +119,7 @@ public class DesignMerger(
private static void ReduceParameters(in DesignData design, CustomizeParameterFlag parameterFlags, MergedDesign ret,
StateSource source)
{
- parameterFlags &= ~ret.Design.ApplyParameters;
+ parameterFlags &= ~ret.Design.Application.Parameters;
if (parameterFlags == 0)
return;
@@ -136,7 +137,7 @@ public class DesignMerger(
private void ReduceEquip(in DesignData design, EquipFlag equipFlags, MergedDesign ret, StateSource source,
bool respectOwnership)
{
- equipFlags &= ~ret.Design.ApplyEquip;
+ equipFlags &= ~ret.Design.Application.Equip;
if (equipFlags == 0)
return;
@@ -174,6 +175,22 @@ public class DesignMerger(
}
}
+ private void ReduceBonusItems(in DesignData design, BonusItemFlag bonusItems, MergedDesign ret, StateSource source, bool respectOwnership)
+ {
+ bonusItems &= ~ret.Design.Application.BonusItem;
+ if (bonusItems == 0)
+ return;
+
+ foreach (var slot in BonusExtensions.AllFlags.Where(b => bonusItems.HasFlag(b)))
+ {
+ var item = design.BonusItem(slot);
+ if (!respectOwnership || true) // TODO: maybe check unlocks
+ ret.Design.GetDesignDataRef().SetBonusItem(slot, item);
+ ret.Design.SetApplyBonusItem(slot, true);
+ ret.Sources[slot] = source;
+ }
+ }
+
private void ReduceMainhands(in DesignData design, JobFlag allowedJobs, EquipFlag equipFlags, MergedDesign ret, StateSource source,
bool respectOwnership)
{
diff --git a/Glamourer/Designs/Links/MergedDesign.cs b/Glamourer/Designs/Links/MergedDesign.cs
index d3c7664..da6cb54 100644
--- a/Glamourer/Designs/Links/MergedDesign.cs
+++ b/Glamourer/Designs/Links/MergedDesign.cs
@@ -64,12 +64,8 @@ public sealed class MergedDesign
{
public MergedDesign(DesignManager designManager)
{
- Design = designManager.CreateTemporary();
- Design.ApplyEquip = 0;
- Design.ApplyCustomize = 0;
- Design.ApplyCrest = 0;
- Design.ApplyParameters = 0;
- Design.ApplyMeta = 0;
+ Design = designManager.CreateTemporary();
+ Design.Application = ApplicationCollection.None;
}
public MergedDesign(DesignBase design)
diff --git a/Glamourer/Events/BonusSlotUpdating.cs b/Glamourer/Events/BonusSlotUpdating.cs
index 3685f3c..3f6e761 100644
--- a/Glamourer/Events/BonusSlotUpdating.cs
+++ b/Glamourer/Events/BonusSlotUpdating.cs
@@ -15,7 +15,7 @@ namespace Glamourer.Events;
///
///
public sealed class BonusSlotUpdating()
- : EventWrapperRef34(nameof(BonusSlotUpdating))
+ : EventWrapperRef34(nameof(BonusSlotUpdating))
{
public enum Priority
{
diff --git a/Glamourer/Events/DesignChanged.cs b/Glamourer/Events/DesignChanged.cs
index 1837aad..1588a96 100644
--- a/Glamourer/Events/DesignChanged.cs
+++ b/Glamourer/Events/DesignChanged.cs
@@ -89,6 +89,9 @@ public sealed class DesignChanged()
/// An existing design changed whether a specific equipment piece is applied. Data is the slot of the equipment [EquipSlot].
ApplyEquip,
+ /// An existing design changed whether a specific bonus item is applied. Data is the slot of the item [BonusItemFlag].
+ ApplyBonus,
+
/// An existing design changed whether a specific stain is applied. Data is the slot of the equipment [EquipSlot].
ApplyStain,
diff --git a/Glamourer/Gui/DesignQuickBar.cs b/Glamourer/Gui/DesignQuickBar.cs
index a0a341a..c3829fa 100644
--- a/Glamourer/Gui/DesignQuickBar.cs
+++ b/Glamourer/Gui/DesignQuickBar.cs
@@ -179,8 +179,7 @@ public sealed class DesignQuickBar : Window, IDisposable
return;
}
- var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags();
- using var _ = design!.TemporarilyRestrictApplication(applyGear, applyCustomize, applyCrest, applyParameters);
+ using var _ = design!.TemporarilyRestrictApplication(ApplicationCollection.FromKeys());
_stateManager.ApplyDesign(state, design, ApplySettings.ManualWithLinks);
}
diff --git a/Glamourer/Gui/Equipment/BonusDrawData.cs b/Glamourer/Gui/Equipment/BonusDrawData.cs
new file mode 100644
index 0000000..d2a6b2e
--- /dev/null
+++ b/Glamourer/Gui/Equipment/BonusDrawData.cs
@@ -0,0 +1,57 @@
+using Glamourer.Designs;
+using Glamourer.State;
+using Penumbra.GameData.Enums;
+using Penumbra.GameData.Structs;
+
+namespace Glamourer.Gui.Equipment;
+
+public struct BonusDrawData(BonusItemFlag slot, in DesignData designData)
+{
+ private IDesignEditor _editor;
+ private object _object;
+ public readonly BonusItemFlag Slot = slot;
+ public bool Locked;
+ public bool DisplayApplication;
+ public bool AllowRevert;
+
+ public readonly bool IsDesign
+ => _object is Design;
+
+ public readonly bool IsState
+ => _object is ActorState;
+
+ public readonly void SetItem(BonusItem item)
+ => _editor.ChangeBonusItem(_object, Slot, item, ApplySettings.Manual);
+
+ public readonly void SetApplyItem(bool value)
+ {
+ var manager = (DesignManager)_editor;
+ var design = (Design)_object;
+ manager.ChangeApplyBonusItem(design, Slot, value);
+ }
+
+ public BonusItem CurrentItem = designData.BonusItem(slot);
+ public BonusItem GameItem = default;
+ public bool CurrentApply;
+
+ public static BonusDrawData FromDesign(DesignManager manager, Design design, BonusItemFlag slot)
+ => new(slot, design.DesignData)
+ {
+ _editor = manager,
+ _object = design,
+ CurrentApply = design.DoApplyBonusItem(slot),
+ Locked = design.WriteProtected(),
+ DisplayApplication = true,
+ };
+
+ public static BonusDrawData FromState(StateManager manager, ActorState state, BonusItemFlag slot)
+ => new(slot, state.ModelData)
+ {
+ _editor = manager,
+ _object = state,
+ Locked = state.IsLocked,
+ DisplayApplication = false,
+ GameItem = state.BaseData.BonusItem(slot),
+ AllowRevert = true,
+ };
+}
diff --git a/Glamourer/Gui/Equipment/BonusItemCombo.cs b/Glamourer/Gui/Equipment/BonusItemCombo.cs
new file mode 100644
index 0000000..ae8bf19
--- /dev/null
+++ b/Glamourer/Gui/Equipment/BonusItemCombo.cs
@@ -0,0 +1,123 @@
+using Dalamud.Plugin.Services;
+using Glamourer.Services;
+using Glamourer.Unlocks;
+using ImGuiNET;
+using Lumina.Excel.GeneratedSheets;
+using OtterGui;
+using OtterGui.Classes;
+using OtterGui.Log;
+using OtterGui.Raii;
+using OtterGui.Widgets;
+using Penumbra.GameData.Enums;
+using Penumbra.GameData.Structs;
+
+namespace Glamourer.Gui.Equipment;
+
+public sealed class BonusItemCombo : FilterComboCache
+{
+ private readonly FavoriteManager _favorites;
+ public readonly string Label;
+ private BonusItemId _currentItem;
+ private float _innerWidth;
+
+ public PrimaryId CustomSetId { get; private set; }
+ public Variant CustomVariant { get; private set; }
+
+ public BonusItemCombo(IDataManager gameData, ItemManager items, BonusItemFlag slot, Logger log, FavoriteManager favorites)
+ : base(() => GetItems(favorites, items, slot), MouseWheelType.Control, log)
+ {
+ _favorites = favorites;
+ Label = GetLabel(gameData, slot);
+ _currentItem = 0;
+ SearchByParts = true;
+ }
+
+ protected override void DrawList(float width, float itemHeight)
+ {
+ base.DrawList(width, itemHeight);
+ if (NewSelection != null && Items.Count > NewSelection.Value)
+ CurrentSelection = Items[NewSelection.Value];
+ }
+
+ protected override int UpdateCurrentSelected(int currentSelected)
+ {
+ if (CurrentSelection.Id == _currentItem)
+ return currentSelected;
+
+ CurrentSelectionIdx = Items.IndexOf(i => i.Id == _currentItem);
+ CurrentSelection = CurrentSelectionIdx >= 0 ? Items[CurrentSelectionIdx] : default;
+ return base.UpdateCurrentSelected(CurrentSelectionIdx);
+ }
+
+ public bool Draw(string previewName, BonusItemId previewIdx, float width, float innerWidth)
+ {
+ _innerWidth = innerWidth;
+ _currentItem = previewIdx;
+ CustomVariant = 0;
+ return Draw($"##{Label}", previewName, string.Empty, width, ImGui.GetTextLineHeightWithSpacing());
+ }
+
+ protected override float GetFilterWidth()
+ => _innerWidth - 2 * ImGui.GetStyle().FramePadding.X;
+
+ protected override bool DrawSelectable(int globalIdx, bool selected)
+ {
+ var obj = Items[globalIdx];
+ var name = ToString(obj);
+ if (UiHelpers.DrawFavoriteStar(_favorites, obj) && CurrentSelectionIdx == globalIdx)
+ {
+ CurrentSelectionIdx = -1;
+ _currentItem = obj.Id;
+ CurrentSelection = default;
+ }
+
+ ImGui.SameLine();
+ var ret = ImGui.Selectable(name, selected);
+ ImGui.SameLine();
+ using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF808080);
+ ImGuiUtil.RightAlign($"({obj.ModelId.Id}-{obj.Variant.Id})");
+ return ret;
+ }
+
+ protected override bool IsVisible(int globalIndex, LowerString filter)
+ => base.IsVisible(globalIndex, filter) || filter.IsContained(Items[globalIndex].ModelId.Id.ToString());
+
+ protected override string ToString(BonusItem obj)
+ => obj.Name;
+
+ private static string GetLabel(IDataManager gameData, BonusItemFlag slot)
+ {
+ var sheet = gameData.GetExcelSheet()!;
+
+ return slot switch
+ {
+ BonusItemFlag.Glasses => sheet.GetRow(16050)?.Text.ToString() ?? "Facewear",
+ BonusItemFlag.UnkSlot => sheet.GetRow(16051)?.Text.ToString() ?? "Facewear",
+
+ _ => string.Empty,
+ };
+ }
+
+ private static List GetItems(FavoriteManager favorites, ItemManager items, BonusItemFlag slot)
+ {
+ var nothing = BonusItem.Empty(slot);
+ if (slot is not BonusItemFlag.Glasses)
+ return [nothing];
+
+ return items.DictBonusItems.Values.OrderByDescending(favorites.Contains).ThenBy(i => i.Id.Id).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;
+ }
+}
diff --git a/Glamourer/Gui/Equipment/EquipDrawData.cs b/Glamourer/Gui/Equipment/EquipDrawData.cs
index 58f7efc..8058169 100644
--- a/Glamourer/Gui/Equipment/EquipDrawData.cs
+++ b/Glamourer/Gui/Equipment/EquipDrawData.cs
@@ -72,4 +72,4 @@ public struct EquipDrawData(EquipSlot slot, in DesignData designData)
GameStains = state.BaseData.Stain(slot),
AllowRevert = true,
};
-}
+}
\ No newline at end of file
diff --git a/Glamourer/Gui/Equipment/EquipmentDrawer.cs b/Glamourer/Gui/Equipment/EquipmentDrawer.cs
index 53e8ed8..afd3fe5 100644
--- a/Glamourer/Gui/Equipment/EquipmentDrawer.cs
+++ b/Glamourer/Gui/Equipment/EquipmentDrawer.cs
@@ -24,6 +24,7 @@ public class EquipmentDrawer
private readonly GlamourerColorCombo _stainCombo;
private readonly DictStain _stainData;
private readonly ItemCombo[] _itemCombo;
+ private readonly BonusItemCombo[] _bonusItemCombo;
private readonly Dictionary _weaponCombo;
private readonly CodeService _codes;
private readonly TextureService _textures;
@@ -37,16 +38,17 @@ public class EquipmentDrawer
public EquipmentDrawer(FavoriteManager favorites, IDataManager gameData, ItemManager items, CodeService codes, TextureService textures,
Configuration config, GPoseService gPose, AdvancedDyePopup advancedDyes)
{
- _items = items;
- _codes = codes;
- _textures = textures;
- _config = config;
- _gPose = gPose;
- _advancedDyes = advancedDyes;
- _stainData = items.Stains;
- _stainCombo = new GlamourerColorCombo(DefaultWidth - 20, _stainData, favorites);
- _itemCombo = EquipSlotExtensions.EqdpSlots.Select(e => new ItemCombo(gameData, items, e, Glamourer.Log, favorites)).ToArray();
- _weaponCombo = new Dictionary(FullEquipTypeExtensions.WeaponTypes.Count * 2);
+ _items = items;
+ _codes = codes;
+ _textures = textures;
+ _config = config;
+ _gPose = gPose;
+ _advancedDyes = advancedDyes;
+ _stainData = items.Stains;
+ _stainCombo = new GlamourerColorCombo(DefaultWidth - 20, _stainData, favorites);
+ _itemCombo = EquipSlotExtensions.EqdpSlots.Select(e => new ItemCombo(gameData, items, e, Glamourer.Log, favorites)).ToArray();
+ _bonusItemCombo = BonusExtensions.AllFlags.Select(f => new BonusItemCombo(gameData, items, f, Glamourer.Log, favorites)).ToArray();
+ _weaponCombo = new Dictionary(FullEquipTypeExtensions.WeaponTypes.Count * 2);
foreach (var type in Enum.GetValues())
{
if (type.ToSlot() is EquipSlot.MainHand)
@@ -100,6 +102,21 @@ public class EquipmentDrawer
DrawEquipNormal(equipDrawData);
}
+ public void DrawBonusItem(BonusDrawData bonusDrawData)
+ {
+ if (_config.HideApplyCheckmarks)
+ bonusDrawData.DisplayApplication = false;
+
+ using var id = ImRaii.PushId(100 + (int)bonusDrawData.Slot);
+ var spacing = ImGui.GetStyle().ItemInnerSpacing with { Y = ImGui.GetStyle().ItemSpacing.Y };
+ using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing);
+
+ if (_config.SmallEquip)
+ DrawBonusItemSmall(bonusDrawData);
+ else
+ DrawBonusItemNormal(bonusDrawData);
+ }
+
public void DrawWeapons(EquipDrawData mainhand, EquipDrawData offhand, bool allWeapons)
{
if (mainhand.CurrentItem.PrimaryId.Id == 0)
@@ -302,6 +319,25 @@ public class EquipmentDrawer
ImGui.TextUnformatted(label);
}
+ private void DrawBonusItemSmall(in BonusDrawData bonusDrawData)
+ {
+ ImGui.Dummy(new Vector2(StainId.NumStains * ImUtf8.FrameHeight + (StainId.NumStains - 1) * ImUtf8.ItemSpacing.X, ImUtf8.FrameHeight));
+ ImGui.SameLine();
+ DrawBonusItem(bonusDrawData, out var label, true, false, false);
+ if (bonusDrawData.DisplayApplication)
+ {
+ ImGui.SameLine();
+ DrawApply(bonusDrawData);
+ }
+ else if (bonusDrawData.IsState)
+ {
+ _advancedDyes.DrawButton(bonusDrawData.Slot);
+ }
+
+ ImGui.SameLine();
+ ImGui.TextUnformatted(label);
+ }
+
private void DrawWeaponsSmall(EquipDrawData mainhand, EquipDrawData offhand, bool allWeapons)
{
DrawStain(mainhand, true);
@@ -382,6 +418,27 @@ public class EquipmentDrawer
}
}
+ private void DrawBonusItemNormal(in BonusDrawData bonusDrawData)
+ {
+ ImGui.Dummy(_iconSize with { Y = ImUtf8.FrameHeight });
+ var right = ImGui.IsItemClicked(ImGuiMouseButton.Right);
+ var left = ImGui.IsItemClicked(ImGuiMouseButton.Left);
+ ImGui.SameLine();
+ DrawBonusItem(bonusDrawData, out var label, false, right, left);
+ if (bonusDrawData.DisplayApplication)
+ {
+ ImGui.SameLine();
+ DrawApply(bonusDrawData);
+ }
+ else if (bonusDrawData.IsState)
+ {
+ _advancedDyes.DrawButton(bonusDrawData.Slot);
+ }
+
+ ImGui.SameLine();
+ ImGui.TextUnformatted(label);
+ }
+
private void DrawWeaponsNormal(EquipDrawData mainhand, EquipDrawData offhand, bool allWeapons)
{
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
@@ -491,6 +548,25 @@ public class EquipmentDrawer
data.SetItem(item);
}
+ private void DrawBonusItem(in BonusDrawData data, out string label, bool small, bool clear, bool open)
+ {
+ var combo = _bonusItemCombo[data.Slot.ToIndex()];
+ label = combo.Label;
+ if (!data.Locked && open)
+ UiHelpers.OpenCombo($"##{combo.Label}");
+
+ using var disabled = ImRaii.Disabled(data.Locked);
+ var change = combo.Draw(data.CurrentItem.Name, data.CurrentItem.Id, small ? _comboLength - ImGui.GetFrameHeight() : _comboLength,
+ _requiredComboWidth);
+ if (change)
+ data.SetItem(combo.CurrentSelection);
+ else if (combo.CustomVariant.Id > 0)
+ data.SetItem(_items.Identify(data.Slot, combo.CustomSetId, combo.CustomVariant));
+
+ if (ResetOrClear(data.Locked, clear, data.AllowRevert, true, data.CurrentItem, data.GameItem, BonusItem.Empty(data.Slot), out var item))
+ data.SetItem(item);
+ }
+
private static bool ResetOrClear(bool locked, bool clicked, bool allowRevert, bool allowClear,
in T currentItem, in T revertItem, in T clearItem, out T? item) where T : IEquatable
{
@@ -590,6 +666,13 @@ public class EquipmentDrawer
data.SetApplyItem(enabled);
}
+ private static void DrawApply(in BonusDrawData data)
+ {
+ if (UiHelpers.DrawCheckbox($"##apply{data.Slot}", "Apply this bonus item when applying the Design.", data.CurrentApply, out var enabled,
+ data.Locked))
+ data.SetApplyItem(enabled);
+ }
+
private static void DrawApplyStain(in EquipDrawData data)
{
if (UiHelpers.DrawCheckbox($"##applyStain{data.Slot}", "Apply this dye to the item when applying the Design.", data.CurrentApplyStain,
diff --git a/Glamourer/Gui/Materials/AdvancedDyePopup.cs b/Glamourer/Gui/Materials/AdvancedDyePopup.cs
index 232541e..3160bcb 100644
--- a/Glamourer/Gui/Materials/AdvancedDyePopup.cs
+++ b/Glamourer/Gui/Materials/AdvancedDyePopup.cs
@@ -46,6 +46,9 @@ public sealed unsafe class AdvancedDyePopup(
public void DrawButton(EquipSlot slot)
=> DrawButton(MaterialValueIndex.FromSlot(slot));
+ public void DrawButton(BonusItemFlag slot)
+ => DrawButton(MaterialValueIndex.FromSlot(slot));
+
private void DrawButton(MaterialValueIndex index)
{
if (!config.UseAdvancedDyes)
diff --git a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs
index 5218581..4074574 100644
--- a/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs
+++ b/Glamourer/Gui/Tabs/ActorTab/ActorPanel.cs
@@ -215,6 +215,12 @@ public class ActorPanel
var offhand = EquipDrawData.FromState(_stateManager, _state, EquipSlot.OffHand);
_equipmentDrawer.DrawWeapons(mainhand, offhand, GameMain.IsInGPose());
+ foreach (var slot in BonusExtensions.AllFlags)
+ {
+ var data = BonusDrawData.FromState(_stateManager, _state!, slot);
+ _equipmentDrawer.DrawBonusItem(data);
+ }
+
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
DrawEquipmentMetaToggles();
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
diff --git a/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs b/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs
index 7da95a3..8d52a76 100644
--- a/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs
+++ b/Glamourer/Gui/Tabs/AutomationTab/SetPanel.cs
@@ -277,13 +277,13 @@ public class SetPanel(
var size = new Vector2(ImGui.GetFrameHeight());
size.X += ImGuiHelpers.GlobalScale;
- var (equipFlags, customizeFlags, _, _, _) = design.ApplyWhat();
+ var collection = design.ApplyWhat();
var sb = new StringBuilder();
var designData = design.Design.GetDesignData(default);
foreach (var slot in EquipSlotExtensions.EqdpSlots.Append(EquipSlot.MainHand).Append(EquipSlot.OffHand))
{
var flag = slot.ToFlag();
- if (!equipFlags.HasFlag(flag))
+ if (!collection.Equip.HasFlag(flag))
continue;
var item = designData.Item(slot);
@@ -308,7 +308,7 @@ public class SetPanel(
foreach (var type in CustomizationExtensions.All)
{
var flag = type.ToFlag();
- if (!customizeFlags.HasFlag(flag))
+ if (!collection.Customize.HasFlag(flag))
continue;
if (flag.RequiresRedraw())
diff --git a/Glamourer/Gui/Tabs/DebugTab/DesignManagerPanel.cs b/Glamourer/Gui/Tabs/DebugTab/DesignManagerPanel.cs
index 85b4010..b562ecf 100644
--- a/Glamourer/Gui/Tabs/DebugTab/DesignManagerPanel.cs
+++ b/Glamourer/Gui/Tabs/DebugTab/DesignManagerPanel.cs
@@ -25,7 +25,7 @@ public class DesignManagerPanel(DesignManager _designManager, DesignFileSystem _
continue;
DrawDesign(design, _designFileSystem);
- var base64 = DesignBase64Migration.CreateOldBase64(design.DesignData, design.ApplyEquip, design.ApplyCustomizeRaw, design.ApplyMeta,
+ var base64 = DesignBase64Migration.CreateOldBase64(design.DesignData, design.Application.Equip, design.Application.Customize, design.Application.Meta,
design.WriteProtected());
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
ImGuiUtil.TextWrapped(base64);
diff --git a/Glamourer/Gui/Tabs/DebugTab/GlamourPlatePanel.cs b/Glamourer/Gui/Tabs/DebugTab/GlamourPlatePanel.cs
index ff9c2b8..394bd7f 100644
--- a/Glamourer/Gui/Tabs/DebugTab/GlamourPlatePanel.cs
+++ b/Glamourer/Gui/Tabs/DebugTab/GlamourPlatePanel.cs
@@ -112,11 +112,7 @@ public unsafe class GlamourPlatePanel : IGameDataDrawer
public DesignBase CreateDesign(in MirageManager.GlamourPlate plate)
{
var design = _design.CreateTemporary();
- design.ApplyCustomize = 0;
- design.ApplyCrest = 0;
- design.ApplyMeta = 0;
- design.ApplyParameters = 0;
- design.ApplyEquip = 0;
+ design.Application = ApplicationCollection.None;
foreach (var (slot, index) in EquipSlotExtensions.FullSlots.WithIndex())
{
var itemId = plate.ItemIds[index];
@@ -129,7 +125,7 @@ public unsafe class GlamourPlatePanel : IGameDataDrawer
design.GetDesignDataRef().SetItem(slot, item);
design.GetDesignDataRef().SetStain(slot, StainIds.FromGlamourPlate(plate, index));
- design.ApplyEquip |= slot.ToBothFlags();
+ design.Application.Equip |= slot.ToBothFlags();
}
return design;
diff --git a/Glamourer/Gui/Tabs/DebugTab/ModelEvaluationPanel.cs b/Glamourer/Gui/Tabs/DebugTab/ModelEvaluationPanel.cs
index dd1c125..7307f22 100644
--- a/Glamourer/Gui/Tabs/DebugTab/ModelEvaluationPanel.cs
+++ b/Glamourer/Gui/Tabs/DebugTab/ModelEvaluationPanel.cs
@@ -21,7 +21,7 @@ public unsafe class ModelEvaluationPanel(
UpdateSlotService _updateSlotService,
ChangeCustomizeService _changeCustomizeService,
CrestService _crestService,
- DictGlasses _glasses) : IGameDataDrawer
+ DictBonusItems bonusItems) : IGameDataDrawer
{
public string Label
=> "Model Evaluation";
@@ -57,6 +57,16 @@ public unsafe class ModelEvaluationPanel(
ImGui.TextUnformatted($"Transformation Id: {actor.AsCharacter->CharacterData.TransformationId}");
if (actor.AsCharacter->CharacterData.ModelCharaId_2 != -1)
ImGui.TextUnformatted($"ModelChara2 {actor.AsCharacter->CharacterData.ModelCharaId_2}");
+
+ ImGuiUtil.DrawTableColumn("Character Mode");
+ ImGuiUtil.DrawTableColumn($"{actor.AsCharacter->Mode}");
+ ImGui.TableNextColumn();
+ ImGui.TableNextColumn();
+
+ ImGuiUtil.DrawTableColumn("Animation");
+ ImGuiUtil.DrawTableColumn($"{((ushort*)&actor.AsCharacter->Timeline)[0x78]}");
+ ImGui.TableNextColumn();
+ ImGui.TableNextColumn();
}
ImGuiUtil.DrawTableColumn("Mainhand");
@@ -226,7 +236,7 @@ public unsafe class ModelEvaluationPanel(
_updateSlotService.UpdateEquipSlot(model, slot, actor.GetArmor(slot));
}
- foreach (var slot in BonusSlotExtensions.AllFlags)
+ foreach (var slot in BonusExtensions.AllFlags)
{
using var id2 = ImRaii.PushId((int)slot.ToModelIndex());
ImGuiUtil.DrawTableColumn(slot.ToName());
@@ -236,9 +246,9 @@ public unsafe class ModelEvaluationPanel(
}
else
{
- var glassesId = actor.GetBonusSlot(slot);
- if (_glasses.TryGetValue(glassesId, out var glasses))
- ImGuiUtil.DrawTableColumn($"{glasses.Id.Id},{glasses.Variant.Id} ({glassesId})");
+ var glassesId = actor.GetBonusItem(slot);
+ if (bonusItems.TryGetValue(glassesId, out var glasses))
+ ImGuiUtil.DrawTableColumn($"{glasses.ModelId.Id},{glasses.Variant.Id} ({glassesId})");
else
ImGuiUtil.DrawTableColumn($"{glassesId}");
}
diff --git a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs
index 50fd936..b20e00d 100644
--- a/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs
+++ b/Glamourer/Gui/Tabs/DesignTab/DesignPanel.cs
@@ -112,6 +112,13 @@ public class DesignPanel
var mainhand = EquipDrawData.FromDesign(_manager, _selector.Selected!, EquipSlot.MainHand);
var offhand = EquipDrawData.FromDesign(_manager, _selector.Selected!, EquipSlot.OffHand);
_equipmentDrawer.DrawWeapons(mainhand, offhand, true);
+
+ foreach (var slot in BonusExtensions.AllFlags)
+ {
+ var data = BonusDrawData.FromDesign(_manager, _selector.Selected!, slot);
+ _equipmentDrawer.DrawBonusItem(data);
+ }
+
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
DrawEquipmentMetaToggles();
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
@@ -149,7 +156,7 @@ public class DesignPanel
if (!h)
return;
- if (_customizationDrawer.Draw(_selector.Selected!.DesignData.Customize, _selector.Selected.ApplyCustomizeRaw,
+ if (_customizationDrawer.Draw(_selector.Selected!.DesignData.Customize, _selector.Selected.Application.Customize,
_selector.Selected!.WriteProtected(), false))
foreach (var idx in Enum.GetValues())
{
@@ -224,7 +231,7 @@ public class DesignPanel
private void DrawCrestApplication()
{
using var id = ImRaii.PushId("Crests");
- var flags = (uint)_selector.Selected!.ApplyCrest;
+ var flags = (uint)_selector.Selected!.Application.Crest;
var bigChange = ImGui.CheckboxFlags("Apply All Crests", ref flags, (uint)CrestExtensions.AllRelevant);
foreach (var flag in CrestExtensions.AllRelevantSet)
{
@@ -255,7 +262,7 @@ public class DesignPanel
{
void ApplyEquip(string label, EquipFlag allFlags, bool stain, IEnumerable slots)
{
- var flags = (uint)(allFlags & _selector.Selected!.ApplyEquip);
+ var flags = (uint)(allFlags & _selector.Selected!.Application.Equip);
using var id = ImRaii.PushId(label);
var bigChange = ImGui.CheckboxFlags($"Apply All {label}", ref flags, (uint)allFlags);
if (stain)
@@ -302,7 +309,7 @@ public class DesignPanel
{
using var id = ImRaii.PushId("Meta");
const uint all = (uint)MetaExtensions.All;
- var flags = (uint)_selector.Selected!.ApplyMeta;
+ var flags = (uint)_selector.Selected!.Application.Meta;
var bigChange = ImGui.CheckboxFlags("Apply All Meta Changes", ref flags, all);
var labels = new[]
@@ -324,7 +331,7 @@ public class DesignPanel
private void DrawParameterApplication()
{
using var id = ImRaii.PushId("Parameter");
- var flags = (uint)_selector.Selected!.ApplyParameters;
+ var flags = (uint)_selector.Selected!.Application.Parameters;
var bigChange = ImGui.CheckboxFlags("Apply All Customize Parameters", ref flags, (uint)CustomizeParameterExtensions.All);
foreach (var flag in CustomizeParameterExtensions.AllFlags)
{
@@ -408,8 +415,7 @@ public class DesignPanel
if (_state.GetOrCreate(id, data.Objects[0], out var state))
{
- var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags();
- using var _ = _selector.Selected!.TemporarilyRestrictApplication(applyGear, applyCustomize, applyCrest, applyParameters);
+ using var _ = _selector.Selected!.TemporarilyRestrictApplication(ApplicationCollection.FromKeys());
_state.ApplyDesign(state, _selector.Selected!, ApplySettings.ManualWithLinks);
}
}
@@ -427,8 +433,7 @@ public class DesignPanel
if (_state.GetOrCreate(id, data.Objects[0], out var state))
{
- var (applyGear, applyCustomize, applyCrest, applyParameters) = UiHelpers.ConvertKeysToFlags();
- using var _ = _selector.Selected!.TemporarilyRestrictApplication(applyGear, applyCustomize, applyCrest, applyParameters);
+ using var _ = _selector.Selected!.TemporarilyRestrictApplication(ApplicationCollection.FromKeys());
_state.ApplyDesign(state, _selector.Selected!, ApplySettings.ManualWithLinks);
}
}
diff --git a/Glamourer/Gui/UiHelpers.cs b/Glamourer/Gui/UiHelpers.cs
index 7e22ff1..88f51f5 100644
--- a/Glamourer/Gui/UiHelpers.cs
+++ b/Glamourer/Gui/UiHelpers.cs
@@ -1,6 +1,5 @@
using Dalamud.Interface;
using Dalamud.Interface.Utility;
-using Glamourer.GameData;
using Glamourer.Services;
using Glamourer.Unlocks;
using ImGuiNET;
@@ -98,15 +97,6 @@ public static class UiHelpers
return (currentValue != newValue, currentApply != newApply);
}
- public static (EquipFlag, CustomizeFlag, CrestFlag, CustomizeParameterFlag) ConvertKeysToFlags()
- => (ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift) switch
- {
- (false, false) => (EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant, CrestExtensions.All, CustomizeParameterExtensions.All),
- (true, true) => (EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant, CrestExtensions.All, CustomizeParameterExtensions.All),
- (true, false) => (EquipFlagExtensions.All, (CustomizeFlag)0, CrestExtensions.All, 0),
- (false, true) => ((EquipFlag)0, CustomizeFlagExtensions.AllRelevant, 0, CustomizeParameterExtensions.All),
- };
-
public static (bool, bool) ConvertKeysToBool()
=> (ImGui.GetIO().KeyCtrl, ImGui.GetIO().KeyShift) switch
{
@@ -126,16 +116,36 @@ public static class UiHelpers
using var c = ImRaii.PushColor(ImGuiCol.Text,
hovering ? ColorId.FavoriteStarHovered.Value() : favorite ? ColorId.FavoriteStarOn.Value() : ColorId.FavoriteStarOff.Value());
ImGui.TextUnformatted(FontAwesomeIcon.Star.ToIconString());
- if (ImGui.IsItemClicked())
- {
- if (favorite)
- favorites.Remove(item);
- else
- favorites.TryAdd(item);
- return true;
- }
+ if (!ImGui.IsItemClicked())
+ return false;
+
+ if (favorite)
+ favorites.Remove(item);
+ else
+ favorites.TryAdd(item);
+ return true;
+
+ }
+
+ public static bool DrawFavoriteStar(FavoriteManager favorites, BonusItem item)
+ {
+ var favorite = favorites.Contains(item);
+ var hovering = ImGui.IsMouseHoveringRect(ImGui.GetCursorScreenPos(),
+ ImGui.GetCursorScreenPos() + new Vector2(ImGui.GetTextLineHeight()));
+
+ using var font = ImRaii.PushFont(UiBuilder.IconFont);
+ using var c = ImRaii.PushColor(ImGuiCol.Text,
+ hovering ? ColorId.FavoriteStarHovered.Value() : favorite ? ColorId.FavoriteStarOn.Value() : ColorId.FavoriteStarOff.Value());
+ ImGui.TextUnformatted(FontAwesomeIcon.Star.ToIconString());
+ if (!ImGui.IsItemClicked())
+ return false;
+
+ if (favorite)
+ favorites.Remove(item);
+ else
+ favorites.TryAdd(item);
+ return true;
- return false;
}
public static bool DrawFavoriteStar(FavoriteManager favorites, StainId stain)
@@ -149,15 +159,14 @@ public static class UiHelpers
hovering ? ColorId.FavoriteStarHovered.Value() : favorite ? ColorId.FavoriteStarOn.Value() : ColorId.FavoriteStarOff.Value());
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(FontAwesomeIcon.Star.ToIconString());
- if (ImGui.IsItemClicked())
- {
- if (favorite)
- favorites.Remove(stain);
- else
- favorites.TryAdd(stain);
- return true;
- }
+ if (!ImGui.IsItemClicked())
+ return false;
+
+ if (favorite)
+ favorites.Remove(stain);
+ else
+ favorites.TryAdd(stain);
+ return true;
- return false;
}
-}
+}
\ No newline at end of file
diff --git a/Glamourer/Interop/Material/MaterialValueIndex.cs b/Glamourer/Interop/Material/MaterialValueIndex.cs
index 2096bc7..254675e 100644
--- a/Glamourer/Interop/Material/MaterialValueIndex.cs
+++ b/Glamourer/Interop/Material/MaterialValueIndex.cs
@@ -42,6 +42,12 @@ public readonly record struct MaterialValueIndex(
return Invalid;
}
+ public static MaterialValueIndex FromSlot(BonusItemFlag slot)
+ {
+ var idx = slot.ToIndex();
+ return idx > 2 ? Invalid : new MaterialValueIndex(DrawObjectType.Human, (byte)(idx + 16), 0, 0);
+ }
+
public EquipSlot ToEquipSlot()
=> DrawObject switch
{
diff --git a/Glamourer/Interop/PalettePlus/PaletteImport.cs b/Glamourer/Interop/PalettePlus/PaletteImport.cs
index 93c3fa2..4887255 100644
--- a/Glamourer/Interop/PalettePlus/PaletteImport.cs
+++ b/Glamourer/Interop/PalettePlus/PaletteImport.cs
@@ -37,17 +37,13 @@ public class PaletteImport(IDalamudPluginInterface pluginInterface, DesignManage
}
var design = designManager.CreateEmpty(fullPath, true);
- design.ApplyCustomize = 0;
- design.ApplyEquip = 0;
- design.ApplyCrest = 0;
- designManager.ChangeApplyMeta(design, MetaIndex.VisorState, false);
- designManager.ChangeApplyMeta(design, MetaIndex.HatState, false);
- designManager.ChangeApplyMeta(design, MetaIndex.WeaponState, false);
+ design.Application = ApplicationCollection.None;
foreach (var flag in flags.Iterate())
{
designManager.ChangeApplyParameter(design, flag, true);
designManager.ChangeCustomizeParameter(design, flag, palette[flag]);
}
+
Glamourer.Log.Information($"Added design for palette {name} at {fullPath}.");
}
}
diff --git a/Glamourer/Interop/UpdateSlotService.cs b/Glamourer/Interop/UpdateSlotService.cs
index 2be31cf..55db36d 100644
--- a/Glamourer/Interop/UpdateSlotService.cs
+++ b/Glamourer/Interop/UpdateSlotService.cs
@@ -14,14 +14,14 @@ public unsafe class UpdateSlotService : IDisposable
{
public readonly EquipSlotUpdating EquipSlotUpdatingEvent;
public readonly BonusSlotUpdating BonusSlotUpdatingEvent;
- private readonly DictGlasses _glasses;
+ private readonly DictBonusItems _bonusItems;
public UpdateSlotService(EquipSlotUpdating equipSlotUpdating, BonusSlotUpdating bonusSlotUpdating, IGameInteropProvider interop,
- DictGlasses glasses)
+ DictBonusItems bonusItems)
{
EquipSlotUpdatingEvent = equipSlotUpdating;
BonusSlotUpdatingEvent = bonusSlotUpdating;
- _glasses = glasses;
+ _bonusItems = bonusItems;
interop.InitializeFromAttributes(this);
_flagSlotForUpdateHook.Enable();
_flagBonusSlotForUpdateHook.Enable();
@@ -41,7 +41,7 @@ public unsafe class UpdateSlotService : IDisposable
FlagSlotForUpdateInterop(drawObject, slot, data);
}
- public void UpdateBonusSlot(Model drawObject, BonusEquipFlag slot, CharacterArmor data)
+ public void UpdateBonusSlot(Model drawObject, BonusItemFlag slot, CharacterArmor data)
{
if (!drawObject.IsCharacterBase)
return;
@@ -53,13 +53,13 @@ public unsafe class UpdateSlotService : IDisposable
_flagBonusSlotForUpdateHook.Original(drawObject.Address, index, &data);
}
- public void UpdateGlasses(Model drawObject, GlassesId id)
+ public void UpdateGlasses(Model drawObject, BonusItemId id)
{
- if (!_glasses.TryGetValue(id, out var glasses))
+ if (!_bonusItems.TryGetValue(id, out var glasses))
return;
- var armor = new CharacterArmor(glasses.Id, glasses.Variant, StainIds.None);
- _flagBonusSlotForUpdateHook.Original(drawObject.Address, BonusEquipFlag.Glasses.ToIndex(), &armor);
+ var armor = new CharacterArmor(glasses.ModelId, glasses.Variant, StainIds.None);
+ _flagBonusSlotForUpdateHook.Original(drawObject.Address, BonusItemFlag.Glasses.ToIndex(), &armor);
}
public void UpdateArmor(Model drawObject, EquipSlot slot, CharacterArmor armor, StainIds stains)
diff --git a/Glamourer/Services/ItemManager.cs b/Glamourer/Services/ItemManager.cs
index c5f537f..7b83199 100644
--- a/Glamourer/Services/ItemManager.cs
+++ b/Glamourer/Services/ItemManager.cs
@@ -20,12 +20,13 @@ public class ItemManager
public readonly ExcelSheet ItemSheet;
public readonly DictStain Stains;
public readonly ItemData ItemData;
+ public readonly DictBonusItems DictBonusItems;
public readonly RestrictedGear RestrictedGear;
public readonly EquipItem DefaultSword;
public ItemManager(Configuration config, IDataManager gameData, ObjectIdentification objectIdentification,
- ItemData itemData, DictStain stains, RestrictedGear restrictedGear)
+ ItemData itemData, DictStain stains, RestrictedGear restrictedGear, DictBonusItems dictBonusItems)
{
_config = config;
ItemSheet = gameData.GetExcelSheet()!;
@@ -33,6 +34,7 @@ public class ItemManager
ItemData = itemData;
Stains = stains;
RestrictedGear = restrictedGear;
+ DictBonusItems = dictBonusItems;
DefaultSword = EquipItem.FromMainhand(ItemSheet.GetRow(1601)!); // Weathered Shortsword
}
@@ -124,6 +126,22 @@ public class ItemManager
}
}
+ public BonusItem Identify(BonusItemFlag slot, PrimaryId id, Variant variant)
+ {
+ var index = slot.ToIndex();
+ if (index == uint.MaxValue)
+ return new BonusItem($"Invalid ({id.Id}-{variant})", 0, 0, id, variant, slot);
+
+ if (id.Id == 0)
+ return BonusItem.Empty(slot);
+
+ return ObjectIdentification.Identify(id, variant, slot)
+ .FirstOrDefault(new BonusItem($"Invalid ({id.Id}-{variant})", 0, 0, id, variant, slot));
+ }
+
+ public BonusItem Resolve(BonusItemFlag slot, BonusItemId id)
+ => IsBonusItemValid(slot, id, out var item) ? item : new BonusItem($"Invalid ({id.Id})", 0, id, 0, 0, slot);
+
/// Return the default offhand for a given mainhand, that is for both handed weapons, return the correct offhand part, and for everything else Nothing.
public EquipItem GetDefaultOffhand(EquipItem mainhand)
{
@@ -161,6 +179,18 @@ public class ItemManager
return item.Valid;
}
+ /// Returns whether a bonus item id represents a valid item for a slot and gives the item.
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ public bool IsBonusItemValid(BonusItemFlag slot, BonusItemId itemId, out BonusItem item)
+ {
+ if (itemId.Id != 0)
+ return DictBonusItems.TryGetValue(itemId, out item) && slot == item.Slot;
+
+ item = BonusItem.Empty(slot);
+ return true;
+ }
+
+
///
/// Check whether an item id resolves to an existing item of the correct slot (which should not be weapons.)
/// The returned item is either the resolved correct item, or the Nothing item for that slot.
diff --git a/Glamourer/State/InternalStateEditor.cs b/Glamourer/State/InternalStateEditor.cs
index 17072e7..71af0aa 100644
--- a/Glamourer/State/InternalStateEditor.cs
+++ b/Glamourer/State/InternalStateEditor.cs
@@ -151,6 +151,18 @@ public class InternalStateEditor(
return true;
}
+ /// Change a single bonus item.
+ public bool ChangeBonusItem(ActorState state, BonusItemFlag slot, BonusItem item, StateSource source, out BonusItem oldItem, uint key = 0)
+ {
+ oldItem = state.ModelData.BonusItem(slot);
+ if (!state.CanUnlock(key))
+ return false;
+
+ state.ModelData.SetBonusItem(slot, item);
+ state.Sources[slot] = source;
+ return true;
+ }
+
/// Change a single piece of equipment including stain.
public bool ChangeEquip(ActorState state, EquipSlot slot, EquipItem item, StainIds stains, StateSource source, out EquipItem oldItem,
out StainIds oldStains, uint key = 0)
diff --git a/Glamourer/State/StateApplier.cs b/Glamourer/State/StateApplier.cs
index 5a6c21e..a4a1df4 100644
--- a/Glamourer/State/StateApplier.cs
+++ b/Glamourer/State/StateApplier.cs
@@ -125,6 +125,33 @@ public class StateApplier(
return data;
}
+ public void ChangeBonusItem(ActorData data, BonusItemFlag slot, PrimaryId id, Variant variant)
+ {
+ var item = new CharacterArmor(id, variant, StainIds.None);
+ foreach (var actor in data.Objects.Where(a => a.IsCharacter))
+ {
+ var mdl = actor.Model;
+ if (!mdl.IsHuman)
+ continue;
+
+ _updateSlot.UpdateBonusSlot(actor.Model, slot, item);
+ }
+ }
+
+ ///
+ public ActorData ChangeBonusItem(ActorState state, BonusItemFlag slot, bool apply)
+ {
+ // If the source is not IPC we do not want to apply restrictions.
+ var data = GetData(state);
+ if (apply)
+ {
+ var item = state.ModelData.BonusItem(slot);
+ ChangeBonusItem(data, slot, item.ModelId, item.Variant);
+ }
+
+ return data;
+ }
+
///
/// Change the stain of a single piece of armor or weapon.
diff --git a/Glamourer/State/StateEditor.cs b/Glamourer/State/StateEditor.cs
index dccb283..c39fd31 100644
--- a/Glamourer/State/StateEditor.cs
+++ b/Glamourer/State/StateEditor.cs
@@ -89,6 +89,18 @@ public class StateEditor(
StateChanged.Invoke(type, settings.Source, state, actors, (old, item, slot));
}
+ public void ChangeBonusItem(object data, BonusItemFlag slot, BonusItem item, ApplySettings settings = default)
+ {
+ var state = (ActorState)data;
+ if (!Editor.ChangeBonusItem(state, slot, item, settings.Source, out var old, settings.Key))
+ return;
+
+ var actors = Applier.ChangeBonusItem(state, slot, settings.Source.RequiresChange());
+ Glamourer.Log.Verbose(
+ $"Set {slot.ToName()} in state {state.Identifier.Incognito(null)} from {old.Name} ({old.Id}) to {item.Name} ({item.Id}). [Affecting {actors.ToLazyString("nothing")}.]");
+ StateChanged.Invoke(StateChangeType.BonusItem, settings.Source, state, actors, (old, item, slot));
+ }
+
///
public void ChangeEquip(object data, EquipSlot slot, EquipItem? item, StainIds? stains, ApplySettings settings)
{
@@ -226,7 +238,7 @@ public class StateEditor(
out _, settings.Key);
}
- var customizeFlags = mergedDesign.Design.ApplyCustomizeRaw;
+ var customizeFlags = mergedDesign.Design.Application.Customize;
if (mergedDesign.Design.DoApplyCustomize(CustomizeIndex.Clan))
customizeFlags |= CustomizeFlag.Race;
@@ -245,7 +257,7 @@ public class StateEditor(
state.Sources[parameter] = StateSource.Game;
}
- foreach (var parameter in mergedDesign.Design.ApplyParameters.Iterate())
+ foreach (var parameter in mergedDesign.Design.Application.Parameters.Iterate())
{
if (settings.RespectManual && state.Sources[parameter].IsManual())
continue;
@@ -273,6 +285,13 @@ public class StateEditor(
Source(slot.ToState(true)), out _, settings.Key);
}
+ foreach (var slot in BonusExtensions.AllFlags)
+ {
+ if (mergedDesign.Design.DoApplyBonusItem(slot))
+ if (!settings.RespectManual || !state.Sources[slot].IsManual())
+ Editor.ChangeBonusItem(state, slot, mergedDesign.Design.DesignData.BonusItem(slot), Source(slot), out _, settings.Key);
+ }
+
foreach (var weaponSlot in EquipSlotExtensions.WeaponSlots)
{
if (mergedDesign.Design.DoApplyStain(weaponSlot))
diff --git a/Glamourer/State/StateIndex.cs b/Glamourer/State/StateIndex.cs
index 28cc722..a569499 100644
--- a/Glamourer/State/StateIndex.cs
+++ b/Glamourer/State/StateIndex.cs
@@ -38,6 +38,13 @@ public readonly record struct StateIndex(int Value) : IEqualityOperators Invalid,
};
+ public static implicit operator StateIndex(BonusItemFlag flag)
+ => flag switch
+ {
+ BonusItemFlag.Glasses => new StateIndex(BonusItemGlasses),
+ _ => Invalid,
+ };
+
public static implicit operator StateIndex(CustomizeIndex index)
=> index switch
{
@@ -198,23 +205,13 @@ public readonly record struct StateIndex(int Value) : IEqualityOperators All
=> Enumerable.Range(0, Size - 1).Select(i => new StateIndex(i));
- public bool GetApply(DesignBase data)
- => GetFlag() switch
- {
- EquipFlag e => data.ApplyEquip.HasFlag(e),
- CustomizeFlag c => data.ApplyCustomize.HasFlag(c),
- MetaFlag m => data.ApplyMeta.HasFlag(m),
- CrestFlag c => data.ApplyCrest.HasFlag(c),
- CustomizeParameterFlag c => data.ApplyParameters.HasFlag(c),
- bool v => v,
- _ => false,
- };
-
public string ToName()
=> GetFlag() switch
{
@@ -223,6 +220,7 @@ public readonly record struct StateIndex(int Value) : IEqualityOperators m.ToIndex().ToName(),
CrestFlag c => c.ToLabel(),
CustomizeParameterFlag c => c.ToName(),
+ BonusItemFlag b => b.ToName(),
bool v => "Model ID",
_ => "Unknown",
};
@@ -317,6 +315,8 @@ public readonly record struct StateIndex(int Value) : IEqualityOperators CustomizeParameterFlag.FacePaintUvOffset,
ParamDecalColor => CustomizeParameterFlag.DecalColor,
+ BonusItemGlasses => BonusItemFlag.Glasses,
+
_ => -1,
};
@@ -411,6 +411,8 @@ public readonly record struct StateIndex(int Value) : IEqualityOperators data.Parameters[CustomizeParameterFlag.FacePaintUvOffset],
ParamDecalColor => data.Parameters[CustomizeParameterFlag.DecalColor],
+ BonusItemGlasses => data.BonusItem(BonusItemFlag.Glasses),
+
_ => null,
};
}
diff --git a/Glamourer/State/StateListener.cs b/Glamourer/State/StateListener.cs
index 72b3122..f82b2fc 100644
--- a/Glamourer/State/StateListener.cs
+++ b/Glamourer/State/StateListener.cs
@@ -32,7 +32,8 @@ public class StateListener : IDisposable
private readonly ItemManager _items;
private readonly CustomizeService _customizations;
private readonly PenumbraService _penumbra;
- private readonly EquipSlotUpdating _equipSlotUpdating;
+ private readonly EquipSlotUpdating _equipSlotUpdating;
+ private readonly BonusSlotUpdating _bonusSlotUpdating;
private readonly WeaponLoading _weaponLoading;
private readonly HeadGearVisibilityChanged _headGearVisibility;
private readonly VisorStateChanged _visorState;
@@ -52,17 +53,18 @@ public class StateListener : IDisposable
private CharacterWeapon _lastFistOffhand = CharacterWeapon.Empty;
public StateListener(StateManager manager, ItemManager items, PenumbraService penumbra, ActorManager actors, Configuration config,
- EquipSlotUpdating equipSlotUpdating, WeaponLoading weaponLoading, VisorStateChanged visorState, WeaponVisibilityChanged weaponVisibility,
- HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier, FunModule funModule, HumanModelList humans,
- StateApplier applier, MovedEquipment movedEquipment, ObjectManager objects, GPoseService gPose,
- ChangeCustomizeService changeCustomizeService, CustomizeService customizations, ICondition condition, CrestService crestService)
+ EquipSlotUpdating equipSlotUpdating, WeaponLoading weaponLoading, VisorStateChanged visorState,
+ WeaponVisibilityChanged weaponVisibility, HeadGearVisibilityChanged headGearVisibility, AutoDesignApplier autoDesignApplier,
+ FunModule funModule, HumanModelList humans, StateApplier applier, MovedEquipment movedEquipment, ObjectManager objects,
+ GPoseService gPose, ChangeCustomizeService changeCustomizeService, CustomizeService customizations, ICondition condition,
+ CrestService crestService, BonusSlotUpdating bonusSlotUpdating)
{
_manager = manager;
_items = items;
_penumbra = penumbra;
_actors = actors;
_config = config;
- _equipSlotUpdating = equipSlotUpdating;
+ _equipSlotUpdating = equipSlotUpdating;
_weaponLoading = weaponLoading;
_visorState = visorState;
_weaponVisibility = weaponVisibility;
@@ -78,6 +80,7 @@ public class StateListener : IDisposable
_customizations = customizations;
_condition = condition;
_crestService = crestService;
+ _bonusSlotUpdating = bonusSlotUpdating;
Subscribe();
}
@@ -227,6 +230,35 @@ public class StateListener : IDisposable
(_, armor) = _items.RestrictedGear.ResolveRestricted(armor, slot, customize.Race, customize.Gender);
}
+ private void OnBonusSlotUpdating(Model model, BonusItemFlag slot, ref CharacterArmor item, ref ulong returnValue)
+ {
+ var actor = _penumbra.GameObjectFromDrawObject(model);
+ if (_condition[ConditionFlag.CreatingCharacter] && actor.Index >= ObjectIndex.CutsceneStart)
+ return;
+
+ if (actor.Identifier(_actors, out var identifier)
+ && _manager.TryGetValue(identifier, out var state))
+ switch (UpdateBaseData(actor, state, slot, item))
+ {
+ // Base data changed equipment while actors were not there.
+ // Update model state if not on fixed design.
+ case UpdateState.Change:
+ var apply = false;
+ if (!state.Sources[slot].IsFixed())
+ _manager.ChangeBonusItem(state, slot, state.BaseData.BonusItem(slot), ApplySettings.Game);
+ else
+ apply = true;
+ if (apply)
+ item = state.ModelData.BonusItem(slot).ToArmor();
+ break;
+ // Use current model data.
+ case UpdateState.NoChange:
+ item = state.ModelData.BonusItem(slot).ToArmor();
+ break;
+ case UpdateState.Transformed: break;
+ }
+ }
+
private void OnMovedEquipment((EquipSlot, uint, StainIds)[] items)
{
_objects.Update();
@@ -403,6 +435,28 @@ public class StateListener : IDisposable
}
}
+ private UpdateState UpdateBaseData(Actor actor, ActorState state, BonusItemFlag slot, CharacterArmor item)
+ {
+ var actorItemId = actor.GetBonusItem(slot);
+ if (!_items.IsBonusItemValid(slot, actorItemId, out var actorItem))
+ return UpdateState.NoChange;
+
+ // The actor item does not correspond to the model item, thus the actor is transformed.
+ if (actorItem.ModelId != item.Set || actorItem.Variant != item.Variant)
+ return UpdateState.Transformed;
+
+ var baseData = state.BaseData.BonusItem(slot);
+ var change = UpdateState.NoChange;
+ if (baseData.Id != actorItem.Id || baseData.ModelId != item.Set || baseData.Variant != item.Variant)
+ {
+ var identified = _items.Identify(slot, item.Set, item.Variant);
+ state.BaseData.SetBonusItem(slot, identified);
+ change = UpdateState.Change;
+ }
+
+ return change;
+ }
+
/// Handle a full equip slot update for base data and model data.
private void HandleEquipSlot(Actor actor, ActorState state, EquipSlot slot, ref CharacterArmor armor)
{
@@ -700,6 +754,7 @@ public class StateListener : IDisposable
_penumbra.CreatingCharacterBase += OnCreatingCharacterBase;
_penumbra.CreatedCharacterBase += OnCreatedCharacterBase;
_equipSlotUpdating.Subscribe(OnEquipSlotUpdating, EquipSlotUpdating.Priority.StateListener);
+ _bonusSlotUpdating.Subscribe(OnBonusSlotUpdating, BonusSlotUpdating.Priority.StateListener);
_movedEquipment.Subscribe(OnMovedEquipment, MovedEquipment.Priority.StateListener);
_weaponLoading.Subscribe(OnWeaponLoading, WeaponLoading.Priority.StateListener);
_visorState.Subscribe(OnVisorChange, VisorStateChanged.Priority.StateListener);
@@ -716,6 +771,7 @@ public class StateListener : IDisposable
_penumbra.CreatingCharacterBase -= OnCreatingCharacterBase;
_penumbra.CreatedCharacterBase -= OnCreatedCharacterBase;
_equipSlotUpdating.Unsubscribe(OnEquipSlotUpdating);
+ _bonusSlotUpdating.Unsubscribe(OnBonusSlotUpdating);
_movedEquipment.Unsubscribe(OnMovedEquipment);
_weaponLoading.Unsubscribe(OnWeaponLoading);
_visorState.Unsubscribe(OnVisorChange);
diff --git a/Glamourer/State/StateManager.cs b/Glamourer/State/StateManager.cs
index 92cf6e5..5033577 100644
--- a/Glamourer/State/StateManager.cs
+++ b/Glamourer/State/StateManager.cs
@@ -160,6 +160,13 @@ public sealed class StateManager(
foreach (var slot in CrestExtensions.AllRelevantSet)
ret.SetCrest(slot, CrestService.GetModelCrest(actor, slot));
+
+ foreach (var slot in BonusExtensions.AllFlags)
+ {
+ var data = model.GetBonus(slot);
+ var item = Items.Identify(slot, data.Set, data.Variant);
+ ret.SetBonusItem(slot, item);
+ }
}
else
{
@@ -181,6 +188,13 @@ public sealed class StateManager(
foreach (var slot in CrestExtensions.AllRelevantSet)
ret.SetCrest(slot, actor.GetCrest(slot));
+
+ foreach (var slot in BonusExtensions.AllFlags)
+ {
+ var id = actor.GetBonusItem(slot);
+ var item = Items.Resolve(slot, id);
+ ret.SetBonusItem(slot, item);
+ }
}
// Set the weapons regardless of source.
@@ -241,6 +255,9 @@ public sealed class StateManager(
state.Sources[slot, false] = StateSource.Game;
}
+ foreach (var slot in BonusExtensions.AllFlags)
+ state.Sources[slot] = StateSource.Game;
+
foreach (var type in Enum.GetValues())
state.Sources[type] = StateSource.Game;
@@ -328,6 +345,12 @@ public sealed class StateManager(
state.ModelData.IsHatVisible());
}
+ foreach (var slot in BonusExtensions.AllFlags)
+ {
+ var item = state.ModelData.BonusItem(slot);
+ Applier.ChangeBonusItem(actors, slot, item.ModelId, item.Variant);
+ }
+
var mainhandActors = state.ModelData.MainhandType != state.BaseData.MainhandType ? actors.OnlyGPose() : actors;
Applier.ChangeMainhand(mainhandActors, state.ModelData.Item(EquipSlot.MainHand), state.ModelData.Stain(EquipSlot.MainHand));
var offhandActors = state.ModelData.OffhandType != state.BaseData.OffhandType ? actors.OnlyGPose() : actors;
@@ -364,6 +387,15 @@ public sealed class StateManager(
}
}
+ foreach (var slot in BonusExtensions.AllFlags)
+ {
+ if (state.Sources[slot] is StateSource.Fixed)
+ {
+ state.Sources[slot] = StateSource.Game;
+ state.ModelData.SetBonusItem(slot, state.BaseData.BonusItem(slot));
+ }
+ }
+
foreach (var slot in CrestExtensions.AllRelevantSet)
{
if (state.Sources[slot] is StateSource.Fixed)
diff --git a/Glamourer/Unlocks/FavoriteManager.cs b/Glamourer/Unlocks/FavoriteManager.cs
index de22ea8..f4576c6 100644
--- a/Glamourer/Unlocks/FavoriteManager.cs
+++ b/Glamourer/Unlocks/FavoriteManager.cs
@@ -25,6 +25,7 @@ public class FavoriteManager : ISavable
private readonly HashSet _favorites = [];
private readonly HashSet _favoriteColors = [];
private readonly HashSet _favoriteHairStyles = [];
+ private readonly HashSet _favoriteBonusItems = [];
public FavoriteManager(SaveService saveService)
{
@@ -62,6 +63,7 @@ public class FavoriteManager : ISavable
_favorites.UnionWith(load!.FavoriteItems.Select(i => (ItemId)i));
_favoriteColors.UnionWith(load!.FavoriteColors.Select(i => (StainId)i));
_favoriteHairStyles.UnionWith(load!.FavoriteHairStyles.Select(t => new FavoriteHairStyle(t)));
+ _favoriteBonusItems.UnionWith(load!.FavoriteBonusItems.Select(b => new BonusItemId(b)));
break;
default: throw new Exception($"Unknown Version {load?.Version ?? 0}");
@@ -109,6 +111,11 @@ public class FavoriteManager : ISavable
foreach (var hairStyle in _favoriteHairStyles)
j.WriteValue(hairStyle.ToValue());
j.WriteEndArray();
+ j.WriteStartArray();
+ j.WritePropertyName(nameof(LoadIntermediary.FavoriteBonusItems));
+ foreach (var item in _favoriteBonusItems)
+ j.WriteValue(item.Id);
+ j.WriteEndArray();
j.WriteEndObject();
}
@@ -124,9 +131,6 @@ public class FavoriteManager : ISavable
return true;
}
- public bool TryAdd(Stain stain)
- => TryAdd(stain.RowIndex);
-
public bool TryAdd(StainId stain)
{
if (stain.Id == 0 || !_favoriteColors.Add(stain))
@@ -136,6 +140,15 @@ public class FavoriteManager : ISavable
return true;
}
+ public bool TryAdd(BonusItem bonusItem)
+ {
+ if (bonusItem.Id == 0 || !_favoriteBonusItems.Add(bonusItem.Id))
+ return false;
+
+ Save();
+ return true;
+ }
+
public bool TryAdd(Gender gender, SubRace race, CustomizeIndex type, CustomizeValue value)
{
if (!TypeAllowed(type) || !_favoriteHairStyles.Add(new FavoriteHairStyle(gender, race, type, value)))
@@ -157,9 +170,6 @@ public class FavoriteManager : ISavable
return true;
}
- public bool Remove(Stain stain)
- => Remove(stain.RowIndex);
-
public bool Remove(StainId stain)
{
if (!_favoriteColors.Remove(stain))
@@ -169,6 +179,15 @@ public class FavoriteManager : ISavable
return true;
}
+ public bool Remove(BonusItem bonusItem)
+ {
+ if (!_favoriteBonusItems.Remove(bonusItem.Id))
+ return false;
+
+ Save();
+ return true;
+ }
+
public bool Remove(Gender gender, SubRace race, CustomizeIndex type, CustomizeValue value)
{
if (!_favoriteHairStyles.Remove(new FavoriteHairStyle(gender, race, type, value)))
@@ -181,23 +200,21 @@ public class FavoriteManager : ISavable
public bool Contains(EquipItem item)
=> _favorites.Contains(item.ItemId);
- public bool Contains(Stain stain)
- => _favoriteColors.Contains(stain.RowIndex);
-
- public bool Contains(ItemId item)
- => _favorites.Contains(item);
-
public bool Contains(StainId stain)
=> _favoriteColors.Contains(stain);
+ public bool Contains(BonusItem bonusItem)
+ => _favoriteBonusItems.Contains(bonusItem.Id);
+
public bool Contains(Gender gender, SubRace race, CustomizeIndex type, CustomizeValue value)
=> _favoriteHairStyles.Contains(new FavoriteHairStyle(gender, race, type, value));
private class LoadIntermediary
{
- public int Version = CurrentVersion;
- public uint[] FavoriteItems = [];
- public byte[] FavoriteColors = [];
- public uint[] FavoriteHairStyles = [];
+ public int Version = CurrentVersion;
+ public uint[] FavoriteItems = [];
+ public byte[] FavoriteColors = [];
+ public uint[] FavoriteHairStyles = [];
+ public ushort[] FavoriteBonusItems = [];
}
}
diff --git a/Penumbra.GameData b/Penumbra.GameData
index d83303c..8928015 160000
--- a/Penumbra.GameData
+++ b/Penumbra.GameData
@@ -1 +1 @@
-Subproject commit d83303ccc3ec5d7237f5da621e9c2433ad28f9e1
+Subproject commit 8928015f38f951810a9a6fbb44fb4a0cb9a712dd