diff --git a/Penumbra.Api b/Penumbra.Api index fdda2054..882b778e 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit fdda2054c26a30111ac55984ed6efde7f7214b68 +Subproject commit 882b778e78bb0806dd7d38e8b3670ff138a84a31 diff --git a/Penumbra.String b/Penumbra.String index dd83f972..0647fbc5 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit dd83f97299ac33cfacb1064bde4f4d1f6a260936 +Subproject commit 0647fbc5017ef9ced3f3ce1c2496eefd57c5b7a8 diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index 4027975b..b78523d3 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -1,5 +1,4 @@ using OtterGui; -using OtterGui.Log; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; @@ -25,20 +24,18 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable private readonly CollectionManager _collectionManager; private readonly CollectionEditor _collectionEditor; private readonly CommunicatorService _communicator; - private readonly ApiHelpers _helpers; public ModSettingsApi(CollectionResolver collectionResolver, ModManager modManager, CollectionManager collectionManager, CollectionEditor collectionEditor, - CommunicatorService communicator, ApiHelpers helpers) + CommunicatorService communicator) { _collectionResolver = collectionResolver; _modManager = modManager; _collectionManager = collectionManager; _collectionEditor = collectionEditor; _communicator = communicator; - _helpers = helpers; _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ApiModSettings); _communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api); _communicator.ModOptionChanged.Subscribe(OnModOptionEdited, ModOptionChanged.Priority.Api); @@ -209,142 +206,6 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable return ApiHelpers.Return(PenumbraApiEc.Success, args); } - public PenumbraApiEc SetTemporaryModSetting(Guid collectionId, string modDirectory, string modName, bool enabled, int priority, - IReadOnlyDictionary> options, string source, int key) - { - var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Enabled", enabled, - "Priority", priority, "Options", options, "Source", source, "Key", key); - if (!_collectionManager.Storage.ById(collectionId, out var collection)) - return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); - - return SetTemporaryModSetting(args, collection, modDirectory, modName, enabled, priority, options, source, key); - } - - public PenumbraApiEc TemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, bool enabled, int priority, - IReadOnlyDictionary> options, string source, int key) - { - return PenumbraApiEc.Success; - } - - private PenumbraApiEc SetTemporaryModSetting(in LazyString args, ModCollection collection, string modDirectory, string modName, - bool enabled, int priority, - IReadOnlyDictionary> options, string source, int key) - { - if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); - - if (collection.GetTempSettings(mod.Index) is { } settings && settings.Lock != 0 && settings.Lock != key) - return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args); - - settings = new TemporaryModSettings - { - Enabled = enabled, - Priority = new ModPriority(priority), - Lock = key, - Source = source, - Settings = SettingList.Default(mod), - }; - - foreach (var (groupName, optionNames) in options) - { - var groupIdx = mod.Groups.IndexOf(g => g.Name == groupName); - if (groupIdx < 0) - return ApiHelpers.Return(PenumbraApiEc.OptionGroupMissing, args); - - var setting = Setting.Zero; - switch (mod.Groups[groupIdx]) - { - case { Behaviour: GroupDrawBehaviour.SingleSelection } single: - { - var optionIdx = optionNames.Count == 0 ? -1 : single.Options.IndexOf(o => o.Name == optionNames[^1]); - if (optionIdx < 0) - return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); - - setting = Setting.Single(optionIdx); - break; - } - case { Behaviour: GroupDrawBehaviour.MultiSelection } multi: - { - foreach (var name in optionNames) - { - var optionIdx = multi.Options.IndexOf(o => o.Name == name); - if (optionIdx < 0) - return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); - - setting |= Setting.Multi(optionIdx); - } - - break; - } - } - - settings.Settings[groupIdx] = setting; - } - - collection.Settings.SetTemporary(mod.Index, settings); - return ApiHelpers.Return(PenumbraApiEc.Success, args); - } - - public PenumbraApiEc RemoveTemporaryModSettings(Guid collectionId, string modDirectory, string modName, int key) - { - var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Key", key); - if (!_collectionManager.Storage.ById(collectionId, out var collection)) - return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); - - return RemoveTemporaryModSettings(args, collection, modDirectory, modName, key); - } - - private PenumbraApiEc RemoveTemporaryModSettings(in LazyString args, ModCollection collection, string modDirectory, string modName, int key) - { - if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) - return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); - - if (collection.GetTempSettings(mod.Index) is not { } settings) - return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args); - - if (settings.Lock != 0 && settings.Lock != key) - return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args); - - collection.Settings.SetTemporary(mod.Index, null); - return ApiHelpers.Return(PenumbraApiEc.Success, args); - } - - public PenumbraApiEc RemoveTemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, int key) - { - return PenumbraApiEc.Success; - } - - public PenumbraApiEc RemoveAllTemporaryModSettings(Guid collectionId, int key) - { - var args = ApiHelpers.Args("CollectionId", collectionId, "Key", key); - if (!_collectionManager.Storage.ById(collectionId, out var collection)) - return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); - - return RemoveAllTemporaryModSettings(args, collection, key); - } - - public PenumbraApiEc RemoveAllTemporaryModSettingsPlayer(int objectIndex, int key) - { - return PenumbraApiEc.Success; - } - - private PenumbraApiEc RemoveAllTemporaryModSettings(in LazyString args, ModCollection collection, int key) - { - var numRemoved = 0; - for (var i = 0; i < collection.Settings.Count; ++i) - { - if (collection.GetTempSettings(i) is { } settings && (settings.Lock == 0 || settings.Lock == key)) - { - collection.Settings.SetTemporary(i, null); - ++numRemoved; - } - } - - return ApiHelpers.Return(numRemoved > 0 ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged, args); - } - - - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void TriggerSettingEdited(Mod mod) { diff --git a/Penumbra/Api/Api/PenumbraApi.cs b/Penumbra/Api/Api/PenumbraApi.cs index eaaf9f38..894b2674 100644 --- a/Penumbra/Api/Api/PenumbraApi.cs +++ b/Penumbra/Api/Api/PenumbraApi.cs @@ -22,7 +22,7 @@ public class PenumbraApi( } public (int Breaking, int Feature) ApiVersion - => (5, 3); + => (5, 4); public bool Valid { get; private set; } = true; public IPenumbraApiCollection Collection { get; } = collection; diff --git a/Penumbra/Api/Api/TemporaryApi.cs b/Penumbra/Api/Api/TemporaryApi.cs index 201839e7..afddeae8 100644 --- a/Penumbra/Api/Api/TemporaryApi.cs +++ b/Penumbra/Api/Api/TemporaryApi.cs @@ -1,8 +1,11 @@ +using OtterGui.Log; using OtterGui.Services; using Penumbra.Api.Enums; +using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.GameData.Interop; +using Penumbra.Mods.Manager; using Penumbra.Mods.Settings; using Penumbra.String.Classes; @@ -13,7 +16,9 @@ public class TemporaryApi( ObjectManager objects, ActorManager actors, CollectionManager collectionManager, - TempModManager tempMods) : IPenumbraApiTemporary, IApiService + TempModManager tempMods, + ApiHelpers apiHelpers, + ModManager modManager) : IPenumbraApiTemporary, IApiService { public Guid CreateTemporaryCollection(string name) => tempCollections.CreateTemporaryCollection(name); @@ -125,6 +130,139 @@ public class TemporaryApi( return ApiHelpers.Return(ret, args); } + + public PenumbraApiEc SetTemporaryModSettings(Guid collectionId, string modDirectory, string modName, bool inherit, bool enabled, int priority, + IReadOnlyDictionary> options, string source, int key) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Inherit", inherit, "Enabled", enabled, + "Priority", priority, "Options", options, "Source", source, "Key", key); + if (!collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + return SetTemporaryModSettings(args, collection, modDirectory, modName, inherit, enabled, priority, options, source, key); + } + + public PenumbraApiEc SetTemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, bool inherit, bool enabled, int priority, + IReadOnlyDictionary> options, string source, int key) + { + var args = ApiHelpers.Args("ObjectIndex", objectIndex, "ModDirectory", modDirectory, "ModName", modName, "Inherit", inherit, "Enabled", enabled, + "Priority", priority, "Options", options, "Source", source, "Key", key); + if (!apiHelpers.AssociatedCollection(objectIndex, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); + + return SetTemporaryModSettings(args, collection, modDirectory, modName, inherit, enabled, priority, options, source, key); + } + + private PenumbraApiEc SetTemporaryModSettings(in LazyString args, ModCollection collection, string modDirectory, string modName, + bool inherit, bool enabled, int priority, IReadOnlyDictionary> options, string source, int key) + { + if (collection.Identity.Index <= 0) + return ApiHelpers.Return(PenumbraApiEc.TemporarySettingImpossible, args); + + if (!modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); + + if (!collectionManager.Editor.CanSetTemporarySettings(collection, mod, key)) + if (collection.GetTempSettings(mod.Index) is { } oldSettings && oldSettings.Lock != 0 && oldSettings.Lock != key) + return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args); + + var newSettings = new TemporaryModSettings() + { + ForceInherit = inherit, + Enabled = enabled, + Priority = new ModPriority(priority), + Lock = key, + Source = source, + Settings = SettingList.Default(mod), + }; + + + foreach (var (groupName, optionNames) in options) + { + var ec = ModSettingsApi.ConvertModSetting(mod, groupName, optionNames, out var groupIdx, out var setting); + if (ec != PenumbraApiEc.Success) + return ApiHelpers.Return(ec, args); + + newSettings.Settings[groupIdx] = setting; + } + + if (collectionManager.Editor.SetTemporarySettings(collection, mod, newSettings, key)) + return ApiHelpers.Return(PenumbraApiEc.Success, args); + + // This should not happen since all error cases had been checked before. + return ApiHelpers.Return(PenumbraApiEc.UnknownError, args); + } + + public PenumbraApiEc RemoveTemporaryModSettings(Guid collectionId, string modDirectory, string modName, int key) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Key", key); + if (!collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + return RemoveTemporaryModSettings(args, collection, modDirectory, modName, key); + } + + public PenumbraApiEc RemoveTemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, int key) + { + var args = ApiHelpers.Args("ObjectIndex", objectIndex, "ModDirectory", modDirectory, "ModName", modName, "Key", key); + if (!apiHelpers.AssociatedCollection(objectIndex, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); + + return RemoveTemporaryModSettings(args, collection, modDirectory, modName, key); + } + + private PenumbraApiEc RemoveTemporaryModSettings(in LazyString args, ModCollection collection, string modDirectory, string modName, int key) + { + if (collection.Identity.Index <= 0) + return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args); + + if (!modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); + + if (collection.GetTempSettings(mod.Index) is null) + return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args); + + if (!collectionManager.Editor.SetTemporarySettings(collection, mod, null, key)) + return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args); + + return ApiHelpers.Return(PenumbraApiEc.Success, args); + } + + public PenumbraApiEc RemoveAllTemporaryModSettings(Guid collectionId, int key) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "Key", key); + if (!collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + return RemoveAllTemporaryModSettings(args, collection, key); + } + + public PenumbraApiEc RemoveAllTemporaryModSettingsPlayer(int objectIndex, int key) + { + var args = ApiHelpers.Args("ObjectIndex", objectIndex, "Key", key); + if (!apiHelpers.AssociatedCollection(objectIndex, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.InvalidArgument, args); + + return RemoveAllTemporaryModSettings(args, collection, key); + } + + private PenumbraApiEc RemoveAllTemporaryModSettings(in LazyString args, ModCollection collection, int key) + { + if (collection.Identity.Index <= 0) + return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args); + + var numRemoved = 0; + for (var i = 0; i < collection.Settings.Count; ++i) + { + if (collection.GetTempSettings(i) is not null + && collectionManager.Editor.SetTemporarySettings(collection, modManager[i], null, key)) + ++numRemoved; + } + + return ApiHelpers.Return(numRemoved > 0 ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged, args); + } + + /// /// Convert a dictionary of strings to a dictionary of game paths to full paths. /// Only returns true if all paths can successfully be converted and added. diff --git a/Penumbra/Api/IpcProviders.cs b/Penumbra/Api/IpcProviders.cs index 861225fa..6f3b2c38 100644 --- a/Penumbra/Api/IpcProviders.cs +++ b/Penumbra/Api/IpcProviders.cs @@ -63,7 +63,7 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.ApiVersion.Provider(pi, api), new FuncProvider<(int Major, int Minor)>(pi, "Penumbra.ApiVersions", () => api.ApiVersion), // backward compatibility - new FuncProvider(pi, "Penumbra.ApiVersion", () => api.ApiVersion.Breaking), // backward compatibility + new FuncProvider(pi, "Penumbra.ApiVersion", () => api.ApiVersion.Breaking), // backward compatibility IpcSubscribers.GetModDirectory.Provider(pi, api.PluginState), IpcSubscribers.GetConfiguration.Provider(pi, api.PluginState), IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState), @@ -97,6 +97,12 @@ public sealed class IpcProviders : IDisposable, IApiService IpcSubscribers.AddTemporaryMod.Provider(pi, api.Temporary), IpcSubscribers.RemoveTemporaryModAll.Provider(pi, api.Temporary), IpcSubscribers.RemoveTemporaryMod.Provider(pi, api.Temporary), + IpcSubscribers.SetTemporaryModSettings.Provider(pi, api.Temporary), + IpcSubscribers.SetTemporaryModSettingsPlayer.Provider(pi, api.Temporary), + IpcSubscribers.RemoveTemporaryModSettings.Provider(pi, api.Temporary), + IpcSubscribers.RemoveTemporaryModSettingsPlayer.Provider(pi, api.Temporary), + IpcSubscribers.RemoveAllTemporaryModSettings.Provider(pi, api.Temporary), + IpcSubscribers.RemoveAllTemporaryModSettingsPlayer.Provider(pi, api.Temporary), IpcSubscribers.ChangedItemTooltip.Provider(pi, api.Ui), IpcSubscribers.ChangedItemClicked.Provider(pi, api.Ui), diff --git a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs index f3c23831..2364dddf 100644 --- a/Penumbra/Api/IpcTester/TemporaryIpcTester.cs +++ b/Penumbra/Api/IpcTester/TemporaryIpcTester.cs @@ -33,6 +33,7 @@ public class TemporaryIpcTester( private string _tempCollectionName = string.Empty; private string _tempCollectionGuidName = string.Empty; private string _tempModName = string.Empty; + private string _modDirectory = string.Empty; private string _tempGamePath = "test/game/path.mtrl"; private string _tempFilePath = "test/success.mtrl"; private string _tempManipulation = string.Empty; @@ -50,6 +51,7 @@ public class TemporaryIpcTester( ImGuiUtil.GuidInput("##guid", "Collection GUID...", string.Empty, ref _tempGuid, ref _tempCollectionGuidName); ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0); ImGui.InputTextWithHint("##tempMod", "Temporary Mod Name...", ref _tempModName, 32); + ImGui.InputTextWithHint("##mod", "Existing Mod Name...", ref _modDirectory, 256); ImGui.InputTextWithHint("##tempGame", "Game Path...", ref _tempGamePath, 256); ImGui.InputTextWithHint("##tempFile", "File Path...", ref _tempFilePath, 256); ImUtf8.InputText("##tempManip"u8, ref _tempManipulation, "Manipulation Base64 String..."u8); @@ -121,6 +123,44 @@ public class TemporaryIpcTester( IpcTester.DrawIntro(RemoveTemporaryModAll.Label, "Remove Temporary Mod from all Collections"); if (ImGui.Button("Remove##ModAll")) _lastTempError = new RemoveTemporaryModAll(pi).Invoke(_tempModName, int.MaxValue); + + IpcTester.DrawIntro(SetTemporaryModSettings.Label, "Set Temporary Mod Settings (to default) in specific Collection"); + if (ImUtf8.Button("Set##SetTemporary"u8)) + _lastTempError = new SetTemporaryModSettings(pi).Invoke(guid, _modDirectory, string.Empty, false, true, 1337, new Dictionary>(), + "IPC Tester", 1337); + + IpcTester.DrawIntro(SetTemporaryModSettingsPlayer.Label, "Set Temporary Mod Settings (to default) in game object collection"); + if (ImUtf8.Button("Set##SetTemporaryPlayer"u8)) + _lastTempError = new SetTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, string.Empty, false, true, 1337, new Dictionary>(), + "IPC Tester", 1337); + + IpcTester.DrawIntro(RemoveTemporaryModSettings.Label, "Remove Temporary Mod Settings from specific Collection"); + if (ImUtf8.Button("Remove##RemoveTemporary"u8)) + _lastTempError = new RemoveTemporaryModSettings(pi).Invoke(guid, _modDirectory, string.Empty, 1337); + ImGui.SameLine(); + if (ImUtf8.Button("Remove (Wrong Key)##RemoveTemporary"u8)) + _lastTempError = new RemoveTemporaryModSettings(pi).Invoke(guid, _modDirectory, string.Empty, 1338); + + IpcTester.DrawIntro(RemoveTemporaryModSettingsPlayer.Label, "Remove Temporary Mod Settings from game object Collection"); + if (ImUtf8.Button("Remove##RemoveTemporaryPlayer"u8)) + _lastTempError = new RemoveTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, string.Empty, 1337); + ImGui.SameLine(); + if (ImUtf8.Button("Remove (Wrong Key)##RemoveTemporaryPlayer"u8)) + _lastTempError = new RemoveTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, _modDirectory, string.Empty, 1338); + + IpcTester.DrawIntro(RemoveAllTemporaryModSettings.Label, "Remove All Temporary Mod Settings from specific Collection"); + if (ImUtf8.Button("Remove##RemoveAllTemporary"u8)) + _lastTempError = new RemoveAllTemporaryModSettings(pi).Invoke(guid, 1337); + ImGui.SameLine(); + if (ImUtf8.Button("Remove (Wrong Key)##RemoveAllTemporary"u8)) + _lastTempError = new RemoveAllTemporaryModSettings(pi).Invoke(guid, 1338); + + IpcTester.DrawIntro(RemoveAllTemporaryModSettingsPlayer.Label, "Remove All Temporary Mod Settings from game object Collection"); + if (ImUtf8.Button("Remove##RemoveAllTemporaryPlayer"u8)) + _lastTempError = new RemoveAllTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, 1337); + ImGui.SameLine(); + if (ImUtf8.Button("Remove (Wrong Key)##RemoveAllTemporaryPlayer"u8)) + _lastTempError = new RemoveAllTemporaryModSettingsPlayer(pi).Invoke(_tempActorIndex, 1338); } public void DrawCollections() diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index b456686e..124f8cf7 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -106,8 +106,7 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu public bool SetTemporarySettings(ModCollection collection, Mod mod, TemporaryModSettings? settings, int key = 0) { key = settings?.Lock ?? key; - var old = collection.GetTempSettings(mod.Index); - if (old != null && old.Lock != 0 && old.Lock != key) + if (!CanSetTemporarySettings(collection, mod, key)) return false; collection.Settings.SetTemporary(mod.Index, settings); @@ -115,6 +114,12 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu return true; } + public bool CanSetTemporarySettings(ModCollection collection, Mod mod, int key) + { + var old = collection.GetTempSettings(mod.Index); + return old == null || old.Lock == 0 || old.Lock == key; + } + /// Copy the settings of an existing (sourceMod != null) or stored (sourceName) mod to another mod, if they exist. public bool CopyModSettings(ModCollection collection, Mod? sourceMod, string sourceName, Mod? targetMod, string targetName) { diff --git a/Penumbra/Interop/Services/SchedulerResourceManagementService.cs b/Penumbra/Interop/Services/SchedulerResourceManagementService.cs index 1d56fcdb..b7f57a44 100644 --- a/Penumbra/Interop/Services/SchedulerResourceManagementService.cs +++ b/Penumbra/Interop/Services/SchedulerResourceManagementService.cs @@ -69,7 +69,7 @@ public unsafe class SchedulerResourceManagementService : IService, IDisposable if (_actionTmbs.TryGetValue(tmb, out var rowId)) _listedTmbIds[rowId] = tmb; else - Penumbra.Log.Debug($"Action TMB {gamePath} encountered with no corresponding row ID."); + Penumbra.Log.Verbose($"Action TMB {gamePath} encountered with no corresponding row ID."); } [Signature(Sigs.SchedulerResourceManagementInstance, ScanType = ScanType.StaticAddress)] diff --git a/Penumbra/Mods/Settings/TemporaryModSettings.cs b/Penumbra/Mods/Settings/TemporaryModSettings.cs index 27987fa6..de4570c5 100644 --- a/Penumbra/Mods/Settings/TemporaryModSettings.cs +++ b/Penumbra/Mods/Settings/TemporaryModSettings.cs @@ -16,6 +16,22 @@ public sealed class TemporaryModSettings : ModSettings Priority = ModPriority.Default, Settings = SettingList.Default(mod), }; + + public TemporaryModSettings() + { } + + public TemporaryModSettings(ModSettings? clone, string source, int key = 0) + { + Source = source; + Lock = key; + ForceInherit = clone == null; + if (clone != null) + { + Enabled = clone.Enabled; + Priority = clone.Priority; + Settings = clone.Settings.Clone(); + } + } } public static class ModSettingsExtensions diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index fbead9c3..4c0d1694 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -56,12 +56,24 @@ public static class Colors { var tintValue = ImGui.ColorConvertU32ToFloat4(tint.Value()); var value = ImGui.ColorConvertU32ToFloat4(color.Value()); - var negAlpha = 1 - tintValue.W; - var newAlpha = negAlpha * value.W + tintValue.W; - var newR = (negAlpha * value.W * value.X + tintValue.W * tintValue.X) / newAlpha; - var newG = (negAlpha * value.W * value.Y + tintValue.W * tintValue.Y) / newAlpha; - var newB = (negAlpha * value.W * value.Z + tintValue.W * tintValue.Z) / newAlpha; - return ImGui.ColorConvertFloat4ToU32(new Vector4(newR, newG, newB, newAlpha)); + return ImGui.ColorConvertFloat4ToU32(TintColor(value, tintValue)); + } + + public static unsafe uint Tinted(this ImGuiCol color, ColorId tint) + { + var tintValue = ImGui.ColorConvertU32ToFloat4(tint.Value()); + ref var value = ref *ImGui.GetStyleColorVec4(color); + return ImGui.ColorConvertFloat4ToU32(TintColor(value, tintValue)); + } + + private static unsafe Vector4 TintColor(in Vector4 color, in Vector4 tint) + { + var negAlpha = 1 - tint.W; + var newAlpha = negAlpha * color.W + tint.W; + var newR = (negAlpha * color.W * color.X + tint.W * tint.X) / newAlpha; + var newG = (negAlpha * color.W * color.Y + tint.W * tint.Y) / newAlpha; + var newB = (negAlpha * color.W * color.Z + tint.W * tint.Z) / newAlpha; + return new Vector4(newR, newG, newB, newAlpha); } public static (uint DefaultColor, string Name, string Description) Data(this ColorId color) diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs index dec77430..527d8bce 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs @@ -3,6 +3,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using OtterGui.Services; +using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Collections; using Penumbra.Collections.Manager; @@ -16,13 +17,19 @@ namespace Penumbra.UI.ModsTab.Groups; public sealed class ModGroupDrawer(Configuration config, CollectionManager collectionManager) : IUiService { private readonly List<(IModGroup, int)> _blockGroupCache = []; + private bool _temporary; + private bool _locked; + private TemporaryModSettings? _tempSettings; - public void Draw(Mod mod, ModSettings settings) + public void Draw(Mod mod, ModSettings settings, TemporaryModSettings? tempSettings) { if (mod.Groups.Count <= 0) return; _blockGroupCache.Clear(); + _tempSettings = tempSettings; + _temporary = tempSettings != null; + _locked = (tempSettings?.Lock ?? 0) != 0; var useDummy = true; foreach (var (group, idx) in mod.Groups.WithIndex()) { @@ -63,22 +70,23 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle /// private void DrawSingleGroupCombo(IModGroup group, int groupIdx, Setting setting) { - using var id = ImRaii.PushId(groupIdx); - var selectedOption = setting.AsIndex; + using var id = ImUtf8.PushId(groupIdx); + var selectedOption = setting.AsIndex; + using var disabled = ImRaii.Disabled(_locked); ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X * 3 / 4); var options = group.Options; - using (var combo = ImRaii.Combo(string.Empty, options[selectedOption].Name)) + using (var combo = ImUtf8.Combo(""u8, options[selectedOption].Name)) { if (combo) for (var idx2 = 0; idx2 < options.Count; ++idx2) { id.Push(idx2); var option = options[idx2]; - if (ImGui.Selectable(option.Name, idx2 == selectedOption)) + if (ImUtf8.Selectable(option.Name, idx2 == selectedOption)) SetModSetting(group, groupIdx, Setting.Single(idx2)); if (option.Description.Length > 0) - ImGuiUtil.SelectableHelpMarker(option.Description); + ImUtf8.SelectableHelpMarker(option.Description); id.Pop(); } @@ -86,9 +94,9 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle ImGui.SameLine(); if (group.Description.Length > 0) - ImGuiUtil.LabeledHelpMarker(group.Name, group.Description); + ImUtf8.LabeledHelpMarker(group.Name, group.Description); else - ImGui.TextUnformatted(group.Name); + ImUtf8.Text(group.Name); } /// @@ -97,10 +105,10 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle /// private void DrawSingleGroupRadio(IModGroup group, int groupIdx, Setting setting) { - using var id = ImRaii.PushId(groupIdx); - var selectedOption = setting.AsIndex; - var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); - var options = group.Options; + using var id = ImUtf8.PushId(groupIdx); + var selectedOption = setting.AsIndex; + var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); + var options = group.Options; DrawCollapseHandling(options, minWidth, DrawOptions); Widget.EndFramedGroup(); @@ -108,11 +116,12 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle void DrawOptions() { + using var disabled = ImRaii.Disabled(_locked); for (var idx = 0; idx < group.Options.Count; ++idx) { - using var i = ImRaii.PushId(idx); - var option = options[idx]; - if (ImGui.RadioButton(option.Name, selectedOption == idx)) + using var i = ImUtf8.PushId(idx); + var option = options[idx]; + if (ImUtf8.RadioButton(option.Name, selectedOption == idx)) SetModSetting(group, groupIdx, Setting.Single(idx)); if (option.Description.Length <= 0) @@ -130,28 +139,29 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle /// private void DrawMultiGroup(IModGroup group, int groupIdx, Setting setting) { - using var id = ImRaii.PushId(groupIdx); - var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); - var options = group.Options; + using var id = ImUtf8.PushId(groupIdx); + var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); + var options = group.Options; DrawCollapseHandling(options, minWidth, DrawOptions); Widget.EndFramedGroup(); var label = $"##multi{groupIdx}"; if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) - ImGui.OpenPopup($"##multi{groupIdx}"); + ImUtf8.OpenPopup($"##multi{groupIdx}"); DrawMultiPopup(group, groupIdx, label); return; void DrawOptions() { + using var disabled = ImRaii.Disabled(_locked); for (var idx = 0; idx < options.Count; ++idx) { - using var i = ImRaii.PushId(idx); - var option = options[idx]; - var enabled = setting.HasFlag(idx); + using var i = ImUtf8.PushId(idx); + var option = options[idx]; + var enabled = setting.HasFlag(idx); - if (ImGui.Checkbox(option.Name, ref enabled)) + if (ImUtf8.Checkbox(option.Name, ref enabled)) SetModSetting(group, groupIdx, setting.SetBit(idx, enabled)); if (option.Description.Length > 0) @@ -171,11 +181,12 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle return; ImGui.TextUnformatted(group.Name); + using var disabled = ImRaii.Disabled(_locked); ImGui.Separator(); - if (ImGui.Selectable("Enable All")) + if (ImUtf8.Selectable("Enable All"u8)) SetModSetting(group, groupIdx, Setting.AllBits(group.Options.Count)); - if (ImGui.Selectable("Disable All")) + if (ImUtf8.Selectable("Disable All"u8)) SetModSetting(group, groupIdx, Setting.Zero); } @@ -187,11 +198,11 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle } else { - var collapseId = ImGui.GetID("Collapse"); - var shown = ImGui.GetStateStorage().GetBool(collapseId, true); + var collapseId = ImUtf8.GetId("Collapse"); + var shown = ImGui.GetStateStorage().GetBool(collapseId, true); var buttonTextShow = $"Show {options.Count} Options"; var buttonTextHide = $"Hide {options.Count} Options"; - var buttonWidth = Math.Max(ImGui.CalcTextSize(buttonTextShow).X, ImGui.CalcTextSize(buttonTextHide).X) + var buttonWidth = Math.Max(ImUtf8.CalcTextSize(buttonTextShow).X, ImUtf8.CalcTextSize(buttonTextHide).X) + 2 * ImGui.GetStyle().FramePadding.X; minWidth = Math.Max(buttonWidth, minWidth); if (shown) @@ -204,22 +215,22 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle } - var width = Math.Max(ImGui.GetItemRectSize().X, minWidth); + var width = Math.Max(ImGui.GetItemRectSize().X, minWidth); var endPos = ImGui.GetCursorPos(); ImGui.SetCursorPos(pos); - if (ImGui.Button(buttonTextHide, new Vector2(width, 0))) + if (ImUtf8.Button(buttonTextHide, new Vector2(width, 0))) ImGui.GetStateStorage().SetBool(collapseId, !shown); ImGui.SetCursorPos(endPos); } else { - var optionWidth = options.Max(o => ImGui.CalcTextSize(o.Name).X) + var optionWidth = options.Max(o => ImUtf8.CalcTextSize(o.Name).X) + ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X; var width = Math.Max(optionWidth, minWidth); - if (ImGui.Button(buttonTextShow, new Vector2(width, 0))) + if (ImUtf8.Button(buttonTextShow, new Vector2(width, 0))) ImGui.GetStateStorage().SetBool(collapseId, !shown); } } @@ -228,6 +239,18 @@ public sealed class ModGroupDrawer(Configuration config, CollectionManager colle private ModCollection Current => collectionManager.Active.Current; + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void SetModSetting(IModGroup group, int groupIdx, Setting setting) - => collectionManager.Editor.SetModSetting(Current, group.Mod, groupIdx, setting); + { + if (_temporary) + { + _tempSettings!.ForceInherit = false; + _tempSettings!.Settings[groupIdx] = setting; + collectionManager.Editor.SetTemporarySettings(Current, group.Mod, _tempSettings); + } + else + { + collectionManager.Editor.SetModSetting(Current, group.Mod, groupIdx, setting); + } + } } diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 261f6e92..cf64c00a 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -1,6 +1,5 @@ using ImGuiNET; using OtterGui.Raii; -using OtterGui; using OtterGui.Services; using OtterGui.Text; using OtterGui.Widgets; @@ -24,6 +23,8 @@ public class ModPanelSettingsTab( : ITab, IUiService { private bool _inherited; + private bool _temporary; + private bool _locked; private int? _currentPriority; public ReadOnlySpan Label @@ -37,11 +38,14 @@ public class ModPanelSettingsTab( public void DrawContent() { - using var child = ImRaii.Child("##settings"); + using var child = ImUtf8.Child("##settings"u8, default); if (!child) return; - _inherited = selection.Collection != collectionManager.Active.Current; + _inherited = selection.Collection != collectionManager.Active.Current; + _temporary = selection.TemporarySettings != null; + _locked = (selection.TemporarySettings?.Lock ?? 0) != 0; + DrawTemporaryWarning(); DrawInheritedWarning(); UiHelpers.DefaultLineSpace(); communicator.PreSettingsPanelDraw.Invoke(selection.Mod!.Identifier); @@ -54,11 +58,27 @@ public class ModPanelSettingsTab( communicator.PostEnabledDraw.Invoke(selection.Mod!.Identifier); - modGroupDrawer.Draw(selection.Mod!, selection.Settings); + modGroupDrawer.Draw(selection.Mod!, selection.Settings, selection.TemporarySettings); UiHelpers.DefaultLineSpace(); communicator.PostSettingsPanelDraw.Invoke(selection.Mod!.Identifier); } + /// Draw a big tinted bar if the current setting is temporary. + private void DrawTemporaryWarning() + { + if (!_temporary) + return; + + using var color = ImRaii.PushColor(ImGuiCol.Button, ImGuiCol.Button.Tinted(ColorId.TemporaryModSettingsTint)); + var width = new Vector2(ImGui.GetContentRegionAvail().X, 0); + if (ImUtf8.ButtonEx($"These settings are temporary from {selection.TemporarySettings!.Source}{(_locked ? " and locked." : ".")}", width, + _locked)) + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, null); + + ImUtf8.HoverTooltip("Changing settings in temporary settings will not save them across sessions.\n"u8 + + "You can click this button to remove the temporary settings and return to your normal settings."u8); + } + /// Draw a big red bar if the current setting is inherited. private void DrawInheritedWarning() { @@ -67,22 +87,42 @@ public class ModPanelSettingsTab( using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.PressEnterWarningBg); var width = new Vector2(ImGui.GetContentRegionAvail().X, 0); - if (ImGui.Button($"These settings are inherited from {selection.Collection.Identity.Name}.", width)) - collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, false); + if (ImUtf8.ButtonEx($"These settings are inherited from {selection.Collection.Identity.Name}.", width, _locked)) + { + if (_temporary) + { + selection.TemporarySettings!.ForceInherit = false; + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, selection.TemporarySettings); + } + else + { + collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, false); + } + } - ImGuiUtil.HoverTooltip("You can click this button to copy the current settings to the current selection.\n" - + "You can also just change any setting, which will copy the settings with the single setting changed to the current selection."); + ImUtf8.HoverTooltip("You can click this button to copy the current settings to the current selection.\n"u8 + + "You can also just change any setting, which will copy the settings with the single setting changed to the current selection."u8); } /// Draw a checkbox for the enabled status of the mod. private void DrawEnabledInput() { - var enabled = selection.Settings.Enabled; - if (!ImGui.Checkbox("Enabled", ref enabled)) + var enabled = selection.Settings.Enabled; + using var disabled = ImRaii.Disabled(_locked); + if (!ImUtf8.Checkbox("Enabled"u8, ref enabled)) return; modManager.SetKnown(selection.Mod!); - collectionManager.Editor.SetModState(collectionManager.Active.Current, selection.Mod!, enabled); + if (_temporary) + { + selection.TemporarySettings!.ForceInherit = false; + selection.TemporarySettings!.Enabled = enabled; + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, selection.TemporarySettings); + } + else + { + collectionManager.Editor.SetModState(collectionManager.Active.Current, selection.Mod!, enabled); + } } /// @@ -91,45 +131,66 @@ public class ModPanelSettingsTab( /// private void DrawPriorityInput() { - using var group = ImRaii.Group(); + using var group = ImUtf8.Group(); var settings = selection.Settings; var priority = _currentPriority ?? settings.Priority.Value; ImGui.SetNextItemWidth(50 * UiHelpers.Scale); - if (ImGui.InputInt("##Priority", ref priority, 0, 0)) + using var disabled = ImRaii.Disabled(_locked); + if (ImUtf8.InputScalar("##Priority"u8, ref priority)) _currentPriority = priority; if (new ModPriority(priority).IsHidden) - ImUtf8.HoverTooltip($"This priority is special-cased to hide this mod in conflict tabs ({ModPriority.HiddenMin}, {ModPriority.HiddenMax})."); + ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, + $"This priority is special-cased to hide this mod in conflict tabs ({ModPriority.HiddenMin}, {ModPriority.HiddenMax})."); if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) { if (_currentPriority != settings.Priority.Value) - collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selection.Mod!, - new ModPriority(_currentPriority.Value)); + { + if (_temporary) + { + selection.TemporarySettings!.ForceInherit = false; + selection.TemporarySettings!.Priority = new ModPriority(_currentPriority.Value); + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, + selection.TemporarySettings); + } + else + { + collectionManager.Editor.SetModPriority(collectionManager.Active.Current, selection.Mod!, + new ModPriority(_currentPriority.Value)); + } + } _currentPriority = null; } - ImGuiUtil.LabeledHelpMarker("Priority", "Mods with a higher number here take precedence before Mods with a lower number.\n" - + "That means, if Mod A should overwrite changes from Mod B, Mod A should have a higher priority number than Mod B."); + ImUtf8.LabeledHelpMarker("Priority"u8, "Mods with a higher number here take precedence before Mods with a lower number.\n"u8 + + "That means, if Mod A should overwrite changes from Mod B, Mod A should have a higher priority number than Mod B."u8); } /// /// Draw a button to remove the current settings and inherit them instead - /// on the top-right corner of the window/tab. + /// in the top-right corner of the window/tab. /// private void DrawRemoveSettings() { - const string text = "Inherit Settings"; if (_inherited || selection.Settings == ModSettings.Empty) return; var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0; - ImGui.SameLine(ImGui.GetWindowWidth() - ImGui.CalcTextSize(text).X - ImGui.GetStyle().FramePadding.X * 2 - scroll); - if (ImGui.Button(text)) - collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, true); + ImGui.SameLine(ImGui.GetWindowWidth() - ImUtf8.CalcTextSize("Inherit Settings"u8).X - ImGui.GetStyle().FramePadding.X * 2 - scroll); + if (!ImUtf8.ButtonEx("Inherit Settings"u8, "Remove current settings from this collection so that it can inherit them.\n"u8 + + "If no inherited collection has settings for this mod, it will be disabled."u8, default, _locked)) + return; - ImGuiUtil.HoverTooltip("Remove current settings from this collection so that it can inherit them.\n" - + "If no inherited collection has settings for this mod, it will be disabled."); + if (_temporary) + { + selection.TemporarySettings!.ForceInherit = true; + collectionManager.Editor.SetTemporarySettings(collectionManager.Active.Current, selection.Mod!, selection.TemporarySettings); + } + else + { + collectionManager.Editor.SetModInheritance(collectionManager.Active.Current, selection.Mod!, true); + } } } diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 5fd38d94..c5168109 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -791,19 +791,25 @@ public class DebugTab : Window, ITab, IUiService ImGuiClip.DrawEndDummy(dummy, ImGui.GetTextLineHeightWithSpacing()); } + private string _tmbKeyFilter = string.Empty; + private CiByteString _tmbKeyFilterU8 = CiByteString.Empty; + private void DrawActionTmbs() { using var mainTree = TreeNode("Action TMBs"); if (!mainTree) return; + if (ImGui.InputText("Key", ref _tmbKeyFilter, 256)) + _tmbKeyFilterU8 = CiByteString.FromString(_tmbKeyFilter, out var r, MetaDataComputation.All) ? r : CiByteString.Empty; using var table = Table("##table", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit, new Vector2(-1, 12 * ImGui.GetTextLineHeightWithSpacing())); if (!table) return; var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeightWithSpacing()); - var dummy = ImGuiClip.ClippedDraw(_schedulerService.ActionTmbs.OrderBy(r => r.Value), skips, + var dummy = ImGuiClip.FilteredClippedDraw(_schedulerService.ActionTmbs.OrderBy(r => r.Value), skips, + kvp => kvp.Key.Contains(_tmbKeyFilterU8), p => { ImUtf8.DrawTableColumn($"{p.Value}");