diff options
Diffstat (limited to 'src/TrainerMod/Framework')
32 files changed, 1845 insertions, 0 deletions
diff --git a/src/TrainerMod/Framework/Commands/ArgumentParser.cs b/src/TrainerMod/Framework/Commands/ArgumentParser.cs new file mode 100644 index 00000000..6bcd3ff8 --- /dev/null +++ b/src/TrainerMod/Framework/Commands/ArgumentParser.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using StardewModdingAPI; + +namespace TrainerMod.Framework.Commands +{ + /// <summary>Provides methods for parsing command-line arguments.</summary> + internal class ArgumentParser : IReadOnlyList<string> + { + /********* + ** Properties + *********/ + /// <summary>The command name for errors.</summary> + private readonly string CommandName; + + /// <summary>The arguments to parse.</summary> + private readonly string[] Args; + + /// <summary>Writes messages to the console and log file.</summary> + private readonly IMonitor Monitor; + + + /********* + ** Accessors + *********/ + /// <summary>Get the number of arguments.</summary> + public int Count => this.Args.Length; + + /// <summary>Get the argument at the specified index in the list.</summary> + /// <param name="index">The zero-based index of the element to get.</param> + public string this[int index] => this.Args[index]; + + /// <summary>A method which parses a string argument into the given value.</summary> + /// <typeparam name="T">The expected argument type.</typeparam> + /// <param name="input">The argument to parse.</param> + /// <param name="output">The parsed value.</param> + /// <returns>Returns whether the argument was successfully parsed.</returns> + public delegate bool ParseDelegate<T>(string input, out T output); + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="commandName">The command name for errors.</param> + /// <param name="args">The arguments to parse.</param> + /// <param name="monitor">Writes messages to the console and log file.</param> + public ArgumentParser(string commandName, string[] args, IMonitor monitor) + { + this.CommandName = commandName; + this.Args = args; + this.Monitor = monitor; + } + + /// <summary>Try to read a string argument.</summary> + /// <param name="index">The argument index.</param> + /// <param name="name">The argument name for error messages.</param> + /// <param name="value">The parsed value.</param> + /// <param name="required">Whether to show an error if the argument is missing.</param> + /// <param name="oneOf">Require that the argument match one of the given values (case-insensitive).</param> + public bool TryGet(int index, string name, out string value, bool required = true, string[] oneOf = null) + { + value = null; + + // validate + if (this.Args.Length < index + 1) + { + if (required) + this.LogError($"Argument {index} ({name}) is required."); + return false; + } + if (oneOf?.Any() == true && !oneOf.Contains(this.Args[index], StringComparer.InvariantCultureIgnoreCase)) + { + this.LogError($"Argument {index} ({name}) must be one of {string.Join(", ", oneOf)}."); + return false; + } + + // get value + value = this.Args[index]; + return true; + } + + /// <summary>Try to read an integer argument.</summary> + /// <param name="index">The argument index.</param> + /// <param name="name">The argument name for error messages.</param> + /// <param name="value">The parsed value.</param> + /// <param name="required">Whether to show an error if the argument is missing.</param> + /// <param name="min">The minimum value allowed.</param> + /// <param name="max">The maximum value allowed.</param> + public bool TryGetInt(int index, string name, out int value, bool required = true, int? min = null, int? max = null) + { + value = 0; + + // get argument + if (!this.TryGet(index, name, out string raw, required)) + return false; + + // parse + if (!int.TryParse(raw, out value)) + { + this.LogIntFormatError(index, name, min, max); + return false; + } + + // validate + if ((min.HasValue && value < min) || (max.HasValue && value > max)) + { + this.LogIntFormatError(index, name, min, max); + return false; + } + + return true; + } + + /// <summary>Returns an enumerator that iterates through the collection.</summary> + /// <returns>An enumerator that can be used to iterate through the collection.</returns> + public IEnumerator<string> GetEnumerator() + { + return ((IEnumerable<string>)this.Args).GetEnumerator(); + } + + /// <summary>Returns an enumerator that iterates through a collection.</summary> + /// <returns>An <see cref="T:System.Collections.IEnumerator" /> object that can be used to iterate through the collection.</returns> + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + + /********* + ** Private methods + *********/ + /// <summary>Log a usage error.</summary> + /// <param name="message">The message describing the error.</param> + private void LogError(string message) + { + this.Monitor.Log($"{message} Type 'help {this.CommandName}' for usage.", LogLevel.Error); + } + + /// <summary>Print an error for an invalid int argument.</summary> + /// <param name="index">The argument index.</param> + /// <param name="name">The argument name for error messages.</param> + /// <param name="min">The minimum value allowed.</param> + /// <param name="max">The maximum value allowed.</param> + private void LogIntFormatError(int index, string name, int? min, int? max) + { + if (min.HasValue && max.HasValue) + this.LogError($"Argument {index} ({name}) must be an integer between {min} and {max}."); + else if (min.HasValue) + this.LogError($"Argument {index} ({name}) must be an integer and at least {min}."); + else if (max.HasValue) + this.LogError($"Argument {index} ({name}) must be an integer and at most {max}."); + else + this.LogError($"Argument {index} ({name}) must be an integer."); + } + } +} diff --git a/src/TrainerMod/Framework/Commands/ITrainerCommand.cs b/src/TrainerMod/Framework/Commands/ITrainerCommand.cs new file mode 100644 index 00000000..3d97e799 --- /dev/null +++ b/src/TrainerMod/Framework/Commands/ITrainerCommand.cs @@ -0,0 +1,34 @@ +using StardewModdingAPI; + +namespace TrainerMod.Framework.Commands +{ + /// <summary>A TrainerMod command to register.</summary> + internal interface ITrainerCommand + { + /********* + ** Accessors + *********/ + /// <summary>The command name the user must type.</summary> + string Name { get; } + + /// <summary>The command description.</summary> + string Description { get; } + + /// <summary>Whether the command needs to perform logic when the game updates.</summary> + bool NeedsUpdate { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Handle the command.</summary> + /// <param name="monitor">Writes messages to the console and log file.</param> + /// <param name="command">The command name.</param> + /// <param name="args">The command arguments.</param> + void Handle(IMonitor monitor, string command, ArgumentParser args); + + /// <summary>Perform any logic needed on update tick.</summary> + /// <param name="monitor">Writes messages to the console and log file.</param> + void Update(IMonitor monitor); + } +} diff --git a/src/TrainerMod/Framework/Commands/Other/DebugCommand.cs b/src/TrainerMod/Framework/Commands/Other/DebugCommand.cs new file mode 100644 index 00000000..8c6e9f3b --- /dev/null +++ b/src/TrainerMod/Framework/Commands/Other/DebugCommand.cs @@ -0,0 +1,33 @@ +using StardewModdingAPI; +using StardewValley; + +namespace TrainerMod.Framework.Commands.Other +{ + /// <summary>A command which sends a debug command to the game.</summary> + internal class DebugCommand : TrainerCommand + { + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public DebugCommand() + : base("debug", "Run one of the game's debug commands; for example, 'debug warp FarmHouse 1 1' warps the player to the farmhouse.") { } + + /// <summary>Handle the command.</summary> + /// <param name="monitor">Writes messages to the console and log file.</param> + /// <param name="command">The command name.</param> + /// <param name="args">The command arguments.</param> + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // submit command + string debugCommand = string.Join(" ", args); + string oldOutput = Game1.debugOutput; + Game1.game1.parseDebugInput(debugCommand); + + // show result + monitor.Log(Game1.debugOutput != oldOutput + ? $"> {Game1.debugOutput}" + : "Sent debug command to the game, but there was no output.", LogLevel.Info); + } + } +} diff --git a/src/TrainerMod/Framework/Commands/Other/ShowDataFilesCommand.cs b/src/TrainerMod/Framework/Commands/Other/ShowDataFilesCommand.cs new file mode 100644 index 00000000..367a70c6 --- /dev/null +++ b/src/TrainerMod/Framework/Commands/Other/ShowDataFilesCommand.cs @@ -0,0 +1,26 @@ +using System.Diagnostics; +using StardewModdingAPI; + +namespace TrainerMod.Framework.Commands.Other +{ + /// <summary>A command which shows the data files.</summary> + internal class ShowDataFilesCommand : TrainerCommand + { + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public ShowDataFilesCommand() + : base("show_data_files", "Opens the folder containing the save and log files.") { } + + /// <summary>Handle the command.</summary> + /// <param name="monitor">Writes messages to the console and log file.</param> + /// <param name="command">The command name.</param> + /// <param name="args">The command arguments.</param> + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + Process.Start(Constants.DataPath); + monitor.Log($"OK, opening {Constants.DataPath}.", LogLevel.Info); + } + } +} diff --git a/src/TrainerMod/Framework/Commands/Other/ShowGameFilesCommand.cs b/src/TrainerMod/Framework/Commands/Other/ShowGameFilesCommand.cs new file mode 100644 index 00000000..67fa83a3 --- /dev/null +++ b/src/TrainerMod/Framework/Commands/Other/ShowGameFilesCommand.cs @@ -0,0 +1,26 @@ +using System.Diagnostics; +using StardewModdingAPI; + +namespace TrainerMod.Framework.Commands.Other +{ + /// <summary>A command which shows the game files.</summary> + internal class ShowGameFilesCommand : TrainerCommand + { + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public ShowGameFilesCommand() + : base("show_game_files", "Opens the game folder.") { } + + /// <summary>Handle the command.</summary> + /// <param name="monitor">Writes messages to the console and log file.</param> + /// <param name="command">The command name.</param> + /// <param name="args">The command arguments.</param> + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + Process.Start(Constants.ExecutionPath); + monitor.Log($"OK, opening {Constants.ExecutionPath}.", LogLevel.Info); + } + } +} diff --git a/src/TrainerMod/Framework/Commands/Player/AddCommand.cs b/src/TrainerMod/Framework/Commands/Player/AddCommand.cs new file mode 100644 index 00000000..47840202 --- /dev/null +++ b/src/TrainerMod/Framework/Commands/Player/AddCommand.cs @@ -0,0 +1,81 @@ +using System; +using System.Linq; +using StardewModdingAPI; +using StardewValley; +using TrainerMod.Framework.ItemData; +using Object = StardewValley.Object; + +namespace TrainerMod.Framework.Commands.Player +{ + /// <summary>A command which adds an item to the player inventory.</summary> + internal class AddCommand : TrainerCommand + { + /********* + ** Properties + *********/ + /// <summary>Provides methods for searching and constructing items.</summary> + private readonly ItemRepository Items = new ItemRepository(); + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public AddCommand() + : base("player_add", AddCommand.GetDescription()) + { } + + /// <summary>Handle the command.</summary> + /// <param name="monitor">Writes messages to the console and log file.</param> + /// <param name="command">The command name.</param> + /// <param name="args">The command arguments.</param> + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // read arguments + if (!args.TryGet(0, "item type", out string rawType, oneOf: Enum.GetNames(typeof(ItemType)))) + return; + if (!args.TryGetInt(1, "item ID", out int id, min: 0)) + return; + if (!args.TryGetInt(2, "count", out int count, min: 1, required: false)) + count = 1; + if (!args.TryGetInt(3, "quality", out int quality, min: Object.lowQuality, max: Object.bestQuality, required: false)) + quality = Object.lowQuality; + ItemType type = (ItemType)Enum.Parse(typeof(ItemType), rawType, ignoreCase: true); + + // find matching item + SearchableItem match = this.Items.GetAll().FirstOrDefault(p => p.Type == type && p.ID == id); + if (match == null) + { + monitor.Log($"There's no {type} item with ID {id}.", LogLevel.Error); + return; + } + + // apply count & quality + match.Item.Stack = count; + if (match.Item is Object obj) + obj.quality = quality; + + // add to inventory + Game1.player.addItemByMenuIfNecessary(match.Item); + monitor.Log($"OK, added {match.Name} ({match.Type} #{match.ID}) to your inventory.", LogLevel.Info); + } + + /********* + ** Private methods + *********/ + private static string GetDescription() + { + string[] typeValues = Enum.GetNames(typeof(ItemType)); + return "Gives the player an item.\n" + + "\n" + + "Usage: player_add <type> <item> [count] [quality]\n" + + $"- type: the item type (one of {string.Join(", ", typeValues)}).\n" + + "- item: the item ID (use the 'list_items' command to see a list).\n" + + "- count (optional): how many of the item to give.\n" + + $"- quality (optional): one of {Object.lowQuality} (normal), {Object.medQuality} (silver), {Object.highQuality} (gold), or {Object.bestQuality} (iridium).\n" + + "\n" + + "This example adds the galaxy sword to your inventory:\n" + + " player_add weapon 4"; + } + } +} diff --git a/src/TrainerMod/Framework/Commands/Player/ListItemTypesCommand.cs b/src/TrainerMod/Framework/Commands/Player/ListItemTypesCommand.cs new file mode 100644 index 00000000..5f14edbb --- /dev/null +++ b/src/TrainerMod/Framework/Commands/Player/ListItemTypesCommand.cs @@ -0,0 +1,53 @@ +using System.Linq; +using StardewModdingAPI; +using TrainerMod.Framework.ItemData; + +namespace TrainerMod.Framework.Commands.Player +{ + /// <summary>A command which list item types.</summary> + internal class ListItemTypesCommand : TrainerCommand + { + /********* + ** Properties + *********/ + /// <summary>Provides methods for searching and constructing items.</summary> + private readonly ItemRepository Items = new ItemRepository(); + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public ListItemTypesCommand() + : base("list_item_types", "Lists item types you can filter in other commands.\n\nUsage: list_item_types") { } + + /// <summary>Handle the command.</summary> + /// <param name="monitor">Writes messages to the console and log file.</param> + /// <param name="command">The command name.</param> + /// <param name="args">The command arguments.</param> + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // validate + if (!Context.IsWorldReady) + { + monitor.Log("You need to load a save to use this command.", LogLevel.Error); + return; + } + + // handle + ItemType[] matches = + ( + from item in this.Items.GetAll() + orderby item.Type.ToString() + select item.Type + ) + .Distinct() + .ToArray(); + string summary = "Searching...\n"; + if (matches.Any()) + monitor.Log(summary + this.GetTableString(matches, new[] { "type" }, val => new[] { val.ToString() }), LogLevel.Info); + else + monitor.Log(summary + "No item types found.", LogLevel.Info); + } + } +} diff --git a/src/TrainerMod/Framework/Commands/Player/ListItemsCommand.cs b/src/TrainerMod/Framework/Commands/Player/ListItemsCommand.cs new file mode 100644 index 00000000..7f4f454c --- /dev/null +++ b/src/TrainerMod/Framework/Commands/Player/ListItemsCommand.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewModdingAPI; +using TrainerMod.Framework.ItemData; + +namespace TrainerMod.Framework.Commands.Player +{ + /// <summary>A command which list items available to spawn.</summary> + internal class ListItemsCommand : TrainerCommand + { + /********* + ** Properties + *********/ + /// <summary>Provides methods for searching and constructing items.</summary> + private readonly ItemRepository Items = new ItemRepository(); + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public ListItemsCommand() + : base("list_items", "Lists and searches items in the game data.\n\nUsage: list_items [search]\n- search (optional): an arbitrary search string to filter by.") { } + + /// <summary>Handle the command.</summary> + /// <param name="monitor">Writes messages to the console and log file.</param> + /// <param name="command">The command name.</param> + /// <param name="args">The command arguments.</param> + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // validate + if (!Context.IsWorldReady) + { + monitor.Log("You need to load a save to use this command.", LogLevel.Error); + return; + } + + // handle + SearchableItem[] matches = + ( + from item in this.GetItems(args.ToArray()) + orderby item.Type.ToString(), item.Name + select item + ) + .ToArray(); + string summary = "Searching...\n"; + if (matches.Any()) + monitor.Log(summary + this.GetTableString(matches, new[] { "type", "name", "id" }, val => new[] { val.Type.ToString(), val.Name, val.ID.ToString() }), LogLevel.Info); + else + monitor.Log(summary + "No items found", LogLevel.Info); + } + + + /********* + ** Private methods + *********/ + /// <summary>Get all items which can be searched and added to the player's inventory through the console.</summary> + /// <param name="searchWords">The search string to find.</param> + private IEnumerable<SearchableItem> GetItems(string[] searchWords) + { + // normalise search term + searchWords = searchWords?.Where(word => !string.IsNullOrWhiteSpace(word)).ToArray(); + if (searchWords?.Any() == false) + searchWords = null; + + // find matches + return ( + from item in this.Items.GetAll() + let term = $"{item.ID}|{item.Type}|{item.Name}|{item.DisplayName}" + where searchWords == null || searchWords.All(word => term.IndexOf(word, StringComparison.CurrentCultureIgnoreCase) != -1) + select item + ); + } + } +} diff --git a/src/TrainerMod/Framework/Commands/Player/SetColorCommand.cs b/src/TrainerMod/Framework/Commands/Player/SetColorCommand.cs new file mode 100644 index 00000000..28ace0df --- /dev/null +++ b/src/TrainerMod/Framework/Commands/Player/SetColorCommand.cs @@ -0,0 +1,76 @@ +using Microsoft.Xna.Framework; +using StardewModdingAPI; +using StardewValley; + +namespace TrainerMod.Framework.Commands.Player +{ + /// <summary>A command which edits the color of a player feature.</summary> + internal class SetColorCommand : TrainerCommand + { + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public SetColorCommand() + : base("player_changecolor", "Sets the color of a player feature.\n\nUsage: player_changecolor <target> <color>\n- target: what to change (one of 'hair', 'eyes', or 'pants').\n- color: a color value in RGB format, like (255,255,255).") { } + + /// <summary>Handle the command.</summary> + /// <param name="monitor">Writes messages to the console and log file.</param> + /// <param name="command">The command name.</param> + /// <param name="args">The command arguments.</param> + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // parse arguments + if (!args.TryGet(0, "target", out string target, oneOf: new[] { "hair", "eyes", "pants" })) + return; + if (!args.TryGet(1, "color", out string rawColor)) + return; + + // parse color + if (!this.TryParseColor(rawColor, out Color color)) + { + this.LogUsageError(monitor, "Argument 1 (color) must be an RBG value like '255,150,0'."); + return; + } + + // handle + switch (target) + { + case "hair": + Game1.player.hairstyleColor = color; + monitor.Log("OK, your hair color is updated.", LogLevel.Info); + break; + + case "eyes": + Game1.player.changeEyeColor(color); + monitor.Log("OK, your eye color is updated.", LogLevel.Info); + break; + + case "pants": + Game1.player.pantsColor = color; + monitor.Log("OK, your pants color is updated.", LogLevel.Info); + break; + } + } + + + /********* + ** Private methods + *********/ + /// <summary>Try to parse a color from a string.</summary> + /// <param name="input">The input string.</param> + /// <param name="color">The color to set.</param> + private bool TryParseColor(string input, out Color color) + { + string[] colorHexes = input.Split(new[] { ',' }, 3); + if (int.TryParse(colorHexes[0], out int r) && int.TryParse(colorHexes[1], out int g) && int.TryParse(colorHexes[2], out int b)) + { + color = new Color(r, g, b); + return true; + } + + color = Color.Transparent; + return false; + } + } +} diff --git a/src/TrainerMod/Framework/Commands/Player/SetHealthCommand.cs b/src/TrainerMod/Framework/Commands/Player/SetHealthCommand.cs new file mode 100644 index 00000000..f64e9035 --- /dev/null +++ b/src/TrainerMod/Framework/Commands/Player/SetHealthCommand.cs @@ -0,0 +1,72 @@ +using System.Linq; +using StardewModdingAPI; +using StardewValley; + +namespace TrainerMod.Framework.Commands.Player +{ + /// <summary>A command which edits the player's current health.</summary> + internal class SetHealthCommand : TrainerCommand + { + /********* + ** Properties + *********/ + /// <summary>Whether to keep the player's health at its maximum.</summary> + private bool InfiniteHealth; + + + /********* + ** Accessors + *********/ + /// <summary>Whether the command needs to perform logic when the game updates.</summary> + public override bool NeedsUpdate => this.InfiniteHealth; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public SetHealthCommand() + : base("player_sethealth", "Sets the player's health.\n\nUsage: player_sethealth [value]\n- value: an integer amount, or 'inf' for infinite health.") { } + + /// <summary>Handle the command.</summary> + /// <param |
