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