From 51de495ae4c1b9da13cce24dc15ac844b24f657e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 6 Jan 2021 23:43:48 -0500 Subject: add a way to send console commands to a specific screen --- src/SMAPI/Framework/CommandManager.cs | 67 ++++++++++++++++++++++++++++++++++- src/SMAPI/Framework/SCore.cs | 67 +++++++++++++++++++++++------------ src/SMAPI/Utilities/PerScreen.cs | 33 ++++++++++------- 3 files changed, 131 insertions(+), 36 deletions(-) (limited to 'src') 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 /// 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. @@ -81,8 +91,9 @@ namespace StardewModdingAPI.Framework /// 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, 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(); } + /// 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) 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 ****/ /// Manages console commands. - private readonly CommandManager CommandManager = new CommandManager(); + private readonly CommandManager CommandManager; /// The underlying game instance. private SGameRunner Game; @@ -130,9 +130,12 @@ namespace StardewModdingAPI.Framework /// Asset interceptors added or removed since the last tick. private readonly List ReloadAssetInterceptorsQueue = new List(); - /// A list of queued commands to execute. + /// A list of queued commands to parse and execute. /// This property must be thread-safe, since it's accessed from a separate console input thread. - public ConcurrentQueue CommandQueue { get; } = new ConcurrentQueue(); + private readonly ConcurrentQueue RawCommandQueue = new ConcurrentQueue(); + + /// A list of commands to execute on each screen. + private readonly PerScreen>> 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)) @@ -579,6 +573,33 @@ namespace StardewModdingAPI.Framework this.JustReturnedToTitle = false; } + /********* + ** 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 *********/ diff --git a/src/SMAPI/Utilities/PerScreen.cs b/src/SMAPI/Utilities/PerScreen.cs index 55dae0d8..89d08e87 100644 --- a/src/SMAPI/Utilities/PerScreen.cs +++ b/src/SMAPI/Utilities/PerScreen.cs @@ -28,18 +28,8 @@ namespace StardewModdingAPI.Utilities /// The value is initialized the first time it's requested for that player, unless it's set manually first. public T Value { - get - { - this.RemoveDeadPlayers(); - return this.States.TryGetValue(Context.ScreenId, out T state) - ? state - : this.States[Context.ScreenId] = this.CreateNewState(); - } - set - { - this.RemoveDeadPlayers(); - this.States[Context.ScreenId] = value; - } + get => this.GetValueForScreen(Context.ScreenId); + set => this.SetValueForScreen(Context.ScreenId, value); } @@ -57,6 +47,25 @@ namespace StardewModdingAPI.Utilities this.CreateNewState = createNewState ?? (() => default); } + /// Get the value for a given screen ID, creating it if needed. + /// The screen ID to check. + internal T GetValueForScreen(int screenId) + { + this.RemoveDeadPlayers(); + return this.States.TryGetValue(screenId, out T state) + ? state + : this.States[screenId] = this.CreateNewState(); + } + + /// Set the value for a given screen ID, creating it if needed. + /// The screen ID whose value set. + /// The value to set. + internal void SetValueForScreen(int screenId, T value) + { + this.RemoveDeadPlayers(); + this.States[screenId] = value; + } + /********* ** Private methods -- cgit