using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using StardewModdingAPI.Framework.Commands;
namespace StardewModdingAPI.Framework
{
/// Manages console commands.
internal class CommandManager
{
/*********
** Fields
*********/
/// The commands registered with SMAPI.
private readonly IDictionary Commands = new Dictionary(StringComparer.OrdinalIgnoreCase);
/// Writes messages to the console.
private readonly IMonitor Monitor;
/*********
** Public methods
*********/
/// Construct an instance.
/// Writes messages to the console.
public CommandManager(IMonitor monitor)
{
this.Monitor = monitor;
}
/// Add a console command.
/// The mod adding the command (or null for a SMAPI command).
/// The command name, which the user must type to trigger it.
/// The human-readable documentation shown when the player runs the built-in 'help' command.
/// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user.
/// The or is null or empty.
/// The is not a valid format.
/// There's already a command with that name.
public CommandManager Add(IModMetadata? mod, string name, string documentation, Action callback)
{
name = this.GetNormalizedName(name)!; // null-checked below
// validate format
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentNullException(nameof(name), "Can't register a command with no name.");
if (name.Any(char.IsWhiteSpace))
throw new FormatException($"Can't register the '{name}' command because the name can't contain whitespace.");
if (callback == null)
throw new ArgumentNullException(nameof(callback), $"Can't register the '{name}' command because without a callback.");
// ensure uniqueness
if (this.Commands.ContainsKey(name))
throw new ArgumentException(nameof(callback), $"Can't register the '{name}' command because there's already a command with that name.");
// add command
this.Commands.Add(name, new Command(mod, name, documentation, callback));
return this;
}
/// Add a console command.
/// the SMAPI console command to add.
/// Writes messages to the console.
/// There's already a command with that name.
public CommandManager Add(IInternalCommand command, IMonitor monitor)
{
return this.Add(null, command.Name, command.Description, (_, args) => command.HandleCommand(args, monitor));
}
/// Get a command by its unique name.
/// The command name.
/// Returns the matching command, or null if not found.
public Command? Get(string? name)
{
name = this.GetNormalizedName(name)!;
if (string.IsNullOrWhiteSpace(name))
return null;
this.Commands.TryGetValue(name, out Command? command);
return command;
}
/// Get all registered commands.
public IEnumerable GetAll()
{
return this.Commands
.Values
.OrderBy(p => p.Name);
}
/// Try to parse a raw line of user input into an executable command.
/// The raw user input.
/// The parsed command name.
/// The parsed command arguments.
/// The command which can handle the input.
/// The screen ID on which to run the command.
/// Returns true if the input was successfully parsed and matched to a command; else false.
public bool TryParse(string? input, [NotNullWhen(true)] out string? name, [NotNullWhen(true)] out string[]? args, [NotNullWhen(true)] out Command? command, out int screenId)
{
// ignore if blank
if (string.IsNullOrWhiteSpace(input))
{
name = null;
args = null;
command = null;
screenId = 0;
return false;
}
// parse input
args = this.ParseArgs(input);
name = this.GetNormalizedName(args[0])!;
args = args.Skip(1).ToArray();
// get screen ID argument
screenId = 0;
for (int i = 0; i < args.Length; i++)
{
// consume arg & set screen ID
if (this.TryParseScreenId(args[i], out int rawScreenId, out string? error))
{
args = args.Take(i).Concat(args.Skip(i + 1)).ToArray();
screenId = rawScreenId;
continue;
}
// invalid screen arg
if (error != null)
{
this.Monitor.Log(error, LogLevel.Error);
command = null;
return false;
}
}
// get command
return this.Commands.TryGetValue(name, out command);
}
/// Trigger a command.
/// The command name.
/// The command arguments.
/// Returns whether a matching command was triggered.
public bool Trigger(string? name, string[] arguments)
{
// get normalized name
name = this.GetNormalizedName(name)!;
if (string.IsNullOrWhiteSpace(name))
return false;
// get command
if (this.Commands.TryGetValue(name, out Command? command))
{
command.Callback.Invoke(name, arguments);
return true;
}
return false;
}
/*********
** Private methods
*********/
/// Parse a string into command arguments.
/// The string to parse.
private string[] ParseArgs(string input)
{
bool inQuotes = false;
IList args = new List();
StringBuilder currentArg = new();
foreach (char ch in input)
{
if (ch == '"')
inQuotes = !inQuotes;
else if (!inQuotes && char.IsWhiteSpace(ch))
{
args.Add(currentArg.ToString());
currentArg.Clear();
}
else
currentArg.Append(ch);
}
args.Add(currentArg.ToString());
return args.Where(item => !string.IsNullOrWhiteSpace(item)).ToArray();
}
/// Try to parse a 'screen=X' command argument, which specifies the screen that should receive the command.
/// The raw argument to parse.
/// The parsed screen ID, if any.
/// The error which indicates an invalid screen ID, if applicable.
/// Returns whether the screen ID was parsed successfully.
private bool TryParseScreenId(string arg, out int screen, out string? error)
{
screen = -1;
error = null;
// skip non-screen arg
if (!arg.StartsWith("screen="))
return false;
// get screen ID
string rawScreen = arg.Substring("screen=".Length);
if (!int.TryParse(rawScreen, out screen))
{
error = $"invalid screen ID format: {rawScreen}";
return false;
}
// validate ID
if (!Context.HasScreenId(screen))
{
error = $"there's no active screen with ID {screen}. Active screen IDs: {string.Join(", ", Context.ActiveScreenIds)}.";
return false;
}
return true;
}
/// Get a normalized command name.
/// The command name.
private string? GetNormalizedName(string? name)
{
name = name?.Trim().ToLower();
return !string.IsNullOrWhiteSpace(name)
? name
: null;
}
}
}