From 5fa31d991775eee25af8b68c992ac72082b8e160 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 17 Jul 2023 00:28:21 +0200 Subject: [PATCH] Add Chat Commands. --- Glamourer/Interop/ObjectManager.cs | 24 ++ Glamourer/Services/CommandService.cs | 360 ++++++++++++++++++++++++++- 2 files changed, 374 insertions(+), 10 deletions(-) diff --git a/Glamourer/Interop/ObjectManager.cs b/Glamourer/Interop/ObjectManager.cs index 0a7d0d8..328c4c6 100644 --- a/Glamourer/Interop/ObjectManager.cs +++ b/Glamourer/Interop/ObjectManager.cs @@ -137,6 +137,12 @@ public class ObjectManager : IReadOnlyDictionary public Actor Target => _targets.Target?.Address ?? nint.Zero; + public Actor Focus + => _targets.FocusTarget?.Address ?? nint.Zero; + + public Actor MouseOver + => _targets.MouseOverTarget?.Address ?? nint.Zero; + public (ActorIdentifier Identifier, ActorData Data) PlayerData { get @@ -186,4 +192,22 @@ public class ObjectManager : IReadOnlyDictionary public IEnumerable Values => Identifiers.Values; + + public bool GetName(string lowerName, out Actor actor) + { + (actor, var ret) = lowerName switch + { + "" => (Actor.Null, true), + "" => (Player, true), + "self" => (Player, true), + "" => (Target, true), + "target" => (Target, true), + "" => (Focus, true), + "focus" => (Focus, true), + "" => (MouseOver, true), + "mouseover" => (MouseOver, true), + _ => (Actor.Null, false), + }; + return ret; + } } diff --git a/Glamourer/Services/CommandService.cs b/Glamourer/Services/CommandService.cs index 5595310..af9dab8 100644 --- a/Glamourer/Services/CommandService.cs +++ b/Glamourer/Services/CommandService.cs @@ -1,26 +1,54 @@ using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using Dalamud.Game.Command; +using Dalamud.Game.Gui; +using Dalamud.Game.Text.SeStringHandling; +using Glamourer.Automation; +using Glamourer.Customization; +using Glamourer.Designs; +using Glamourer.Events; using Glamourer.Gui; -using Glamourer.Gui.Tabs; +using Glamourer.Interop; +using Glamourer.State; +using Glamourer.Structs; +using ImGuiNET; +using OtterGui.Classes; +using Penumbra.GameData.Actors; namespace Glamourer.Services; public class CommandService : IDisposable { - private const string HelpString = "[Copy|Apply|Save],[Name or PlaceHolder],"; private const string MainCommandString = "/glamourer"; private const string ApplyCommandString = "/glamour"; - private readonly CommandManager _commands; - private readonly MainWindow _mainWindow; + private readonly CommandManager _commands; + private readonly MainWindow _mainWindow; + private readonly ChatGui _chat; + private readonly ActorService _actors; + private readonly ObjectManager _objects; + private readonly StateManager _stateManager; + private readonly AutoDesignApplier _autoDesignApplier; + private readonly DesignManager _designManager; + private readonly DesignConverter _converter; - public CommandService(CommandManager commands, MainWindow mainWindow) + public CommandService(CommandManager commands, MainWindow mainWindow, ChatGui chat, ActorService actors, ObjectManager objects, + AutoDesignApplier autoDesignApplier, StateManager stateManager, DesignManager designManager, DesignConverter converter) { - _commands = commands; - _mainWindow = mainWindow; + _commands = commands; + _mainWindow = mainWindow; + _chat = chat; + _actors = actors; + _objects = objects; + _autoDesignApplier = autoDesignApplier; + _stateManager = stateManager; + _designManager = designManager; + _converter = converter; - _commands.AddHandler(MainCommandString, new CommandInfo(OnGlamourer) { HelpMessage = "Open or close the Glamourer window." }); - _commands.AddHandler(ApplyCommandString, new CommandInfo(OnGlamour) { HelpMessage = $"Use Glamourer Functions: {HelpString}" }); + _commands.AddHandler(MainCommandString, new CommandInfo(OnGlamourer) { HelpMessage = "Open or close the Glamourer window." }); + _commands.AddHandler(ApplyCommandString, + new CommandInfo(OnGlamour) { HelpMessage = "Use Glamourer Functions. Use with 'help' or '?' for extended help." }); } public void Dispose() @@ -33,5 +61,317 @@ public class CommandService : IDisposable => _mainWindow.Toggle(); private void OnGlamour(string command, string arguments) - { } + { + var argumentList = arguments.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (argumentList.Length < 1) + return; + + var argument = argumentList.Length == 2 ? argumentList[1] : string.Empty; + var _ = argumentList[0].ToLowerInvariant() switch + { + "apply" => Apply(argument), + "reapply" => ReapplyState(argument), + "revert" => Revert(argument), + "reapplyautomation" => ReapplyAutomation(argument), + //"automation" => SetAutomation(argument), + "copy" => CopyState(argument), + "save" => SaveState(argument), + _ => PrintHelp(argumentList[0]), + }; + } + + private bool PrintHelp(string argument) + { + if (!string.Equals(argument, "help", StringComparison.OrdinalIgnoreCase) && argument != "?") + _chat.Print(new SeStringBuilder().AddText("The given argument ").AddRed(argument, true) + .AddText(" is not valid. Valid arguments are:").BuiltString); + else + _chat.Print("Valid arguments for /glamour are:"); + + _chat.Print(new SeStringBuilder().AddCommand("apply", "Applies a given design to a given character. Use without arguments for help.") + .BuiltString); + _chat.Print(new SeStringBuilder() + .AddCommand("reapply", "Re-applies the current supposed state of a given character. Use without arguments for help.").BuiltString); + _chat.Print(new SeStringBuilder().AddCommand("revert", "Reverts a given character to its game state. Use without arguments for help.") + .BuiltString); + _chat.Print(new SeStringBuilder().AddCommand("reapplyautomation", + "Reverts a given character to its supposed state using automated designs. Use without arguments for help.").BuiltString); + _chat.Print(new SeStringBuilder() + .AddCommand("copy", "Copy the current state of a character to clipboard. Use without arguments for help.").BuiltString); + _chat.Print(new SeStringBuilder() + .AddCommand("save", "Save the current state of a character to a named design. Use without arguments for help.").BuiltString); + //_chat.Print(new SeStringBuilder() + // .AddCommand("automation", "Change the configuration for automated designs. Use without arguments for help.").BuiltString); + return true; + } + + // TODO: implement automation changes via chat. + private bool SetAutomation(string arguments) + => true; + + private bool ReapplyAutomation(string argument) + { + if (argument.Length == 0) + { + _chat.Print(new SeStringBuilder().AddText("Use with /glamour reapplyautomation ").AddGreen("[Character Identifier]").BuiltString); + PlayerIdentifierHelp(false); + return true; + } + + if (!IdentifierHandling(argument, out var identifier, false)) + return false; + + _objects.Update(); + if (!_objects.TryGetValue(identifier, out var data)) + return true; + + foreach (var actor in data.Objects) + { + if (_stateManager.GetOrCreate(identifier, actor, out var state)) + { + _autoDesignApplier.ReapplyAutomation(actor, identifier, state); + _stateManager.ReapplyState(actor); + } + } + + return true; + } + + private bool Revert(string argument) + { + if (argument.Length == 0) + { + _chat.Print(new SeStringBuilder().AddText("Use with /glamour revert ").AddGreen("[Character Identifier]").BuiltString); + PlayerIdentifierHelp(false); + return true; + } + + if (!IdentifierHandling(argument, out var identifier, false)) + return false; + + if (_stateManager.TryGetValue(identifier, out var state)) + _stateManager.ResetState(state); + + return true; + } + + private bool ReapplyState(string argument) + { + if (argument.Length == 0) + { + _chat.Print(new SeStringBuilder().AddText("Use with /glamour revert ").AddGreen("[Character Identifier]").BuiltString); + PlayerIdentifierHelp(false); + return true; + } + + if (!IdentifierHandling(argument, out var identifier, false)) + return false; + + _objects.Update(); + if (!_objects.TryGetValue(identifier, out var data)) + return true; + + foreach (var actor in data.Objects) + _stateManager.ReapplyState(actor); + + return true; + } + + private bool Apply(string arguments) + { + var split = arguments.Split('|', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (split.Length != 2) + { + _chat.Print(new SeStringBuilder().AddText("Use with /glamour apply ").AddYellow("[Design Name or Identifier]").AddText(" | ") + .AddGreen("[Character Identifier]").BuiltString); + _chat.Print(new SeStringBuilder() + .AddText(" 》 The design name must match up to case. If multiple designs of that name exist, the first one is chosen.") + .BuiltString); + _chat.Print(new SeStringBuilder() + .AddText( + " 》 If using the design identifier, you need to specify at least 4 characters for it, and the first one starting with the provided characters is chosen.") + .BuiltString); + PlayerIdentifierHelp(false); + } + + if (!GetDesign(split[0], out var design) || !IdentifierHandling(split[1], out var identifier, false)) + return false; + + _objects.Update(); + if (!_objects.TryGetValue(identifier, out var actors)) + { + if (_stateManager.TryGetValue(identifier, out var state)) + _stateManager.ApplyDesign(design, state, StateChanged.Source.Manual); + } + else + { + foreach (var actor in actors.Objects) + { + if (_stateManager.GetOrCreate(identifier, actor, out var state)) + _stateManager.ApplyDesign(design, state, StateChanged.Source.Manual); + } + } + + return true; + } + + private bool CopyState(string argument) + { + if (argument.Length == 0) + { + _chat.Print(new SeStringBuilder().AddText("Use with /glamour copy ").AddGreen("[Character Identifier]").BuiltString); + PlayerIdentifierHelp(false); + } + + if (!IdentifierHandling(argument, out var identifier, false)) + return false; + + string text; + if (_stateManager.TryGetValue(identifier, out var state)) + { + text = _converter.ShareBase64(state); + } + else + { + if (!_objects.TryGetValue(identifier, out var data) + || !data.Valid + || !_stateManager.GetOrCreate(identifier, data.Objects[0], out state)) + { + _chat.Print(new SeStringBuilder().AddText("Could not copy state to clipboard: The identified object ") + .AddGreen(identifier.ToString(), true).AddText(" is not available and has no stored state.").BuiltString); + return false; + } + + text = _converter.ShareBase64(state); + } + + try + { + ImGui.SetClipboardText(text); + } + catch + { + _chat.Print("Could not copy state to clipboard: Failure to write to clipboard."); + return false; + } + + return true; + } + + private bool SaveState(string arguments) + { + var split = arguments.Split('|', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (split.Length != 2) + { + _chat.Print(new SeStringBuilder().AddText("Use with /glamour save ").AddYellow("[New Design Name]").AddText(" | ") + .AddGreen("[Character Identifier]").BuiltString); + PlayerIdentifierHelp(false); + } + + if (!IdentifierHandling(split[1], out var identifier, false)) + return false; + + if (!_stateManager.TryGetValue(identifier, out var state)) + { + _objects.Update(); + if (!_objects.TryGetValue(identifier, out var data) + || !data.Valid + || !_stateManager.GetOrCreate(identifier, data.Objects[0], out state)) + { + _chat.Print(new SeStringBuilder().AddText("Could not save state to design ").AddYellow(split[0], true) + .AddText(": The identified object ") + .AddGreen(identifier.ToString(), true).AddText(" is not available and has no stored state.").BuiltString); + return false; + } + } + + var design = _converter.Convert(state, EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant); + _designManager.CreateClone(design, split[0]); + return true; + } + + private bool GetDesign(string argument, [NotNullWhen(true)] out Design? design) + { + design = null; + if (argument.Length == 0) + return false; + + if (Guid.TryParse(argument, out var guid)) + { + design = _designManager.Designs.FirstOrDefault(d => d.Identifier == guid); + } + else + { + var lower = argument.ToLowerInvariant(); + design = _designManager.Designs.FirstOrDefault(d + => d.Name.Lower == lower || lower.Length > 3 && d.Identifier.ToString().StartsWith(lower)); + } + + if (design == null) + { + _chat.Print(new SeStringBuilder().AddText("The token ").AddYellow(argument, true).AddText(" did not resolve to an existing design.") + .BuiltString); + return false; + } + + return true; + } + + private unsafe bool IdentifierHandling(string argument, out ActorIdentifier identifier, bool allowAnyWorld) + { + try + { + if (_objects.GetName(argument.ToLowerInvariant(), out var obj)) + { + identifier = _actors.AwaitedService.FromObject(obj.AsObject, out _, true, true, true); + if (!identifier.IsValid) + { + _chat.Print(new SeStringBuilder().AddText("The placeholder ").AddGreen(argument) + .AddText(" did not resolve to a game object with a valid identifier.").BuiltString); + return false; + } + } + else + { + identifier = _actors.AwaitedService.FromUserString(argument); + if (!allowAnyWorld + && identifier.Type is IdentifierType.Player or IdentifierType.Owned + && identifier.HomeWorld == ushort.MaxValue) + { + _chat.Print(new SeStringBuilder().AddText("The argument ").AddRed(argument, true) + .AddText(" did not specify a world.").BuiltString); + return false; + } + } + + return true; + } + catch (ActorManager.IdentifierParseError e) + { + _chat.Print(new SeStringBuilder().AddText("The argument ").AddRed(argument, true) + .AddText($" could not be converted to an identifier. {e.Message}") + .BuiltString); + identifier = ActorIdentifier.Invalid; + return false; + } + } + + private void PlayerIdentifierHelp(bool allowAnyWorld) + { + _chat.Print(new SeStringBuilder().AddText(" 》 Valid Character Identifiers have 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]@[World Name]") + .AddText(allowAnyWorld ? ", if no @ is provided, Any World is used." : ".") + .BuiltString); + _chat.Print(new SeStringBuilder().AddText(" 》》》").AddGreen("r").AddText(" | ").AddWhite("[Retainer Name]").AddText(".").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); + } }