using Dalamud.Game.Command; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Plugin.Services; using ImGuiNET; using OtterGui.Classes; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.UI; using Penumbra.UI.Knowledge; namespace Penumbra; public class CommandHandler : IDisposable, IApiService { private const string CommandName = "/penumbra"; private readonly ICommandManager _commandManager; private readonly RedrawService _redrawService; private readonly IChatGui _chat; private readonly Configuration _config; private readonly ConfigWindow _configWindow; private readonly ActorManager _actors; private readonly ModManager _modManager; private readonly CollectionManager _collectionManager; private readonly Penumbra _penumbra; private readonly CollectionEditor _collectionEditor; private readonly KnowledgeWindow _knowledgeWindow; public CommandHandler(IFramework framework, ICommandManager commandManager, IChatGui chat, RedrawService redrawService, Configuration config, ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorManager actors, Penumbra penumbra, CollectionEditor collectionEditor, KnowledgeWindow knowledgeWindow) { _commandManager = commandManager; _redrawService = redrawService; _config = config; _configWindow = configWindow; _modManager = modManager; _collectionManager = collectionManager; _actors = actors; _chat = chat; _penumbra = penumbra; _collectionEditor = collectionEditor; _knowledgeWindow = knowledgeWindow; framework.RunOnFrameworkThread(() => { if (_commandManager.Commands.ContainsKey(CommandName)) _commandManager.RemoveHandler(CommandName); _commandManager.AddHandler(CommandName, new CommandInfo(OnCommand) { HelpMessage = "Without arguments, toggles the main window. Use /penumbra help to get further command help.", ShowInHelp = true, }); Penumbra.Log.Information($"Registered {CommandName} with Dalamud."); }); } public void Dispose() => _commandManager.RemoveHandler(CommandName); private void OnCommand(string command, string arguments) { if (arguments.Length == 0) arguments = "window"; var argumentList = arguments.Split(' ', 2); arguments = argumentList.Length == 2 ? argumentList[1] : string.Empty; _ = argumentList[0].ToLowerInvariant() switch { "window" => ToggleWindow(arguments), "enable" => SetPenumbraState(arguments, true), "disable" => SetPenumbraState(arguments, false), "toggle" => SetPenumbraState(arguments, null), "reload" => Reload(arguments), "redraw" => Redraw(arguments), "lockui" => SetUiLockState(arguments), "size" => SetUiMinimumSize(arguments), "debug" => SetDebug(arguments), "collection" => SetCollection(arguments), "mod" => SetMod(arguments), "bulktag" => SetTag(arguments), "knowledge" => HandleKnowledge(arguments), _ => PrintHelp(argumentList[0]), }; } private bool PrintHelp(string arguments) { if (!string.Equals(arguments, "help", StringComparison.OrdinalIgnoreCase) && arguments != "?") _chat.Print(new SeStringBuilder().AddText("The given argument ").AddRed(arguments, true) .AddText(" is not valid. Valid arguments are:").BuiltString); else _chat.Print("Valid arguments for /penumbra are:"); _chat.Print(new SeStringBuilder().AddCommand("window", "Toggle the Penumbra main config window. Can be used with [on|off] to force specific state. Also used when no argument is provided.") .BuiltString); _chat.Print(new SeStringBuilder() .AddCommand("enable", "Enable modding and force a redraw of all game objects if it was previously disabled.").BuiltString); _chat.Print(new SeStringBuilder() .AddCommand("disable", "Disable modding and force a redraw of all game objects if it was previously enabled.").BuiltString); _chat.Print(new SeStringBuilder().AddCommand("toggle", "Toggle modding and force a redraw of all game objects.") .BuiltString); _chat.Print(new SeStringBuilder().AddCommand("reload", "Rediscover the mod directory and reload all mods.").BuiltString); _chat.Print(new SeStringBuilder() .AddCommand("redraw", "Redraw all game objects. Specify a placeholder or a name to redraw specific objects.").BuiltString); _chat.Print(new SeStringBuilder() .AddCommand("lockui", "Toggle the locked state of the main Penumbra window. Can be used with [on|off] to force specific state.") .BuiltString); _chat.Print(new SeStringBuilder().AddCommand("size", "Reset the minimum config window size to its default values.").BuiltString); _chat.Print(new SeStringBuilder() .AddCommand("debug", "Toggle debug mode for Penumbra. Can be used with [on|off] to force specific state.").BuiltString); _chat.Print(new SeStringBuilder() .AddCommand("collection", "Change your active collection setup. Use without further parameters for more detailed help.") .BuiltString); _chat.Print(new SeStringBuilder() .AddCommand("mod", "Change a specific mods settings. Use without further parameters for more detailed help.").BuiltString); _chat.Print(new SeStringBuilder() .AddCommand("bulktag", "Change multiple mods settings based on their tags. Use without further parameters for more detailed help.") .BuiltString); return true; } private bool ToggleWindow(string arguments) { var value = ParseTrueFalseToggle(arguments) ?? !_configWindow.IsOpen; if (value == _configWindow.IsOpen) return false; _configWindow.Toggle(); return true; } private bool Reload(string _) { _modManager.DiscoverMods(); Print($"Reloaded Penumbra mods. You have {_modManager.Count} mods."); return true; } private bool Redraw(string arguments) { if (arguments.Length > 0) _redrawService.RedrawObject(arguments, RedrawType.Redraw); else _redrawService.RedrawAll(RedrawType.Redraw); return true; } private bool SetDebug(string arguments) { var value = ParseTrueFalseToggle(arguments) ?? !_config.DebugMode; if (value == _config.DebugMode) return false; Print(value ? "Debug mode enabled." : "Debug mode disabled."); _config.DebugMode = value; _config.Save(); return true; } private bool SetPenumbraState(string _, bool? newValue) { var value = newValue ?? !_config.EnableMods; if (value == _config.EnableMods) { Print(value ? "Your mods are already enabled. To disable your mods, please run the following command instead: /penumbra disable" : "Your mods are already disabled. To enable your mods, please run the following command instead: /penumbra enable"); return false; } Print(value ? "Your mods have been enabled." : "Your mods have been disabled."); return _penumbra.SetEnabled(value); } private bool SetUiLockState(string arguments) { var value = ParseTrueFalseToggle(arguments) ?? !_config.Ephemeral.FixMainWindow; if (value == _config.Ephemeral.FixMainWindow) return false; if (value) { Print("Penumbra UI locked in place."); _configWindow.Flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize; } else { Print("Penumbra UI unlocked."); _configWindow.Flags &= ~(ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize); } _config.Ephemeral.FixMainWindow = value; _config.Ephemeral.Save(); return true; } private bool SetUiMinimumSize(string _) { if (_config.MinimumSize.X == Configuration.Constants.MinimumSizeX && _config.MinimumSize.Y == Configuration.Constants.MinimumSizeY) return false; _config.MinimumSize.X = Configuration.Constants.MinimumSizeX; _config.MinimumSize.Y = Configuration.Constants.MinimumSizeY; _config.Save(); return true; } private bool SetCollection(string arguments) { if (arguments.Length == 0) { _chat.Print(new SeStringBuilder().AddText("Use with /penumbra collection ").AddBlue("[Collection Type]") .AddText(" | ").AddYellow("[Collection Name]") .AddText(" | ").AddGreen("").BuiltString); _chat.Print(new SeStringBuilder().AddText(" 》 Valid Collection Types are ").AddBlue("Base").AddText(", ") .AddBlue("Ui").AddText(", ") .AddBlue("Selected").AddText(", ") .AddBlue("Individual").AddText(", and all those selectable in Character Groups.").BuiltString); _chat.Print(new SeStringBuilder().AddText(" 》 Valid Collection Names are ").AddYellow("None") .AddText(", all collections you have created by their full names, and ").AddYellow("Delete") .AddText(" to remove assignments (not valid for all types).") .BuiltString); _chat.Print(new SeStringBuilder().AddText(" 》 If the type is ").AddBlue("Individual") .AddText(" you need to specify an individual with an identifier of the form:").BuiltString); _chat.Print(new SeStringBuilder().AddText(" 》》》 ").AddGreen("").AddText(" or ").AddGreen("") .AddText(" or ").AddGreen("") .AddText(" or ").AddGreen("") .AddText(" as placeholders for your character, your target, your mouseover or your focus, if they exist.").BuiltString); _chat.Print(new SeStringBuilder().AddText(" 》》》 ").AddGreen("p").AddText(" | ") .AddWhite("[Player Name]@") .AddText(", if no @ is provided, Any World is used.").BuiltString); _chat.Print(new SeStringBuilder().AddText(" 》》》 ").AddGreen("r").AddText(" | ").AddWhite("[Retainer Name]") .BuiltString); _chat.Print(new SeStringBuilder().AddText(" 》》》 ").AddGreen("n").AddText(" | ").AddPurple("[NPC Type]") .AddText(" : ") .AddRed("[NPC Name]").AddText(", where NPC Type can be ").AddInitialPurple("Mount").AddInitialPurple("Companion") .AddInitialPurple("Accessory") .AddInitialPurple("Event NPC").AddText("or ") .AddInitialPurple("Battle NPC", false).AddText(".").BuiltString); _chat.Print(new SeStringBuilder().AddText(" 》》》 ").AddGreen("o").AddText(" | ").AddPurple("[NPC Type]") .AddText(" : ") .AddRed("[NPC Name]").AddText(" | ").AddWhite("[Player Name]@").AddText(".").BuiltString); return true; } var split = arguments.Split('|', 3, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var typeName = split[0]; if (!CollectionTypeExtensions.TryParse(typeName, out var type)) { _chat.Print(new SeStringBuilder().AddText("The argument ").AddRed(typeName, true) .AddText(" is not a valid collection type.").BuiltString); return false; } if (split.Length == 1) { _chat.Print("There was no collection name provided."); return false; } if (!GetModCollection(split[1], out var collection)) return false; var identifiers = Array.Empty(); if (type is CollectionType.Individual) { if (split.Length == 2) { _chat.Print( "Setting an individual collection requires a collection name and an identifier, but no identifier was provided."); return false; } try { if (_redrawService.GetName(split[2].ToLowerInvariant(), out var obj)) { var identifier = _actors.FromObject(obj, false, true, true); if (!identifier.IsValid) { _chat.Print(new SeStringBuilder().AddText("The placeholder ").AddGreen(split[2]) .AddText(" did not resolve to a game object with a valid identifier.").BuiltString); return false; } identifiers = new[] { identifier, }; } else { identifiers = _actors.FromUserString(split[2], false); } } catch (ActorIdentifierFactory.IdentifierParseError e) { _chat.Print(new SeStringBuilder().AddText("The argument ").AddRed(split[2], true) .AddText($" could not be converted to an identifier. {e.Message}") .BuiltString); return false; } } var anySuccess = false; foreach (var identifier in identifiers.Distinct().DefaultIfEmpty(ActorIdentifier.Invalid)) { var oldCollection = _collectionManager.Active.ByType(type, identifier); if (collection == oldCollection) { _chat.Print(collection == null ? $"The {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}" : string.Empty)} is already unassigned" : $"{collection.Name} already is the {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}"); continue; } var individualIndex = _collectionManager.Active.Individuals.Index(identifier); if (oldCollection == null) { if (type.IsSpecial()) { _collectionManager.Active.CreateSpecialCollection(type); } else if (identifier.IsValid) { var identifierGroup = _collectionManager.Active.Individuals.GetGroup(identifier); individualIndex = _collectionManager.Active.Individuals.Count; _collectionManager.Active.CreateIndividualCollection(identifierGroup); } } else if (collection == null) { if (type.IsSpecial()) { _collectionManager.Active.RemoveSpecialCollection(type); } else if (individualIndex >= 0) { _collectionManager.Active.RemoveIndividualCollection(individualIndex); } else { _chat.Print( $"Can not remove the {type.ToName()} Collection assignment {(identifier.IsValid ? $" for {identifier}." : ".")}"); continue; } Print( $"Removed {oldCollection.Name} as {type.ToName()} Collection assignment {(identifier.IsValid ? $" for {identifier}." : ".")}"); anySuccess = true; continue; } _collectionManager.Active.SetCollection(collection!, type, individualIndex); Print($"Assigned {collection!.Name} as {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}"); } return anySuccess; } private bool SetMod(string arguments) { if (arguments.Length == 0) { var seString = new SeStringBuilder() .AddText("Use with /penumbra mod ").AddBlue("[enable|disable|inherit|toggle]").AddText(" ").AddYellow("[Collection Name]") .AddText(" | ") .AddPurple("[Mod Name or Mod Directory Name]"); _chat.Print(seString.BuiltString); return true; } var split = arguments.Split(' ', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); var nameSplit = split.Length != 2 ? Array.Empty() : split[1].Split('|', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); if (nameSplit.Length != 2) { _chat.Print("Not enough arguments provided."); return false; } var state = ConvertToSettingState(split[0]); if (state == -1) { _chat.Print(new SeStringBuilder().AddRed(split[0], true).AddText(" is not a valid type of setting.").BuiltString); return false; } if (!GetModCollection(nameSplit[0], out var collection) || collection == ModCollection.Empty) return false; if (!_modManager.TryGetMod(nameSplit[1], nameSplit[1], out var mod)) { _chat.Print(new SeStringBuilder().AddText("The mod ").AddRed(nameSplit[1], true).AddText(" does not exist.") .BuiltString); return false; } if (HandleModState(state, collection!, mod)) return true; _chat.Print(new SeStringBuilder().AddText("Mod ").AddPurple(mod.Name, true) .AddText("already had the desired state in collection ") .AddYellow(collection!.Name, true).AddText(".").BuiltString); return false; } private enum TagType { Local, Mod, Both, } private bool SetTag(string arguments) { if (arguments.Length == 0) { var seString = new SeStringBuilder() .AddText("Use with /penumbra bulktag ").AddBlue("[enable|disable|toggle|inherit]").AddText(" ").AddYellow("[Collection Name]") .AddText(" | ") .AddPurple("[Tag]"); _chat.Print(seString.BuiltString); var tagString = new SeStringBuilder() .AddText(" 》 ") .AddPurple("[Tag]") .AddText(" is only Local tags by default, but can be prefixed with '") .AddWhite("b:") .AddText("' for both types of tags or '") .AddWhite("m:") .AddText("' for only Mod tags."); _chat.Print(tagString.BuiltString); return true; } var split = arguments.Split(' ', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); var nameSplit = split.Length != 2 ? Array.Empty() : split[1].Split('|', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); if (nameSplit.Length != 2) { _chat.Print("Not enough arguments provided."); return false; } var state = ConvertToSettingState(split[0]); if (state == -1) { _chat.Print(new SeStringBuilder().AddRed(split[0], true).AddText(" is not a valid type of setting.").BuiltString); return false; } if (!GetModCollection(nameSplit[0], out var collection) || collection == ModCollection.Empty) return false; var tagType = nameSplit[1].Length < 3 || nameSplit[1][1] != ':' ? TagType.Local : nameSplit[1][0] switch { 'b' => TagType.Both, 'm' => TagType.Mod, _ => TagType.Local, }; var tag = tagType is TagType.Local ? nameSplit[1] : nameSplit[1][2..]; var mods = tagType switch { TagType.Local => _modManager.Where(m => m.LocalTags.Contains(tag, StringComparer.OrdinalIgnoreCase)).ToList(), TagType.Mod => _modManager.Where(m => m.ModTags.Contains(tag, StringComparer.OrdinalIgnoreCase)).ToList(), _ => _modManager.Where(m => m.LocalTags.Concat(m.ModTags).Contains(tag, StringComparer.OrdinalIgnoreCase)).ToList(), }; if (mods.Count == 0) { _chat.Print(new SeStringBuilder().AddText("The tag ").AddRed(tag, true).AddText(" does not match any mods.") .BuiltString); return false; } var changes = false; foreach (var mod in mods) changes |= HandleModState(state, collection!, mod); if (!changes) Print(() => new SeStringBuilder().AddText("No mod states were changed in collection ").AddYellow(collection!.Name, true) .AddText(".").BuiltString); return true; } private bool GetModCollection(string collectionName, out ModCollection? collection) { var lowerName = collectionName.ToLowerInvariant(); if (lowerName == "delete") { collection = null; return true; } collection = string.Equals(lowerName, ModCollection.Empty.Name, StringComparison.OrdinalIgnoreCase) ? ModCollection.Empty : _collectionManager.Storage.ByIdentifier(lowerName, out var c) ? c : null; if (collection != null) return true; _chat.Print(new SeStringBuilder().AddText("The collection ").AddRed(collectionName, true).AddText(" does not exist.") .BuiltString); return false; } private static bool? ParseTrueFalseToggle(string value) => value.ToLowerInvariant() switch { "0" => false, "false" => false, "off" => false, "disable" => false, "disabled" => false, "1" => true, "true" => true, "on" => true, "enable" => true, "enabled" => true, _ => null, }; private static int ConvertToSettingState(string text) => text.ToLowerInvariant() switch { "enable" => 0, "enabled" => 0, "disable" => 1, "disabled" => 1, "toggle" => 2, "inherit" => 3, "inherited" => 3, _ => -1, }; private bool HandleModState(int settingState, ModCollection collection, Mod mod) { var settings = collection.Settings[mod.Index]; switch (settingState) { case 0: if (!_collectionEditor.SetModState(collection, mod, true)) return false; Print(() => new SeStringBuilder().AddText("Enabled mod ").AddPurple(mod.Name, true).AddText(" in collection ") .AddYellow(collection.Name, true) .AddText(".").BuiltString); return true; case 1: if (!_collectionEditor.SetModState(collection, mod, false)) return false; Print(() => new SeStringBuilder().AddText("Disabled mod ").AddPurple(mod.Name, true).AddText(" in collection ") .AddYellow(collection.Name, true) .AddText(".").BuiltString); return true; case 2: var setting = !(settings?.Enabled ?? false); if (!_collectionEditor.SetModState(collection, mod, setting)) return false; Print(() => new SeStringBuilder().AddText(setting ? "Enabled mod " : "Disabled mod ").AddPurple(mod.Name, true) .AddText(" in collection ") .AddYellow(collection.Name, true) .AddText(".").BuiltString); return true; case 3: if (!_collectionEditor.SetModInheritance(collection, mod, true)) return false; Print(() => new SeStringBuilder().AddText("Set mod ").AddPurple(mod.Name, true).AddText(" in collection ") .AddYellow(collection.Name, true) .AddText(" to inherit.").BuiltString); return true; } return false; } private void Print(string text) { if (_config.PrintSuccessfulCommandsToChat) _chat.Print(text); } private void Print(DefaultInterpolatedStringHandler text) { if (_config.PrintSuccessfulCommandsToChat) _chat.Print(text.ToStringAndClear()); } private void Print(Func text) { if (_config.PrintSuccessfulCommandsToChat) _chat.Print(text()); } private bool HandleKnowledge(string arguments) { _knowledgeWindow.Toggle(); return true; } }