From d62d7e352f976972df2b4e533cf19e17818b0aa9 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 8 Jan 2024 23:25:51 +0100 Subject: [PATCH] Add option to apply mod associations with /glamour apply. --- Glamourer/Designs/Design.cs | 2 +- Glamourer/Designs/DesignBase.cs | 8 +-- Glamourer/Interop/Penumbra/PenumbraService.cs | 30 ++++++--- Glamourer/Services/CommandService.cs | 62 +++++++++++++++++-- 4 files changed, 82 insertions(+), 20 deletions(-) diff --git a/Glamourer/Designs/Design.cs b/Glamourer/Designs/Design.cs index 709e5ca..c38669b 100644 --- a/Glamourer/Designs/Design.cs +++ b/Glamourer/Designs/Design.cs @@ -66,7 +66,7 @@ public sealed class Design : DesignBase, ISavable ["WriteProtected"] = WriteProtected(), ["Equipment"] = SerializeEquipment(), ["Customize"] = SerializeCustomize(), - ["Parameters"] = SerializeParameters(), + ["Parameters"] = SerializeParameters(), ["Mods"] = SerializeMods(), }; return ret; diff --git a/Glamourer/Designs/DesignBase.cs b/Glamourer/Designs/DesignBase.cs index aa42834..3ed2524 100644 --- a/Glamourer/Designs/DesignBase.cs +++ b/Glamourer/Designs/DesignBase.cs @@ -279,10 +279,10 @@ public class DesignBase { var ret = new JObject { - ["FileVersion"] = FileVersion, - ["Equipment"] = SerializeEquipment(), - ["Customize"] = SerializeCustomize(), - ["Parameters"] = SerializeParameters(), + ["FileVersion"] = FileVersion, + ["Equipment"] = SerializeEquipment(), + ["Customize"] = SerializeCustomize(), + ["Parameters"] = SerializeParameters(), }; return ret; } diff --git a/Glamourer/Interop/Penumbra/PenumbraService.cs b/Glamourer/Interop/Penumbra/PenumbraService.cs index 2629ad7..23ca919 100644 --- a/Glamourer/Interop/Penumbra/PenumbraService.cs +++ b/Glamourer/Interop/Penumbra/PenumbraService.cs @@ -148,7 +148,8 @@ public unsafe class PenumbraService : IDisposable public void OpenModPage(Mod mod) { if (_openModPage.Invoke(TabType.Mods, mod.DirectoryName, mod.Name) == PenumbraApiEc.ModMissing) - Glamourer.Messager.NotificationMessage($"Could not open the mod {mod.Name}, no fitting mod was found in your Penumbra install.", NotificationType.Info, false); + Glamourer.Messager.NotificationMessage($"Could not open the mod {mod.Name}, no fitting mod was found in your Penumbra install.", + NotificationType.Info, false); } public string CurrentCollection @@ -158,7 +159,7 @@ public unsafe class PenumbraService : IDisposable /// Try to set all mod settings as desired. Only sets when the mod should be enabled. /// If it is disabled, ignore all other settings. /// - public string SetMod(Mod mod, ModSettings settings) + public string SetMod(Mod mod, ModSettings settings, string? collection = null) { if (!Available) return "Penumbra is not available."; @@ -166,12 +167,13 @@ public unsafe class PenumbraService : IDisposable var sb = new StringBuilder(); try { - var collection = _currentCollection.Invoke(ApiCollectionType.Current); - var ec = _setMod.Invoke(collection, mod.DirectoryName, mod.Name, settings.Enabled); - if (ec is PenumbraApiEc.ModMissing) - return $"The mod {mod.Name} [{mod.DirectoryName}] could not be found."; - - Debug.Assert(ec is not PenumbraApiEc.CollectionMissing, "Missing collection should not be possible."); + collection ??= _currentCollection.Invoke(ApiCollectionType.Current); + var ec = _setMod.Invoke(collection, mod.DirectoryName, mod.Name, settings.Enabled); + switch (ec) + { + case PenumbraApiEc.ModMissing: return $"The mod {mod.Name} [{mod.DirectoryName}] could not be found."; + case PenumbraApiEc.CollectionMissing: return $"The collection {collection} could not be found."; + } if (!settings.Enabled) return string.Empty; @@ -216,13 +218,23 @@ public unsafe class PenumbraService : IDisposable return valid ? name : string.Empty; } + /// Obtain the name of the collection currently assigned to the given actor. + public string GetActorCollection(Actor actor) + { + if (!Available) + return string.Empty; + + var (valid, _, name) = _objectCollection.Invoke(actor.Index.Index); + return valid ? name : string.Empty; + } + /// Obtain the game object corresponding to a draw object. public Actor GameObjectFromDrawObject(Model drawObject) => Available ? _drawObjectInfo.Invoke(drawObject.Address).Item1 : Actor.Null; /// Obtain the parent of a cutscene actor if it is known. public short CutsceneParent(ushort idx) - => (short) (Available ? _cutsceneParent.Invoke(idx) : -1); + => (short)(Available ? _cutsceneParent.Invoke(idx) : -1); /// Try to redraw the given actor. public void RedrawObject(Actor actor, RedrawType settings) diff --git a/Glamourer/Services/CommandService.cs b/Glamourer/Services/CommandService.cs index b1e41f7..47074f7 100644 --- a/Glamourer/Services/CommandService.cs +++ b/Glamourer/Services/CommandService.cs @@ -10,6 +10,8 @@ using Glamourer.Events; using Glamourer.GameData; using Glamourer.Gui; using Glamourer.Interop; +using Glamourer.Interop.Penumbra; +using Glamourer.Interop.Structs; using Glamourer.State; using ImGuiNET; using OtterGui; @@ -36,10 +38,11 @@ public class CommandService : IDisposable private readonly DesignConverter _converter; private readonly DesignFileSystem _designFileSystem; private readonly Configuration _config; + private readonly PenumbraService _penumbra; public CommandService(ICommandManager commands, MainWindow mainWindow, IChatGui chat, ActorManager actors, ObjectManager objects, AutoDesignApplier autoDesignApplier, StateManager stateManager, DesignManager designManager, DesignConverter converter, - DesignFileSystem designFileSystem, AutoDesignManager autoDesignManager, Configuration config) + DesignFileSystem designFileSystem, AutoDesignManager autoDesignManager, Configuration config, PenumbraService penumbra) { _commands = commands; _mainWindow = mainWindow; @@ -53,6 +56,7 @@ public class CommandService : IDisposable _designFileSystem = designFileSystem; _autoDesignManager = autoDesignManager; _config = config; + _penumbra = penumbra; _commands.AddHandler(MainCommandString, new CommandInfo(OnGlamourer) { HelpMessage = "Open or close the Glamourer window." }); _commands.AddHandler(ApplyCommandString, @@ -368,11 +372,14 @@ public class CommandService : IDisposable private bool Apply(string arguments) { var split = arguments.Split('|', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - if (split.Length != 2) + if (split.Length is not 2) { _chat.Print(new SeStringBuilder().AddText("Use with /glamour apply ").AddYellow("[Design Name, Path or Identifier, or Clipboard]") .AddText(" | ") - .AddGreen("[Character Identifier]").BuiltString); + .AddGreen("[Character Identifier]") + .AddText("; ") + .AddBlue("") + .BuiltString); _chat.Print(new SeStringBuilder() .AddText( " 》 The design name is case-insensitive. If multiple designs of that name up to case exist, the first one is chosen.") @@ -386,10 +393,27 @@ public class CommandService : IDisposable .BuiltString); _chat.Print(new SeStringBuilder() .AddText(" 》 Clipboard as a single word will try to apply a design string currently in your clipboard.").BuiltString); + _chat.Print(new SeStringBuilder() + .AddText(" 》 ").AddBlue("").AddText(" is optional and can be omitted (together with the ;), ").AddBlue("true") + .AddText(" or ").AddBlue("false").AddText(".").BuiltString); + _chat.Print(new SeStringBuilder().AddText("If ").AddBlue("true") + .AddText(", it will try to apply mod associations to the collection assigned to the identified character.").BuiltString); PlayerIdentifierHelp(false, true); } - if (!GetDesign(split[0], out var design, true) || !IdentifierHandling(split[1], out var identifiers, false, true)) + var split2 = split[1].Split(';', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + var applyMods = split2.Length == 2 + && split2[1].ToLowerInvariant() switch + { + "true" => true, + "1" => true, + "t" => true, + "yes" => true, + "y" => true, + _ => false, + }; + if (!GetDesign(split[0], out var design, true) || !IdentifierHandling(split2[0], out var identifiers, false, true)) return false; _objects.Update(); @@ -405,7 +429,10 @@ public class CommandService : IDisposable foreach (var actor in actors.Objects) { if (_stateManager.GetOrCreate(actor.GetIdentifier(_actors), actor, out var state)) + { + ApplyModSettings(design, actor, applyMods); _stateManager.ApplyDesign(design, state, StateChanged.Source.Manual); + } } } } @@ -413,6 +440,29 @@ public class CommandService : IDisposable return true; } + private void ApplyModSettings(DesignBase design, Actor actor, bool applyMods) + { + if (!applyMods || design is not Design d) + return; + + var collection = _penumbra.GetActorCollection(actor); + if (collection.Length <= 0) + return; + + var appliedMods = 0; + foreach (var (mod, setting) in d.AssociatedMods) + { + var message = _penumbra.SetMod(mod, setting, collection); + if (message.Length > 0) + Glamourer.Messager.Chat.Print($"Error applying mod settings: {message}"); + else + ++appliedMods; + } + + if (appliedMods > 0) + Glamourer.Messager.Chat.Print($"Applied {appliedMods} mod settings to {collection}."); + } + private bool Delete(string argument) { if (argument.Length == 0) @@ -501,7 +551,8 @@ public class CommandService : IDisposable && _stateManager.GetOrCreate(identifier, data.Objects[0], out state))) continue; - var design = _converter.Convert(state, EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant, CrestExtensions.All, CustomizeParameterExtensions.All); + var design = _converter.Convert(state, EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant, CrestExtensions.All, + CustomizeParameterExtensions.All); _designManager.CreateClone(design, split[0], true); return true; } @@ -556,7 +607,6 @@ public class CommandService : IDisposable _chat.Print(new SeStringBuilder().AddText("The token ").AddYellow(argument, true).AddText(" did not resolve to an existing design.") .BuiltString); return false; - } private unsafe bool IdentifierHandling(string argument, out ActorIdentifier[] identifiers, bool allowAnyWorld, bool allowIndex)