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)