diff options
Diffstat (limited to 'src/SMAPI/Framework')
-rw-r--r-- | src/SMAPI/Framework/CommandManager.cs | 67 | ||||
-rw-r--r-- | src/SMAPI/Framework/SCore.cs | 67 |
2 files changed, 110 insertions, 24 deletions
diff --git a/src/SMAPI/Framework/CommandManager.cs b/src/SMAPI/Framework/CommandManager.cs index 4a99fd4d..ff540ad8 100644 --- a/src/SMAPI/Framework/CommandManager.cs +++ b/src/SMAPI/Framework/CommandManager.cs @@ -15,10 +15,20 @@ namespace StardewModdingAPI.Framework /// <summary>The commands registered with SMAPI.</summary> private readonly IDictionary<string, Command> Commands = new Dictionary<string, Command>(StringComparer.OrdinalIgnoreCase); + /// <summary>Writes messages to the console.</summary> + private readonly IMonitor Monitor; + /********* ** Public methods *********/ + /// <summary>Construct an instance.</summary> + /// <param name="monitor">Writes messages to the console.</param> + public CommandManager(IMonitor monitor) + { + this.Monitor = monitor; + } + /// <summary>Add a console command.</summary> /// <param name="mod">The mod adding the command (or <c>null</c> for a SMAPI command).</param> /// <param name="name">The command name, which the user must type to trigger it.</param> @@ -81,8 +91,9 @@ namespace StardewModdingAPI.Framework /// <param name="name">The parsed command name.</param> /// <param name="args">The parsed command arguments.</param> /// <param name="command">The command which can handle the input.</param> + /// <param name="screenId">The screen ID on which to run the command.</param> /// <returns>Returns true if the input was successfully parsed and matched to a command; else false.</returns> - public bool TryParse(string input, out string name, out string[] args, out Command command) + public bool TryParse(string input, out string name, out string[] args, out Command command, out int screenId) { // ignore if blank if (string.IsNullOrWhiteSpace(input)) @@ -90,6 +101,7 @@ namespace StardewModdingAPI.Framework name = null; args = null; command = null; + screenId = 0; return false; } @@ -98,6 +110,27 @@ namespace StardewModdingAPI.Framework 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); } @@ -152,6 +185,38 @@ namespace StardewModdingAPI.Framework return args.Where(item => !string.IsNullOrWhiteSpace(item)).ToArray(); } + /// <summary>Try to parse a 'screen=X' command argument, which specifies the screen that should receive the command.</summary> + /// <param name="arg">The raw argument to parse.</param> + /// <param name="screen">The parsed screen ID, if any.</param> + /// <param name="error">The error which indicates an invalid screen ID, if applicable.</param> + /// <returns>Returns whether the screen ID was parsed successfully.</returns> + 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; + } + /// <summary>Get a normalized command name.</summary> /// <param name="name">The command name.</param> private string GetNormalizedName(string name) diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index f9113194..3a51b418 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -81,7 +81,7 @@ namespace StardewModdingAPI.Framework ** Higher-level components ****/ /// <summary>Manages console commands.</summary> - private readonly CommandManager CommandManager = new CommandManager(); + private readonly CommandManager CommandManager; /// <summary>The underlying game instance.</summary> private SGameRunner Game; @@ -130,9 +130,12 @@ namespace StardewModdingAPI.Framework /// <summary>Asset interceptors added or removed since the last tick.</summary> private readonly List<AssetInterceptorChange> ReloadAssetInterceptorsQueue = new List<AssetInterceptorChange>(); - /// <summary>A list of queued commands to execute.</summary> + /// <summary>A list of queued commands to parse and execute.</summary> /// <remarks>This property must be thread-safe, since it's accessed from a separate console input thread.</remarks> - public ConcurrentQueue<string> CommandQueue { get; } = new ConcurrentQueue<string>(); + private readonly ConcurrentQueue<string> RawCommandQueue = new ConcurrentQueue<string>(); + + /// <summary>A list of commands to execute on each screen.</summary> + private readonly PerScreen<List<Tuple<Command, string, string[]>>> ScreenCommandQueue = new(() => new()); /********* @@ -169,11 +172,9 @@ namespace StardewModdingAPI.Framework JsonConvert.PopulateObject(File.ReadAllText(Constants.ApiUserConfigPath), this.Settings); this.LogManager = new LogManager(logPath: logPath, colorConfig: this.Settings.ConsoleColors, writeToConsole: writeToConsole, isVerbose: this.Settings.VerboseLogging, isDeveloperMode: this.Settings.DeveloperMode, getScreenIdForLog: this.GetScreenIdForLog); - + this.CommandManager = new CommandManager(this.Monitor); this.EventManager = new EventManager(this.ModRegistry); - SCore.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); - SDate.Translations = this.Translator; // log SMAPI/OS info @@ -406,7 +407,7 @@ namespace StardewModdingAPI.Framework () => this.LogManager.RunConsoleInputLoop( commandManager: this.CommandManager, reloadTranslations: this.ReloadTranslations, - handleInput: input => this.CommandQueue.Enqueue(input), + handleInput: input => this.RawCommandQueue.Enqueue(input), continueWhile: () => this.IsGameRunning && !this.CancellationToken.IsCancellationRequested ) ).Start(); @@ -489,17 +490,18 @@ namespace StardewModdingAPI.Framework } /********* - ** Execute commands + ** Parse commands *********/ - while (this.CommandQueue.TryDequeue(out string rawInput)) + while (this.RawCommandQueue.TryDequeue(out string rawInput)) { // parse command string name; string[] args; Command command; + int screenId; try { - if (!this.CommandManager.TryParse(rawInput, out name, out args, out command)) + if (!this.CommandManager.TryParse(rawInput, out name, out args, out command, out screenId)) { this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error); continue; @@ -511,18 +513,8 @@ namespace StardewModdingAPI.Framework continue; } - // execute command - try - { - command.Callback.Invoke(name, args); - } - catch (Exception ex) - { - if (command.Mod != null) - command.Mod.LogAsMod($"Mod failed handling that command:\n{ex.GetLogSummary()}", LogLevel.Error); - else - this.Monitor.Log($"Failed handling that command:\n{ex.GetLogSummary()}", LogLevel.Error); - } + // queue command for screen + this.ScreenCommandQueue.GetValueForScreen(screenId).Add(Tuple.Create(command, name, args)); } /********* @@ -570,7 +562,9 @@ namespace StardewModdingAPI.Framework try { - // reapply overrides + /********* + ** Reapply overrides + *********/ if (this.JustReturnedToTitle) { if (!(Game1.mapDisplayDevice is SDisplayDevice)) @@ -580,6 +574,33 @@ namespace StardewModdingAPI.Framework } /********* + ** Execute commands + *********/ + { + var commandQueue = this.ScreenCommandQueue.Value; + foreach (var entry in commandQueue) + { + Command command = entry.Item1; + string name = entry.Item2; + string[] args = entry.Item3; + + try + { + command.Callback.Invoke(name, args); + } + catch (Exception ex) + { + if (command.Mod != null) + command.Mod.LogAsMod($"Mod failed handling that command:\n{ex.GetLogSummary()}", LogLevel.Error); + else + this.Monitor.Log($"Failed handling that command:\n{ex.GetLogSummary()}", LogLevel.Error); + } + } + commandQueue.Clear(); + } + + + /********* ** Update input *********/ // This should *always* run, even when suppressing mod events, since the game uses |