mirror of
https://github.com/Ottermandias/Glamourer.git
synced 2025-12-12 10:17:23 +01:00
387 lines
16 KiB
C#
387 lines
16 KiB
C#
using System;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
using Dalamud.Game.Command;
|
|
using Dalamud.Game.Gui;
|
|
using Dalamud.Game.Text.SeStringHandling;
|
|
using Dalamud.Plugin.Services;
|
|
using Glamourer.Automation;
|
|
using Glamourer.Customization;
|
|
using Glamourer.Designs;
|
|
using Glamourer.Events;
|
|
using Glamourer.Gui;
|
|
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 MainCommandString = "/glamourer";
|
|
private const string ApplyCommandString = "/glamour";
|
|
|
|
private readonly ICommandManager _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;
|
|
private readonly DesignFileSystem _designFileSystem;
|
|
|
|
public CommandService(ICommandManager commands, MainWindow mainWindow, ChatGui chat, ActorService actors, ObjectManager objects,
|
|
AutoDesignApplier autoDesignApplier, StateManager stateManager, DesignManager designManager, DesignConverter converter,
|
|
DesignFileSystem designFileSystem)
|
|
{
|
|
_commands = commands;
|
|
_mainWindow = mainWindow;
|
|
_chat = chat;
|
|
_actors = actors;
|
|
_objects = objects;
|
|
_autoDesignApplier = autoDesignApplier;
|
|
_stateManager = stateManager;
|
|
_designManager = designManager;
|
|
_converter = converter;
|
|
_designFileSystem = designFileSystem;
|
|
|
|
_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()
|
|
{
|
|
_commands.RemoveHandler(MainCommandString);
|
|
_commands.RemoveHandler(ApplyCommandString);
|
|
}
|
|
|
|
private void OnGlamourer(string command, string arguments)
|
|
=> _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, StateChanged.Source.Manual);
|
|
|
|
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, Path or Identifier]").AddText(" | ")
|
|
.AddGreen("[Character Identifier]").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.")
|
|
.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);
|
|
_chat.Print(new SeStringBuilder()
|
|
.AddText(" 》 The design path is the folder path in the selector, with '/' as separators. It is also case-insensitive.")
|
|
.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 && _designFileSystem.Find(lower, out var child) && child is DesignFileSystem.Leaf leaf)
|
|
design = leaf.Value;
|
|
}
|
|
|
|
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("<me>").AddText(" or ").AddGreen("<t>").AddText(" or ").AddGreen("<mo>")
|
|
.AddText(" or ").AddGreen("<f>")
|
|
.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]@<World Name>").AddText(".").BuiltString);
|
|
}
|
|
}
|