diff --git a/OtterGui b/OtterGui index 5028fba7..462acb87 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 5028fba767ca8febd75a1a5ebc312bd354efc81b +Subproject commit 462acb87099650019996e4306d18cc70f76ca576 diff --git a/Penumbra.GameData b/Penumbra.GameData index 595ac572..5fa4d0e7 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 595ac5722c9c400bea36110503ed2ae7b02d1489 +Subproject commit 5fa4d0e7972423b73f8cf569bb2bfbeddd825c8a diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs index 21d8abe0..671d684f 100644 --- a/Penumbra/Mods/Groups/ImcModGroup.cs +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -14,8 +14,7 @@ namespace Penumbra.Mods.Groups; public class ImcModGroup(Mod mod) : IModGroup { - public const int DisabledIndex = 30; - public const int NumAttributes = 10; + public const int DisabledIndex = 60; public Mod Mod { get; } = mod; public string Name { get; set; } = "Option"; @@ -55,23 +54,20 @@ public class ImcModGroup(Mod mod) : IModGroup } } + public bool DefaultDisabled + => _canBeDisabled && DefaultSettings.HasFlag(DisabledIndex); + public IModOption? AddOption(string name, string description = "") { - uint fullMask = GetFullMask(); - var firstUnset = (byte)BitOperations.TrailingZeroCount(~fullMask); - // All attributes handled. - if (firstUnset >= NumAttributes) - return null; - var groupIdx = Mod.Groups.IndexOf(this); if (groupIdx < 0) return null; var subMod = new ImcSubMod(this) { - Name = name, - Description = description, - AttributeIndex = firstUnset, + Name = name, + Description = description, + AttributeMask = 0, }; OptionData.Add(subMod); return subMod; @@ -100,7 +96,7 @@ public class ImcModGroup(Mod mod) : IModGroup continue; var option = OptionData[i]; - mask |= option.Attribute; + mask |= option.AttributeMask; } return mask; @@ -109,11 +105,10 @@ public class ImcModGroup(Mod mod) : IModGroup private ushort GetFullMask() => GetCurrentMask(Setting.AllBits(63)); - private ImcManipulation GetManip(ushort mask) + public ImcManipulation GetManip(ushort mask) => new(ObjectType, BodySlot, PrimaryId, SecondaryId.Id, Variant.Id, EquipSlot, DefaultEntry with { AttributeMask = mask }); - public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) { if (CanBeDisabled && setting.HasFlag(DisabledIndex)) @@ -150,8 +145,8 @@ public class ImcModGroup(Mod mod) : IModGroup { jWriter.WriteStartObject(); SubMod.WriteModOption(jWriter, option); - jWriter.WritePropertyName(nameof(option.AttributeIndex)); - jWriter.WriteValue(option.AttributeIndex); + jWriter.WritePropertyName(nameof(option.AttributeMask)); + jWriter.WriteValue(option.AttributeMask); jWriter.WriteEndObject(); } diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 59c88cf0..c6a723a0 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -202,6 +202,9 @@ public class ModCacheManager : IDisposable foreach (var manip in mod.AllDataContainers.SelectMany(m => m.Manipulations)) ComputeChangedItems(_identifier, changedItems, manip); + foreach(var imcGroup in mod.Groups.OfType()) + ComputeChangedItems(_identifier, changedItems, imcGroup.GetManip(0)); + mod.LowerChangedItemsString = string.Join("\0", mod.ChangedItems.Keys.Select(k => k.ToLowerInvariant())); } diff --git a/Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs b/Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs new file mode 100644 index 00000000..e1235c5b --- /dev/null +++ b/Penumbra/Mods/Manager/OptionEditor/ImcAttributeCache.cs @@ -0,0 +1,123 @@ +using OtterGui; +using Penumbra.GameData.Structs; +using Penumbra.Mods.Groups; +using Penumbra.Mods.SubMods; + +namespace Penumbra.Mods.Manager.OptionEditor; + +public unsafe ref struct ImcAttributeCache +{ + private fixed bool _canChange[ImcEntry.NumAttributes]; + private fixed byte _option[ImcEntry.NumAttributes]; + + /// Obtain the earliest unset flag, or 0 if none are unset. + public readonly ushort LowestUnsetMask; + + public ImcAttributeCache(ImcModGroup group) + { + for (var i = 0; i < ImcEntry.NumAttributes; ++i) + { + _canChange[i] = true; + _option[i] = byte.MaxValue; + + var flag = (ushort)(1 << i); + var set = (group.DefaultEntry.AttributeMask & flag) != 0; + if (set) + { + _canChange[i] = true; + _option[i] = byte.MaxValue - 1; + continue; + } + + foreach (var (option, idx) in group.OptionData.WithIndex()) + { + set = (option.AttributeMask & flag) != 0; + if (set) + { + _canChange[i] = option.AttributeMask != flag; + _option[i] = (byte)idx; + break; + } + } + + if (_option[i] == byte.MaxValue && LowestUnsetMask is 0) + LowestUnsetMask = flag; + } + } + + + /// Checks whether an attribute flag can be set by anything, i.e. if it might be the only flag for an option and thus could not be removed from that option. + public readonly bool CanChange(int idx) + => _canChange[idx]; + + /// Set a default attribute flag to a value if possible, remove it from its prior option if necessary, and return if anything changed. + public readonly bool Set(ImcModGroup group, int idx, bool value) + { + var flag = 1 << idx; + var oldMask = group.DefaultEntry.AttributeMask; + if (!value) + { + var newMask = (ushort)(oldMask & ~flag); + if (oldMask == newMask) + return false; + + group.DefaultEntry = group.DefaultEntry with { AttributeMask = newMask }; + return true; + } + + if (!_canChange[idx]) + return false; + + var mask = (ushort)(oldMask | flag); + if (oldMask == mask) + return false; + + group.DefaultEntry = group.DefaultEntry with { AttributeMask = mask }; + if (_option[idx] <= ImcEntry.NumAttributes) + { + var option = group.OptionData[_option[idx]]; + option.AttributeMask = (ushort)(option.AttributeMask & ~flag); + } + + return true; + } + + /// Set an attribute flag to a value if possible, remove it from its prior option or the default entry if necessary, and return if anything changed. + public readonly bool Set(ImcSubMod option, int idx, bool value) + { + if (!_canChange[idx]) + return false; + + var flag = 1 << idx; + var oldMask = option.AttributeMask; + if (!value) + { + var newMask = (ushort)(oldMask & ~flag); + if (oldMask == newMask) + return false; + + option.AttributeMask = newMask; + return true; + } + + var mask = (ushort)(oldMask | flag); + if (oldMask == mask) + return false; + + option.AttributeMask = mask; + if (_option[idx] <= ImcEntry.NumAttributes) + { + var oldOption = option.Group.OptionData[_option[idx]]; + oldOption.AttributeMask = (ushort)(oldOption.AttributeMask & ~flag); + } + else if (_option[idx] is byte.MaxValue - 1) + { + option.Group.DefaultEntry = option.Group.DefaultEntry with + { + AttributeMask = (ushort)(option.Group.DefaultEntry.AttributeMask & ~flag), + }; + } + + return true; + } +} diff --git a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs index f71547ba..20021d29 100644 --- a/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs +++ b/Penumbra/Mods/Manager/OptionEditor/ImcModGroupEditor.cs @@ -1,11 +1,13 @@ using OtterGui.Classes; using OtterGui.Filesystem; using OtterGui.Services; +using Penumbra.GameData.Structs; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; +using static FFXIVClientStructs.FFXIV.Client.UI.Misc.ConfigModule; namespace Penumbra.Mods.Manager.OptionEditor; @@ -26,6 +28,67 @@ public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveServ return group; } + public ImcSubMod? AddOption(ImcModGroup group, in ImcAttributeCache cache, string name, string description = "", + SaveType saveType = SaveType.Queue) + { + if (cache.LowestUnsetMask == 0) + return null; + + var subMod = new ImcSubMod(group) + { + Name = name, + Description = description, + AttributeMask = cache.LowestUnsetMask, + }; + group.OptionData.Add(subMod); + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, group.Mod, group, subMod, null, -1); + return subMod; + } + + // Hide this method. + private new ImcSubMod? AddOption(ImcModGroup group, string name, SaveType saveType) + => null; + + public void ChangeDefaultAttribute(ImcModGroup group, in ImcAttributeCache cache, int idx, bool value, SaveType saveType = SaveType.Queue) + { + if (!cache.Set(group, idx, value)) + return; + + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, group.Mod, group, null, null, -1); + } + + public void ChangeDefaultEntry(ImcModGroup group, in ImcEntry newEntry, SaveType saveType = SaveType.Queue) + { + var entry = newEntry with { AttributeMask = group.DefaultEntry.AttributeMask }; + if (entry.MaterialId == 0 || group.DefaultEntry.Equals(entry)) + return; + + group.DefaultEntry = entry; + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, group.Mod, group, null, null, -1); + } + + public void ChangeOptionAttribute(ImcSubMod option, in ImcAttributeCache cache, int idx, bool value, SaveType saveType = SaveType.Queue) + { + if (!cache.Set(option, idx, value)) + return; + + SaveService.Save(saveType, new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, option.Mod, option.Group, option, null, -1); + } + + public void ChangeCanBeDisabled(ImcModGroup group, bool canBeDisabled, SaveType saveType = SaveType.Queue) + { + if (group.CanBeDisabled == canBeDisabled) + return; + + group.CanBeDisabled = canBeDisabled; + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, group.Mod, group, null, null, -1); + } + protected override ImcModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) => new(mod) { diff --git a/Penumbra/Mods/SubMods/ImcSubMod.cs b/Penumbra/Mods/SubMods/ImcSubMod.cs index fca817aa..7f46bc95 100644 --- a/Penumbra/Mods/SubMods/ImcSubMod.cs +++ b/Penumbra/Mods/SubMods/ImcSubMod.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json.Linq; +using Penumbra.GameData.Structs; using Penumbra.Mods.Groups; namespace Penumbra.Mods.SubMods; @@ -11,15 +12,13 @@ public class ImcSubMod(ImcModGroup group) : IModOption : this(group) { SubMod.LoadOptionData(json, this); + AttributeMask = (ushort)((json[nameof(AttributeMask)]?.ToObject() ?? 0) & ImcEntry.AttributesMask); } public Mod Mod => Group.Mod; - public byte AttributeIndex; - - public ushort Attribute - => (ushort)(1 << AttributeIndex); + public ushort AttributeMask; Mod IModOption.Mod => Mod; diff --git a/Penumbra/UI/ModsTab/AddGroupDrawer.cs b/Penumbra/UI/ModsTab/AddGroupDrawer.cs new file mode 100644 index 00000000..79c1bf9d --- /dev/null +++ b/Penumbra/UI/ModsTab/AddGroupDrawer.cs @@ -0,0 +1,161 @@ +using ImGuiNET; +using OtterGui.Services; +using OtterGui.Text; +using Penumbra.Api.Enums; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; +using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager.OptionEditor; +using Penumbra.UI.Classes; + +namespace Penumbra.UI.ModsTab; + +public class AddGroupDrawer : IUiService +{ + private string _groupName = string.Empty; + private bool _groupNameValid = false; + + private ImcManipulation _imcManip = new(EquipSlot.Head, 1, 1, new ImcEntry()); + private ImcEntry _defaultEntry; + private bool _imcFileExists; + private bool _entryExists; + private bool _entryInvalid; + private readonly MetaFileManager _metaManager; + private readonly ModManager _modManager; + + public AddGroupDrawer(MetaFileManager metaManager, ModManager modManager) + { + _metaManager = metaManager; + _modManager = modManager; + UpdateEntry(); + } + + public void Draw(Mod mod, float width) + { + var buttonWidth = new Vector2((width - ImUtf8.ItemInnerSpacing.X) / 2, 0); + DrawBasicGroups(mod, width, buttonWidth); + DrawImcData(mod, buttonWidth); + } + + private void DrawBasicGroups(Mod mod, float width, Vector2 buttonWidth) + { + ImGui.SetNextItemWidth(width); + if (ImUtf8.InputText("##name"u8, ref _groupName, "Enter New Name..."u8)) + _groupNameValid = ModGroupEditor.VerifyFileName(mod, null, _groupName, false); + + DrawSingleGroupButton(mod, buttonWidth); + ImUtf8.SameLineInner(); + DrawMultiGroupButton(mod, buttonWidth); + } + + private void DrawSingleGroupButton(Mod mod, Vector2 width) + { + if (!ImUtf8.ButtonEx("Add Single Group"u8, _groupNameValid + ? "Add a new single selection option group to this mod."u8 + : "Can not add a new group of this name."u8, + width, !_groupNameValid)) + return; + + _modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _groupName); + _groupName = string.Empty; + _groupNameValid = false; + } + + private void DrawMultiGroupButton(Mod mod, Vector2 width) + { + if (!ImUtf8.ButtonEx("Add Multi Group"u8, _groupNameValid + ? "Add a new multi selection option group to this mod."u8 + : "Can not add a new group of this name."u8, + width, !_groupNameValid)) + return; + + _modManager.OptionEditor.AddModGroup(mod, GroupType.Multi, _groupName); + _groupName = string.Empty; + _groupNameValid = false; + } + + private void DrawImcInput(float width) + { + var change = MetaManipulationDrawer.DrawObjectType(ref _imcManip, width); + ImUtf8.SameLineInner(); + change |= MetaManipulationDrawer.DrawPrimaryId(ref _imcManip, width); + if (_imcManip.ObjectType is ObjectType.Weapon or ObjectType.Monster) + { + change |= MetaManipulationDrawer.DrawSecondaryId(ref _imcManip, width); + ImUtf8.SameLineInner(); + change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, width); + } + else if (_imcManip.ObjectType is ObjectType.DemiHuman) + { + var quarterWidth = (width - ImUtf8.ItemInnerSpacing.X / ImUtf8.GlobalScale) / 2; + change |= MetaManipulationDrawer.DrawSecondaryId(ref _imcManip, width); + ImUtf8.SameLineInner(); + change |= MetaManipulationDrawer.DrawSlot(ref _imcManip, quarterWidth); + ImUtf8.SameLineInner(); + change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, quarterWidth); + } + else + { + change |= MetaManipulationDrawer.DrawSlot(ref _imcManip, width); + ImUtf8.SameLineInner(); + change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, width); + } + + if (change) + UpdateEntry(); + } + + private void DrawImcData(Mod mod, Vector2 width) + { + var halfWidth = width.X / ImUtf8.GlobalScale; + DrawImcInput(halfWidth); + DrawImcButton(mod, width); + } + + private void DrawImcButton(Mod mod, Vector2 width) + { + if (ImUtf8.ButtonEx("Add IMC Group"u8, !_groupNameValid + ? "Can not add a new group of this name."u8 + : _entryInvalid + ? "The associated IMC entry is invalid."u8 + : "Add a new multi selection option group to this mod."u8, + width, !_groupNameValid || _entryInvalid)) + { + _modManager.OptionEditor.ImcEditor.AddModGroup(mod, _groupName, _imcManip); + _groupName = string.Empty; + _groupNameValid = false; + } + + if (_entryInvalid) + { + ImUtf8.SameLineInner(); + var text = _imcFileExists + ? "IMC Entry Does Not Exist"u8 + : "IMC File Does Not Exist"u8; + ImUtf8.TextFramed(text, Colors.PressEnterWarningBg, width); + } + } + + private void UpdateEntry() + { + try + { + _defaultEntry = ImcFile.GetDefault(_metaManager, _imcManip.GamePath(), _imcManip.EquipSlot, _imcManip.Variant, + out _entryExists); + _imcFileExists = true; + } + catch (Exception) + { + _defaultEntry = new ImcEntry(); + _imcFileExists = false; + _entryExists = false; + } + + _imcManip = _imcManip.Copy(_entryExists ? _defaultEntry : new ImcEntry()); + _entryInvalid = !_imcManip.Validate(true); + } +} diff --git a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs index b7262f95..a94c25ea 100644 --- a/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs +++ b/Penumbra/UI/ModsTab/ModGroupEditDrawer.cs @@ -1,15 +1,12 @@ using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Interface.Utility; using ImGuiNET; -using Lumina.Data.Files; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Services; using OtterGui.Text; using OtterGui.Text.EndObjects; -using Penumbra.Api.Enums; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Meta; @@ -22,7 +19,6 @@ using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.UI.Classes; -using ImcFile = Penumbra.Meta.Files.ImcFile; namespace Penumbra.UI.ModsTab; @@ -104,8 +100,10 @@ public static class MetaManipulationDrawer return ret; } - // A number input for ids with a optional max id of given width. - // Returns true if newId changed against currentId. + /// + /// A number input for ids with an optional max id of given width. + /// Returns true if newId changed against currentId. + /// private static bool IdInput(ReadOnlySpan label, float unscaledWidth, ushort currentId, out ushort newId, int minId, int maxId, bool border) { @@ -121,142 +119,12 @@ public static class MetaManipulationDrawer } } -public class AddGroupDrawer : IUiService -{ - private string _groupName = string.Empty; - private bool _groupNameValid = false; - - private ImcManipulation _imcManip = new(EquipSlot.Head, 1, 1, new ImcEntry()); - private ImcEntry _defaultEntry; - private bool _imcFileExists; - private bool _entryExists; - private bool _entryInvalid; - private readonly MetaFileManager _metaManager; - private readonly ModManager _modManager; - - public AddGroupDrawer(MetaFileManager metaManager, ModManager modManager) - { - _metaManager = metaManager; - _modManager = modManager; - UpdateEntry(); - } - - public void Draw(Mod mod, float width) - { - DrawBasicGroups(mod, width); - DrawImcData(mod, width); - } - - private void UpdateEntry() - { - try - { - _defaultEntry = ImcFile.GetDefault(_metaManager, _imcManip.GamePath(), _imcManip.EquipSlot, _imcManip.Variant, - out _entryExists); - _imcFileExists = true; - } - catch (Exception) - { - _defaultEntry = new ImcEntry(); - _imcFileExists = false; - _entryExists = false; - } - - _imcManip = _imcManip.Copy(_entryExists ? _defaultEntry : new ImcEntry()); - _entryInvalid = !_imcManip.Validate(true); - } - - - private void DrawBasicGroups(Mod mod, float width) - { - ImGui.SetNextItemWidth(width); - if (ImUtf8.InputText("##name"u8, ref _groupName, "Enter New Name..."u8)) - _groupNameValid = ModGroupEditor.VerifyFileName(mod, null, _groupName, false); - - var buttonWidth = new Vector2((width - ImUtf8.ItemInnerSpacing.X) / 2, 0); - if (ImUtf8.ButtonEx("Add Single Group"u8, _groupNameValid - ? "Add a new single selection option group to this mod."u8 - : "Can not add a new group of this name."u8, - buttonWidth, !_groupNameValid)) - { - _modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _groupName); - _groupName = string.Empty; - _groupNameValid = false; - } - - ImUtf8.SameLineInner(); - if (ImUtf8.ButtonEx("Add Multi Group"u8, _groupNameValid - ? "Add a new multi selection option group to this mod."u8 - : "Can not add a new group of this name."u8, - buttonWidth, !_groupNameValid)) - { - _modManager.OptionEditor.AddModGroup(mod, GroupType.Multi, _groupName); - _groupName = string.Empty; - _groupNameValid = false; - } - } - - private void DrawImcData(Mod mod, float width) - { - var halfWidth = (width - ImUtf8.ItemInnerSpacing.X) / 2 / ImUtf8.GlobalScale; - var change = MetaManipulationDrawer.DrawObjectType(ref _imcManip, halfWidth); - ImUtf8.SameLineInner(); - change |= MetaManipulationDrawer.DrawPrimaryId(ref _imcManip, halfWidth); - if (_imcManip.ObjectType is ObjectType.Weapon or ObjectType.Monster) - { - change |= MetaManipulationDrawer.DrawSecondaryId(ref _imcManip, halfWidth); - ImUtf8.SameLineInner(); - change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, halfWidth); - } - else if (_imcManip.ObjectType is ObjectType.DemiHuman) - { - var quarterWidth = (halfWidth - ImUtf8.ItemInnerSpacing.X / ImUtf8.GlobalScale) / 2; - change |= MetaManipulationDrawer.DrawSecondaryId(ref _imcManip, halfWidth); - ImUtf8.SameLineInner(); - change |= MetaManipulationDrawer.DrawSlot(ref _imcManip, quarterWidth); - ImUtf8.SameLineInner(); - change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, quarterWidth); - } - else - { - change |= MetaManipulationDrawer.DrawSlot(ref _imcManip, halfWidth); - ImUtf8.SameLineInner(); - change |= MetaManipulationDrawer.DrawVariant(ref _imcManip, halfWidth); - } - - if (change) - UpdateEntry(); - - var buttonWidth = new Vector2(halfWidth * ImUtf8.GlobalScale, 0); - - if (ImUtf8.ButtonEx("Add IMC Group"u8, !_groupNameValid - ? "Can not add a new group of this name."u8 - : _entryInvalid ? - "The associated IMC entry is invalid."u8 - : "Add a new multi selection option group to this mod."u8, - buttonWidth, !_groupNameValid || _entryInvalid)) - { - _modManager.OptionEditor.ImcEditor.AddModGroup(mod, _groupName, _imcManip); - _groupName = string.Empty; - _groupNameValid = false; - } - - if (_entryInvalid) - { - ImUtf8.SameLineInner(); - var text = _imcFileExists - ? "IMC Entry Does Not Exist" - : "IMC File Does Not Exist"; - ImGuiUtil.DrawTextButton(text, buttonWidth, Colors.PressEnterWarningBg); - } - } -} - public sealed class ModGroupEditDrawer( ModManager modManager, Configuration config, FilenameService filenames, - DescriptionEditPopup descriptionPopup) : IUiService + DescriptionEditPopup descriptionPopup, + MetaFileManager metaManager) : IUiService { private static ReadOnlySpan DragDropLabel => "##DragOption"u8; @@ -501,7 +369,111 @@ public sealed class ModGroupEditDrawer( } private void DrawImcGroup(ImcModGroup group) - { } + { + using (ImUtf8.Group()) + { + ImUtf8.Text("Object Type"u8); + if (group.ObjectType is ObjectType.Equipment or ObjectType.Accessory or ObjectType.DemiHuman) + ImUtf8.Text("Slot"u8); + ImUtf8.Text("Primary ID"); + if (group.ObjectType is not ObjectType.Equipment and not ObjectType.Accessory) + ImUtf8.Text("Secondary ID"); + ImUtf8.Text("Variant"u8); + + ImUtf8.TextFrameAligned("Material ID"u8); + ImUtf8.TextFrameAligned("Material Animation ID"u8); + ImUtf8.TextFrameAligned("Decal ID"u8); + ImUtf8.TextFrameAligned("VFX ID"u8); + ImUtf8.TextFrameAligned("Sound ID"u8); + ImUtf8.TextFrameAligned("Can Be Disabled"u8); + ImUtf8.TextFrameAligned("Default Attributes"u8); + } + + ImGui.SameLine(); + + var attributeCache = new ImcAttributeCache(group); + + using (ImUtf8.Group()) + { + ImUtf8.Text(group.ObjectType.ToName()); + if (group.ObjectType is ObjectType.Equipment or ObjectType.Accessory or ObjectType.DemiHuman) + ImUtf8.Text(group.EquipSlot.ToName()); + ImUtf8.Text($"{group.PrimaryId.Id}"); + if (group.ObjectType is not ObjectType.Equipment and not ObjectType.Accessory) + ImUtf8.Text($"{group.SecondaryId.Id}"); + ImUtf8.Text($"{group.Variant.Id}"); + + ImUtf8.TextFrameAligned($"{group.DefaultEntry.MaterialId}"); + ImUtf8.TextFrameAligned($"{group.DefaultEntry.MaterialAnimationId}"); + ImUtf8.TextFrameAligned($"{group.DefaultEntry.DecalId}"); + ImUtf8.TextFrameAligned($"{group.DefaultEntry.VfxId}"); + ImUtf8.TextFrameAligned($"{group.DefaultEntry.SoundId}"); + + var canBeDisabled = group.CanBeDisabled; + if (ImUtf8.Checkbox("##disabled"u8, ref canBeDisabled)) + modManager.OptionEditor.ImcEditor.ChangeCanBeDisabled(group, canBeDisabled, SaveType.Queue); + + var defaultDisabled = group.DefaultDisabled; + ImUtf8.SameLineInner(); + if (ImUtf8.Checkbox("##defaultDisabled"u8, ref defaultDisabled)) + modManager.OptionEditor.ChangeModGroupDefaultOption(group, + group.DefaultSettings.SetBit(ImcModGroup.DisabledIndex, defaultDisabled)); + + DrawAttributes(modManager.OptionEditor.ImcEditor, attributeCache, group.DefaultEntry.AttributeMask, group); + } + + + foreach (var (option, optionIdx) in group.OptionData.WithIndex()) + { + using var id = ImRaii.PushId(optionIdx); + DrawOptionPosition(group, option, optionIdx); + + ImUtf8.SameLineInner(); + DrawOptionDefaultMultiBehaviour(group, option, optionIdx); + + ImUtf8.SameLineInner(); + DrawOptionName(option); + + ImUtf8.SameLineInner(); + DrawOptionDescription(option); + + ImUtf8.SameLineInner(); + DrawOptionDelete(option); + + ImUtf8.SameLineInner(); + ImGui.Dummy(new Vector2(_priorityWidth, 0)); + + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + _optionIdxSelectable.X + ImUtf8.ItemInnerSpacing.X * 2 + ImUtf8.FrameHeight); + DrawAttributes(modManager.OptionEditor.ImcEditor, attributeCache, option.AttributeMask, option); + } + + DrawNewOption(group, attributeCache); + return; + + static void DrawAttributes(ImcModGroupEditor editor, in ImcAttributeCache cache, ushort mask, object data) + { + for (var i = 0; i < ImcEntry.NumAttributes; ++i) + { + using var id = ImRaii.PushId(i); + var value = (mask & (1 << i)) != 0; + using (ImRaii.Disabled(!cache.CanChange(i))) + { + if (ImUtf8.Checkbox(TerminatedByteString.Empty, ref value)) + { + if (data is ImcModGroup g) + editor.ChangeDefaultAttribute(g, cache, i, value); + else + editor.ChangeOptionAttribute((ImcSubMod)data, cache, i, value); + } + } + + ImUtf8.HoverTooltip($"{(char)('A' + i)}"); + if (i != 9) + ImUtf8.SameLineInner(); + } + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void DrawOptionPosition(IModGroup group, IModOption option, int optionIdx) @@ -575,14 +547,14 @@ public sealed class ModGroupEditDrawer( if (count >= int.MaxValue) return; - DrawNewOptionBase(group, count); + var name = DrawNewOptionBase(group, count); - var validName = _newOptionName?.Length > 0; + var validName = name.Length > 0; if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName ? "Add a new option to this group."u8 : "Please enter a name for the new option."u8, !validName)) { - modManager.OptionEditor.SingleEditor.AddOption(group, _newOptionName!); + modManager.OptionEditor.SingleEditor.AddOption(group, name); _newOptionName = null; } } @@ -593,25 +565,36 @@ public sealed class ModGroupEditDrawer( if (count >= IModGroup.MaxMultiOptions) return; - DrawNewOptionBase(group, count); + var name = DrawNewOptionBase(group, count); - var validName = _newOptionName?.Length > 0; + var validName = name.Length > 0; if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName ? "Add a new option to this group."u8 : "Please enter a name for the new option."u8, !validName)) { - modManager.OptionEditor.MultiEditor.AddOption(group, _newOptionName!); + modManager.OptionEditor.MultiEditor.AddOption(group, name); _newOptionName = null; } } - private void DrawNewOption(ImcModGroup group) + private void DrawNewOption(ImcModGroup group, in ImcAttributeCache cache) { - // TODO + if (cache.LowestUnsetMask == 0) + return; + + var name = DrawNewOptionBase(group, group.Options.Count); + var validName = name.Length > 0; + if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName + ? "Add a new option to this group."u8 + : "Please enter a name for the new option."u8, !validName)) + { + modManager.OptionEditor.ImcEditor.AddOption(group, cache, name); + _newOptionName = null; + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void DrawNewOptionBase(IModGroup group, int count) + private string DrawNewOptionBase(IModGroup group, int count) { ImUtf8.Selectable($"Option #{count + 1}", false, size: _optionIdxSelectable); Target(group, count); @@ -631,6 +614,7 @@ public sealed class ModGroupEditDrawer( } ImUtf8.SameLineInner(); + return newName; } [MethodImpl(MethodImplOptions.AggressiveInlining)]