From 6f356105cc8458c754f14a8d863c327a2cd370d3 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 20 Dec 2022 20:17:18 +0100 Subject: [PATCH] Add better chat command handling, help, and option to set basic mod state. --- Penumbra.GameData/Actors/ActorManager.Data.cs | 9 + .../Actors/ActorManager.Identifiers.cs | 143 +++++- .../Collections/CollectionManager.Active.cs | 2 +- Penumbra/Collections/CollectionType.cs | 46 +- Penumbra/Collections/IndividualCollections.cs | 10 +- Penumbra/CommandHandler.cs | 474 ++++++++++++++++++ Penumbra/Penumbra.cs | 262 ++-------- 7 files changed, 719 insertions(+), 227 deletions(-) create mode 100644 Penumbra/CommandHandler.cs diff --git a/Penumbra.GameData/Actors/ActorManager.Data.cs b/Penumbra.GameData/Actors/ActorManager.Data.cs index 3d962d0e..77bd13bf 100644 --- a/Penumbra.GameData/Actors/ActorManager.Data.cs +++ b/Penumbra.GameData/Actors/ActorManager.Data.cs @@ -60,6 +60,15 @@ public sealed partial class ActorManager : IDisposable public string ToWorldName(ushort worldId) => worldId == ushort.MaxValue ? "Any World" : Worlds.TryGetValue(worldId, out var name) ? name : "Invalid"; + /// + /// Return the world id corresponding to the given name. + /// + /// ushort.MaxValue if the name is empty, 0 if it is not a valid world, or the worlds id. + public ushort ToWorldId(string worldName) + => worldName.Length != 0 + ? Worlds.FirstOrDefault(kvp => string.Equals(kvp.Value, worldName, StringComparison.OrdinalIgnoreCase), default).Key + : ushort.MaxValue; + /// /// Convert a given ID for a certain ObjectKind to a name. /// diff --git a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs index 47c03b82..2483fc5e 100644 --- a/Penumbra.GameData/Actors/ActorManager.Identifiers.cs +++ b/Penumbra.GameData/Actors/ActorManager.Identifiers.cs @@ -1,5 +1,6 @@ using System; -using System.Diagnostics.CodeAnalysis; +using System.Collections.Generic; +using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Types; using Newtonsoft.Json.Linq; @@ -106,6 +107,139 @@ public partial class ActorManager return main; } + public class IdentifierParseError : Exception + { + public IdentifierParseError(string reason) + : base(reason) + { } + } + + public ActorIdentifier FromUserString(string userString) + { + if (userString.Length == 0) + throw new IdentifierParseError("The identifier string was empty."); + + var split = userString.Split('|', 3, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (split.Length < 2) + throw new IdentifierParseError($"The identifier string {userString} does not contain a type and a value."); + + var type = IdentifierType.Invalid; + var playerName = ByteString.Empty; + ushort worldId = 0; + var kind = ObjectKind.Player; + var objectId = 0u; + + (ByteString, ushort) ParsePlayer(string player) + { + var parts = player.Split('@', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (!VerifyPlayerName(parts[0])) + throw new IdentifierParseError($"{parts[0]} is not a valid player name."); + if (!ByteString.FromString(parts[0], out var p, false)) + throw new IdentifierParseError($"The player string {parts[0]} contains invalid symbols."); + + var world = parts.Length == 2 + ? Data.ToWorldId(parts[1]) + : ushort.MaxValue; + + if (!VerifyWorld(world)) + throw new IdentifierParseError($"{parts[1]} is not a valid world name."); + + return (p, world); + } + + (ObjectKind, uint) ParseNpc(string npc) + { + var split = npc.Split(':', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (split.Length != 2) + throw new IdentifierParseError("NPCs need to be specified by '[Object Type]:[NPC Name]'."); + + static bool FindDataId(string name, IReadOnlyDictionary data, out uint dataId) + { + var kvp = data.FirstOrDefault(kvp => kvp.Value.Equals(name, StringComparison.OrdinalIgnoreCase), + new KeyValuePair(uint.MaxValue, string.Empty)); + dataId = kvp.Key; + return kvp.Value.Length > 0; + } + + switch (split[0].ToLowerInvariant()) + { + case "m": + case "mount": + return FindDataId(split[1], Data.Mounts, out var id) + ? (ObjectKind.MountType, id) + : throw new IdentifierParseError($"Could not identify a Mount named {split[1]}."); + case "c": + case "companion": + case "minion": + case "mini": + return FindDataId(split[1], Data.Companions, out id) + ? (ObjectKind.Companion, id) + : throw new IdentifierParseError($"Could not identify a Minion named {split[1]}."); + case "a": + case "o": + case "accessory": + case "ornament": + // TODO: Objectkind ornament. + return FindDataId(split[1], Data.Ornaments, out id) + ? ((ObjectKind)15, id) + : throw new IdentifierParseError($"Could not identify an Accessory named {split[1]}."); + case "e": + case "enpc": + case "eventnpc": + case "event npc": + return FindDataId(split[1], Data.ENpcs, out id) + ? (ObjectKind.EventNpc, id) + : throw new IdentifierParseError($"Could not identify an Event NPC named {split[1]}."); + case "b": + case "bnpc": + case "battlenpc": + case "battle npc": + return FindDataId(split[1], Data.BNpcs, out id) + ? (ObjectKind.BattleNpc, id) + : throw new IdentifierParseError($"Could not identify a Battle NPC named {split[1]}."); + default: + throw new IdentifierParseError($"The argument {split[0]} is not a valid NPC Type."); + } + } + + switch (split[0].ToLowerInvariant()) + { + case "p": + case "player": + type = IdentifierType.Player; + (playerName, worldId) = ParsePlayer(split[1]); + break; + case "r": + case "retainer": + type = IdentifierType.Retainer; + if (!VerifyRetainerName(split[1])) + throw new IdentifierParseError($"{split[1]} is not a valid player name."); + if (!ByteString.FromString(split[1], out playerName, false)) + throw new IdentifierParseError($"The retainer string {split[1]} contains invalid symbols."); + + break; + case "n": + case "npc": + type = IdentifierType.Npc; + (kind, objectId) = ParseNpc(split[1]); + break; + case "o": + case "owned": + if (split.Length < 3) + throw new IdentifierParseError( + "Owned NPCs need a NPC and a player, separated by '|', but only one was provided."); + type = IdentifierType.Owned; + (kind, objectId) = ParseNpc(split[1]); + (playerName, worldId) = ParsePlayer(split[2]); + break; + default: + throw new IdentifierParseError( + $"{split[0]} is not a valid identifier type. Valid types are [P]layer, [R]etainer, [N]PC, or [O]wned"); + } + + return CreateIndividualUnchecked(type, playerName, worldId, kind, objectId); + } + /// /// Compute an ActorIdentifier from a GameObject. If check is true, the values are checked for validity. /// @@ -150,11 +284,11 @@ public partial class ActorManager ? CreateOwned(name, homeWorld, ObjectKind.BattleNpc, nameId) : CreateIndividualUnchecked(IdentifierType.Owned, name, homeWorld, ObjectKind.BattleNpc, nameId); } - + // Hack to support Anamnesis changing ObjectKind for NPC faces. if (nameId == 0 && allowPlayerNpc) { - var name = new ByteString(actor->Name); + var name = new ByteString(actor->Name); if (!name.IsEmpty) { var homeWorld = ((Character*)actor)->HomeWorld; @@ -244,7 +378,8 @@ public partial class ActorManager public unsafe ActorIdentifier FromObject(GameObject? actor, out FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* owner, bool allowPlayerNpc, bool check) - => FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)(actor?.Address ?? IntPtr.Zero), out owner, allowPlayerNpc, check); + => FromObject((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)(actor?.Address ?? IntPtr.Zero), out owner, allowPlayerNpc, + check); public unsafe ActorIdentifier FromObject(GameObject? actor, bool allowPlayerNpc, bool check) => FromObject(actor, out _, allowPlayerNpc, check); diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 355c9297..79c6e83a 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -64,7 +64,7 @@ public partial class ModCollection CollectionType.Default => Default, CollectionType.Interface => Interface, CollectionType.Current => Current, - CollectionType.Individual => identifier.IsValid ? Individuals.TryGetCollection( identifier, out var c ) ? c : null : null, + CollectionType.Individual => identifier.IsValid && Individuals.Individuals.TryGetValue( identifier, out var c ) ? c : null, _ => null, }; } diff --git a/Penumbra/Collections/CollectionType.cs b/Penumbra/Collections/CollectionType.cs index 30da217d..eab5d6eb 100644 --- a/Penumbra/Collections/CollectionType.cs +++ b/Penumbra/Collections/CollectionType.cs @@ -213,6 +213,50 @@ public static class CollectionTypeExtensions }; } + public static bool TryParse( string text, out CollectionType type ) + { + if( Enum.TryParse( text, true, out type ) ) + return type is not CollectionType.Inactive and not CollectionType.Temporary; + + if( string.Equals( text, "character", StringComparison.OrdinalIgnoreCase ) ) + { + type = CollectionType.Individual; + return true; + } + + if( string.Equals( text, "base", StringComparison.OrdinalIgnoreCase ) ) + { + type = CollectionType.Default; + return true; + } + + if( string.Equals( text, "ui", StringComparison.OrdinalIgnoreCase ) ) + { + type = CollectionType.Interface; + return true; + } + + if( string.Equals( text, "selected", StringComparison.OrdinalIgnoreCase ) ) + { + type = CollectionType.Current; + return true; + } + + foreach( var t in Enum.GetValues< CollectionType >() ) + { + if( t is CollectionType.Inactive or CollectionType.Temporary ) + continue; + + if( string.Equals( text, t.ToName(), StringComparison.OrdinalIgnoreCase ) ) + { + type = t; + return true; + } + } + + return false; + } + public static string ToName( this CollectionType collectionType ) => collectionType switch { @@ -288,7 +332,7 @@ public static class CollectionTypeExtensions CollectionType.Inactive => "Collection", CollectionType.Default => "Default", CollectionType.Interface => "Interface", - CollectionType.Individual => "Character", + CollectionType.Individual => "Individual", CollectionType.Current => "Current", _ => string.Empty, }; diff --git a/Penumbra/Collections/IndividualCollections.cs b/Penumbra/Collections/IndividualCollections.cs index d2d8bb70..3d5f3fdd 100644 --- a/Penumbra/Collections/IndividualCollections.cs +++ b/Penumbra/Collections/IndividualCollections.cs @@ -177,10 +177,10 @@ public sealed partial class IndividualCollections } internal bool Delete( ActorIdentifier identifier ) - => Delete( DisplayString( identifier ) ); + => Delete( Index( identifier ) ); internal bool Delete( string displayName ) - => Delete( _assignments.FindIndex( t => t.DisplayName.Equals( displayName, StringComparison.OrdinalIgnoreCase ) ) ); + => Delete( Index( displayName ) ); internal bool Delete( int displayIndex ) { @@ -202,6 +202,12 @@ public sealed partial class IndividualCollections internal bool Move( int from, int to ) => _assignments.Move( from, to ); + internal int Index( string displayName ) + => _assignments.FindIndex( t => t.DisplayName.Equals( displayName, StringComparison.OrdinalIgnoreCase ) ); + + internal int Index( ActorIdentifier identifier ) + => identifier.IsValid ? Index( DisplayString( identifier ) ) : -1; + private string DisplayString( ActorIdentifier identifier ) { return identifier.Type switch diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs new file mode 100644 index 00000000..2669a9f5 --- /dev/null +++ b/Penumbra/CommandHandler.cs @@ -0,0 +1,474 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Dalamud.Game.Command; +using Dalamud.Game.Text.SeStringHandling; +using ImGuiNET; +using Penumbra.Api.Enums; +using Penumbra.Collections; +using Penumbra.GameData.Actors; +using Penumbra.Interop; +using Penumbra.Mods; +using Penumbra.UI; + +namespace Penumbra; + +public static class SeStringBuilderExtensions +{ + public const ushort Green = 504; + public const ushort Yellow = 31; + public const ushort Red = 534; + public const ushort Blue = 517; + public const ushort White = 1; + public const ushort Purple = 541; + + public static SeStringBuilder AddText( this SeStringBuilder sb, string text, int color, bool brackets = false ) + => sb.AddUiForeground( ( ushort )color ).AddText( brackets ? $"[{text}]" : text ).AddUiForegroundOff(); + + public static SeStringBuilder AddGreen( this SeStringBuilder sb, string text, bool brackets = false ) + => AddText( sb, text, Green, brackets ); + + public static SeStringBuilder AddYellow( this SeStringBuilder sb, string text, bool brackets = false ) + => AddText( sb, text, Yellow, brackets ); + + public static SeStringBuilder AddRed( this SeStringBuilder sb, string text, bool brackets = false ) + => AddText( sb, text, Red, brackets ); + + public static SeStringBuilder AddBlue( this SeStringBuilder sb, string text, bool brackets = false ) + => AddText( sb, text, Blue, brackets ); + + public static SeStringBuilder AddWhite( this SeStringBuilder sb, string text, bool brackets = false ) + => AddText( sb, text, White, brackets ); + + public static SeStringBuilder AddPurple( this SeStringBuilder sb, string text, bool brackets = false ) + => AddText( sb, text, Purple, brackets ); + + public static SeStringBuilder AddCommand( this SeStringBuilder sb, string command, string description ) + => sb.AddText( " 》 " ) + .AddBlue( command ) + .AddText( $" - {description}" ); + + public static SeStringBuilder AddInitialPurple( this SeStringBuilder sb, string word, bool withComma = true ) + => sb.AddPurple( $"[{word[ 0 ]}]" ) + .AddText( withComma ? $"{word[ 1.. ]}, " : word[ 1.. ] ); +} + +public class CommandHandler : IDisposable +{ + private const string CommandName = "/penumbra"; + + private readonly CommandManager _commandManager; + private readonly ObjectReloader _objectReloader; + private readonly Configuration _config; + private readonly Penumbra _penumbra; + private readonly ConfigWindow _configWindow; + private readonly ActorManager _actors; + private readonly Mod.Manager _modManager; + private readonly ModCollection.Manager _collectionManager; + + public CommandHandler( CommandManager commandManager, ObjectReloader objectReloader, Configuration config, Penumbra penumbra, ConfigWindow configWindow, Mod.Manager modManager, + ModCollection.Manager collectionManager, ActorManager actors ) + { + _commandManager = commandManager; + _objectReloader = objectReloader; + _config = config; + _penumbra = penumbra; + _configWindow = configWindow; + _modManager = modManager; + _collectionManager = collectionManager; + _actors = actors; + _commandManager.AddHandler( CommandName, new CommandInfo( OnCommand ) ); + } + + 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; + + var _ = 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 ), + "debug" => SetDebug( arguments ), + "collection" => SetCollection( arguments ), + "mod" => SetMod( arguments ), + _ => PrintHelp( argumentList[ 0 ] ), + }; + } + + private static bool PrintHelp( string arguments ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "The given argument " ).AddRed( arguments, true ).AddText( " is not valid. Valid arguments are:" ).BuiltString ); + Dalamud.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 ); + Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "enable", "Enable modding and force a redraw of all game objects if it was previously disabled." ).BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "disable", "Disable modding and force a redraw of all game objects if it was previously enabled." ).BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "toggle", "Toggle modding and force a redraw of all game objects." ).BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "reload", "Rediscover the mod directory and reload all mods." ).BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "redraw", "Redraw all game objects. Specify a placeholder or a name to redraw specific objects." ).BuiltString ); + Dalamud.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 ); + Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "debug", "Toggle debug mode for Penumbra. Can be used with [on|off] to force specific state." ).BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "collection", "Change your active collection setup. Use without further parameters for more detailed help." ) + .BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddCommand( "mod", "Change a specific mods settings. 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(); + Dalamud.Chat.Print( $"Reloaded Penumbra mods. You have {_modManager.Count} mods." ); + return true; + } + + private bool Redraw( string arguments ) + { + if( arguments.Length > 0 ) + { + _objectReloader.RedrawObject( arguments, RedrawType.Redraw ); + } + else + { + _objectReloader.RedrawAll( RedrawType.Redraw ); + } + + return true; + } + + private bool SetDebug( string arguments ) + { + var value = ParseTrueFalseToggle( arguments ) ?? !_config.DebugMode; + if( value == _config.DebugMode ) + { + return false; + } + + Dalamud.Chat.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 ) + { + Dalamud.Chat.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; + } + + Dalamud.Chat.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.FixMainWindow; + if( value == _config.FixMainWindow ) + { + return false; + } + + if( value ) + { + Dalamud.Chat.Print( "Penumbra UI locked in place." ); + _configWindow.Flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize; + } + else + { + Dalamud.Chat.Print( "Penumbra UI unlocked." ); + _configWindow.Flags &= ~( ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize ); + } + + _config.FixMainWindow = value; + _config.Save(); + return true; + } + + private bool SetCollection( string arguments ) + { + if( arguments.Length == 0 ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "Use with /penumbra collection " ).AddBlue( "[Collection Type]" ).AddText( " | " ).AddYellow( "[Collection Name]" ) + .AddText( " | " ).AddGreen( "" ).BuiltString ); + Dalamud.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 ); + Dalamud.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 ); + Dalamud.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 ); + Dalamud.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "p" ).AddText( " | " ).AddWhite( "[Player Name]@" ) + .AddText( ", if no @ is provided, Any World is used." ).BuiltString ); + Dalamud.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "r" ).AddText( " | " ).AddWhite( "[Retainer Name]" ).BuiltString ); + Dalamud.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 ); + Dalamud.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 ) ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "The argument " ).AddRed( typeName, true ).AddText( " is not a valid collection type." ).BuiltString ); + return false; + } + + if( split.Length == 1 ) + { + Dalamud.Chat.Print( "There was no collection name provided." ); + return false; + } + + if( !GetModCollection( split[ 1 ], out var collection ) ) + { + return false; + } + + var identifier = ActorIdentifier.Invalid; + if( type is CollectionType.Individual ) + { + if( split.Length == 2 ) + { + Dalamud.Chat.Print( "Setting an individual collection requires a collection name and an identifier, but no identifier was provided." ); + return false; + } + + try + { + identifier = _actors.FromUserString( split[ 2 ] ); + } + catch( ActorManager.IdentifierParseError e ) + { + Dalamud.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 oldCollection = _collectionManager.ByType( type, identifier ); + if( collection == oldCollection ) + { + Dalamud.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}." : "." )}" ); + return false; + } + + var individualIndex = _collectionManager.Individuals.Index( identifier ); + + if( oldCollection == null ) + { + if( type.IsSpecial() ) + { + _collectionManager.CreateSpecialCollection( type ); + } + else if( identifier.IsValid ) + { + var identifiers = _collectionManager.Individuals.GetGroup( identifier ); + individualIndex = _collectionManager.Individuals.Count; + _collectionManager.CreateIndividualCollection( identifiers ); + } + } + else if( collection == null ) + { + if( type.IsSpecial() ) + { + _collectionManager.RemoveSpecialCollection( type ); + } + else if( individualIndex >= 0 ) + { + _collectionManager.RemoveIndividualCollection( individualIndex ); + } + else + { + Dalamud.Chat.Print( $"Can not remove the {type.ToName()} Collection assignment {( identifier.IsValid ? $" for {identifier}." : "." )}" ); + return false; + } + + Dalamud.Chat.Print( $"Removed {oldCollection.Name} as {type.ToName()} Collection assignment {( identifier.IsValid ? $" for {identifier}." : "." )}" ); + return true; + } + + _collectionManager.SetCollection( collection!, type, individualIndex ); + Dalamud.Chat.Print( $"Assigned {collection!.Name} as {type.ToName()} Collection{( identifier.IsValid ? $" for {identifier}." : "." )}" ); + return true; + } + + private bool SetMod( string arguments ) + { + if( arguments.Length == 0 ) + { + var seString = new SeStringBuilder() + .AddText( "Use with /penumbra mod " ).AddBlue( "[enable|disable|inherit|toggle]" ).AddYellow( "[Collection Name]" ).AddText( " | " ) + .AddPurple( "[Mod Name or Mod Directory Name]" ); + Dalamud.Chat.Print( seString.BuiltString ); + return true; + } + + var split = arguments.Split( ' ', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries ); + var nameSplit = split.Length != 2 ? Array.Empty< string >() : split[ 1 ].Split( '|', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries ); + if( nameSplit.Length != 2 ) + { + Dalamud.Chat.Print( "Not enough arguments provided." ); + return false; + } + + var state = split[ 0 ].ToLowerInvariant() switch + { + "enable" => 0, + "enabled" => 0, + "disable" => 1, + "disabled" => 1, + "toggle" => 2, + "inherit" => 3, + "inherited" => 3, + _ => -1, + }; + if( state == -1 ) + { + Dalamud.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 ) ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "The mod " ).AddRed( nameSplit[ 1 ], true ).AddText( " does not exist." ).BuiltString ); + return false; + } + + var settings = collection.Settings[ mod.Index ]; + switch( state ) + { + case 0: + if( collection.SetModState( mod.Index, true ) ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "Enabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ).AddYellow( collection.Name, true ) + .AddText( "." ).BuiltString ); + return true; + } + + break; + case 1: + if( collection.SetModState( mod.Index, false ) ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "Disabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ).AddYellow( collection.Name, true ) + .AddText( "." ).BuiltString ); + return true; + } + + break; + case 2: + var setting = !( settings?.Enabled ?? false ); + if( collection.SetModState( mod.Index, setting ) ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( setting ? "Enabled mod " : "Disabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ) + .AddYellow( collection.Name, true ) + .AddText( "." ).BuiltString ); + return true; + } + + break; + case 3: + if( collection.SetModInheritance( mod.Index, true ) ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "Set mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ).AddYellow( collection.Name, true ) + .AddText( " to inherit." ).BuiltString ); + return true; + } + + break; + } + + Dalamud.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 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[ lowerName ]; + if( collection == null ) + { + Dalamud.Chat.Print( new SeStringBuilder().AddText( "The collection " ).AddRed( collectionName, true ).AddText( " does not exist." ).BuiltString ); + return false; + } + + return true; + } + + 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, + }; +} \ No newline at end of file diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 4bc7f694..25ad785c 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Reflection; using System.Text; -using Dalamud.Game.Command; using Dalamud.Interface.Windowing; using Dalamud.Plugin; using EmbedIO; @@ -26,7 +25,6 @@ using Penumbra.GameData.Actors; using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; using Penumbra.Mods; -using Penumbra.String; using CharacterUtility = Penumbra.Interop.CharacterUtility; using ResidentResourceManager = Penumbra.Interop.ResidentResourceManager; @@ -41,8 +39,6 @@ public class Penumbra : IDalamudPlugin public string Name => "Penumbra"; - private const string CommandName = "/penumbra"; - public static readonly string Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty; public static readonly string CommitHash = @@ -80,6 +76,7 @@ public class Penumbra : IDalamudPlugin private readonly LaunchButton _launchButton; private readonly WindowSystem _windowSystem; private readonly Changelog _changelog; + private readonly CommandHandler _commandHandler; internal WebServer? WebServer; @@ -116,12 +113,8 @@ public class Penumbra : IDalamudPlugin ObjectReloader = new ObjectReloader(); PathResolver = new PathResolver( ResourceLoader ); - Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand ) - { - HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods", - } ); - SetupInterface( out _configWindow, out _launchButton, out _windowSystem, out _changelog ); + _commandHandler = new CommandHandler( Dalamud.Commands, ObjectReloader, Config, this, _configWindow, ModManager, CollectionManager, Actors ); if( Config.EnableMods ) { @@ -203,54 +196,42 @@ public class Penumbra : IDalamudPlugin public event Action< bool >? EnabledChange; - public bool Enable() - { - if( Config.EnableMods ) - { - return false; - } - - Config.EnableMods = true; - ResourceLoader.EnableReplacements(); - PathResolver.Enable(); - Config.Save(); - if( CharacterUtility.Ready ) - { - CollectionManager.Default.SetFiles(); - ResidentResources.Reload(); - ObjectReloader.RedrawAll( RedrawType.Redraw ); - } - - EnabledChange?.Invoke( true ); - - return true; - } - - public bool Disable() - { - if( !Config.EnableMods ) - { - return false; - } - - Config.EnableMods = false; - ResourceLoader.DisableReplacements(); - PathResolver.Disable(); - Config.Save(); - if( CharacterUtility.Ready ) - { - CharacterUtility.ResetAll(); - ResidentResources.Reload(); - ObjectReloader.RedrawAll( RedrawType.Redraw ); - } - - EnabledChange?.Invoke( false ); - - return true; - } - public bool SetEnabled( bool enabled ) - => enabled ? Enable() : Disable(); + { + if( enabled == Config.EnableMods ) + { + return false; + } + + Config.EnableMods = enabled; + if( enabled ) + { + ResourceLoader.EnableReplacements(); + PathResolver.Enable(); + if( CharacterUtility.Ready ) + { + CollectionManager.Default.SetFiles(); + ResidentResources.Reload(); + ObjectReloader.RedrawAll( RedrawType.Redraw ); + } + } + else + { + ResourceLoader.DisableReplacements(); + PathResolver.Disable(); + if( CharacterUtility.Ready ) + { + CharacterUtility.ResetAll(); + ResidentResources.Reload(); + ObjectReloader.RedrawAll( RedrawType.Redraw ); + } + } + + Config.Save(); + EnabledChange?.Invoke( enabled ); + + return true; + } public void ForceChangelogOpen() => _changelog.ForceOpen = true; @@ -303,181 +284,24 @@ public class Penumbra : IDalamudPlugin public void Dispose() { + ShutdownWebServer(); + IpcProviders?.Dispose(); + Api?.Dispose(); + _commandHandler?.Dispose(); StainManager?.Dispose(); Actors?.Dispose(); Identifier?.Dispose(); Framework?.Dispose(); - ShutdownWebServer(); DisposeInterface(); - IpcProviders?.Dispose(); - Api?.Dispose(); ObjectReloader?.Dispose(); ModFileSystem?.Dispose(); CollectionManager?.Dispose(); - - Dalamud.Commands.RemoveHandler( CommandName ); - PathResolver?.Dispose(); ResourceLogger?.Dispose(); ResourceLoader?.Dispose(); CharacterUtility?.Dispose(); } - public static bool SetCollection( string typeName, string collectionName ) - { - if( !Enum.TryParse< CollectionType >( typeName, true, out var type ) || type == CollectionType.Inactive ) - { - Dalamud.Chat.Print( - "Second command argument is not a valid collection type, the correct command format is: /penumbra collection [| characterName]" ); - return false; - } - - string? characterName = null; - var identifier = ActorIdentifier.Invalid; - if( type is CollectionType.Individual ) - { - var split = collectionName.Split( '|', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries ); - if( split.Length < 2 || split[ 0 ].Length == 0 || split[ 1 ].Length == 0 ) - { - Dalamud.Chat.Print( "You need to provide a collection and a character name in the form of 'collection | name' to set an individual collection." ); - return false; - } - - collectionName = split[ 0 ]; - characterName = split[ 1 ]; - - identifier = Actors.CreatePlayer( ByteString.FromStringUnsafe( characterName, false ), ushort.MaxValue ); - if( !identifier.IsValid ) - { - Dalamud.Chat.Print( $"{characterName} is not a valid character name." ); - return false; - } - } - - collectionName = collectionName.ToLowerInvariant(); - var collection = string.Equals( collectionName, ModCollection.Empty.Name, StringComparison.OrdinalIgnoreCase ) - ? ModCollection.Empty - : CollectionManager[ collectionName ]; - if( collection == null ) - { - Dalamud.Chat.Print( $"The collection {collection} does not exist." ); - return false; - } - - var oldCollection = CollectionManager.ByType( type, identifier ); - if( collection == oldCollection ) - { - Dalamud.Chat.Print( $"{collection.Name} already is the {type.ToName()} Collection." ); - return false; - } - - if( oldCollection == null ) - { - if( type.IsSpecial() ) - { - CollectionManager.CreateSpecialCollection( type ); - } - else if( type is CollectionType.Individual ) - { - CollectionManager.CreateIndividualCollection( identifier ); - } - } - - CollectionManager.SetCollection( collection, type, CollectionManager.Individuals.Count - 1 ); - Dalamud.Chat.Print( $"Set {collection.Name} as {type.ToName()} Collection{( characterName != null ? $" for {characterName}." : "." )}" ); - return true; - } - - private void OnCommand( string command, string rawArgs ) - { - const string modsEnabled = "Your mods have now been enabled."; - const string modsDisabled = "Your mods have now been disabled."; - - var args = rawArgs.Split( new[] { ' ' }, 2 ); - if( args.Length > 0 && args[ 0 ].Length > 0 ) - { - switch( args[ 0 ] ) - { - case "reload": - { - ModManager.DiscoverMods(); - Dalamud.Chat.Print( $"Reloaded Penumbra mods. You have {ModManager.Count} mods." - ); - break; - } - case "redraw": - { - if( args.Length > 1 ) - { - ObjectReloader.RedrawObject( args[ 1 ], RedrawType.Redraw ); - } - else - { - ObjectReloader.RedrawAll( RedrawType.Redraw ); - } - - break; - } - case "debug": - { - Config.DebugMode = true; - Config.Save(); - break; - } - case "enable": - { - Dalamud.Chat.Print( Enable() - ? modsEnabled - : "Your mods are already enabled. To disable your mods, please run the following command instead: /penumbra disable" ); - break; - } - case "disable": - { - Dalamud.Chat.Print( Disable() - ? modsDisabled - : "Your mods are already disabled. To enable your mods, please run the following command instead: /penumbra enable" ); - break; - } - case "toggle": - { - SetEnabled( !Config.EnableMods ); - Dalamud.Chat.Print( Config.EnableMods - ? modsEnabled - : modsDisabled ); - break; - } - case "unfix": - { - Config.FixMainWindow = false; - _configWindow.Flags &= ~( ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize ); - break; - } - case "collection": - { - if( args.Length == 2 ) - { - args = args[ 1 ].Split( new[] { ' ' }, 2 ); - if( args.Length == 2 ) - { - SetCollection( args[ 0 ], args[ 1 ] ); - } - } - else - { - Dalamud.Chat.Print( "Missing arguments, the correct command format is:" - + " /penumbra collection {default} [|characterName]" ); - } - - break; - } - } - - return; - } - - _configWindow.Toggle(); - } - // Collect all relevant files for penumbra configuration. private static IReadOnlyList< FileInfo > PenumbraBackupFiles() { @@ -568,7 +392,7 @@ public class Penumbra : IDalamudPlugin { #if !DEBUG var path = Path.Combine( Dalamud.PluginInterface.DalamudAssetDirectory.Parent?.FullName ?? "INVALIDPATH", "devPlugins", "Penumbra" ); - var dir = new DirectoryInfo( path ); + var dir = new DirectoryInfo( path ); try { @@ -589,7 +413,7 @@ public class Penumbra : IDalamudPlugin { #if !DEBUG var checkedDirectory = Dalamud.PluginInterface.AssemblyLocation.Directory?.Parent?.Parent?.Name; - var ret = checkedDirectory?.Equals( "installedPlugins", StringComparison.OrdinalIgnoreCase ) ?? false; + var ret = checkedDirectory?.Equals( "installedPlugins", StringComparison.OrdinalIgnoreCase ) ?? false; if( !ret ) { Log.Error( $"Penumbra is not correctly installed. Application loaded from \"{Dalamud.PluginInterface.AssemblyLocation.Directory!.FullName}\"." );