From 0134f0b28d355766a17b1c9da89b8173f978d195 Mon Sep 17 00:00:00 2001
From: Jesse Plamondon-Willard <github@jplamondonw.com>
Date: Wed, 25 Apr 2018 13:16:25 -0400
Subject: update release notes, refactor a bit (#474)

---
 .../Framework/Commands/Player/AddCommand.cs        | 123 +++++++++------------
 .../Framework/ItemData/SearchableItem.cs           |  21 +++-
 src/SMAPI/Framework/CommandManager.cs              |  21 ++--
 3 files changed, 84 insertions(+), 81 deletions(-)

(limited to 'src')

diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs
index 803ae7f6..71c3ff98 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs
@@ -1,5 +1,4 @@
 using System;
-using System.Collections.Generic;
 using System.Linq;
 using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData;
 using StardewValley;
@@ -16,16 +15,16 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
         /// <summary>Provides methods for searching and constructing items.</summary>
         private readonly ItemRepository Items = new ItemRepository();
 
-        /// <summary>All possible item types along with Name.</summary>
-        private readonly string[] ItemTypeAndName = Enum.GetNames(typeof(ItemType)).Union(new string[] { "Name" }).ToArray();
+        /// <summary>The type names recognised by this command.</summary>
+        private readonly string[] ValidTypes = Enum.GetNames(typeof(ItemType)).Concat(new[] { "Name" }).ToArray();
+
 
         /*********
         ** Public methods
         *********/
         /// <summary>Construct an instance.</summary>
         public AddCommand()
-            : base("player_add", AddCommand.GetDescription())
-        { }
+            : base("player_add", AddCommand.GetDescription()) { }
 
         /// <summary>Handle the command.</summary>
         /// <param name="monitor">Writes messages to the console and log file.</param>
@@ -40,24 +39,21 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
                 return;
             }
 
-            SearchableItem match;
-
-            //read arguments
-            if (!args.TryGet(0, "item type", out string typeOrName, oneOf: this.ItemTypeAndName))
-                return;
-            if (Enum.GetNames(typeof(ItemType)).Contains(typeOrName, StringComparer.InvariantCultureIgnoreCase))
-                this.FindItemByTypeAndId(monitor, args, typeOrName, out match);
-            else
-                this.FindItemByName(monitor, args, out match);
-
-            if (match == null)
+            // read arguments
+            if (!args.TryGet(0, "item type", out string type, oneOf: this.ValidTypes))
                 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;
 
+            // find matching item
+            SearchableItem match = Enum.TryParse(type, true, out ItemType itemType)
+                ? this.FindItemByID(monitor, args, itemType)
+                : this.FindItemByName(monitor, args);
+            if (match == null)
+                return;
+
             // apply count
             match.Item.Stack = count;
 
@@ -72,90 +68,81 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player
             monitor.Log($"OK, added {match.Name} ({match.Type} #{match.ID}) to your inventory.", LogLevel.Info);
         }
 
+
         /*********
         ** Private methods
         *********/
-
-        /// <summary>Finds a matching item by item type and id.</summary>
+        /// <summary>Get a matching item by its ID.</summary>
         /// <param name="monitor">Writes messages to the console and log file.</param>
         /// <param name="args">The command arguments.</param>
-        /// <param name="rawType">The raw item type.</param>
-        /// <param name="match">The matching item.</param>
-        private void FindItemByTypeAndId(IMonitor monitor, ArgumentParser args, string rawType, out SearchableItem match)
+        /// <param name="type">The item type.</param>
+        private SearchableItem FindItemByID(IMonitor monitor, ArgumentParser args, ItemType type)
         {
-            match = null;
-
             // read arguments
             if (!args.TryGetInt(1, "item ID", out int id, min: 0))
-                return;
-
-            ItemType type = (ItemType)Enum.Parse(typeof(ItemType), rawType, ignoreCase: true);
+                return null;
 
             // find matching item
-            match = this.Items.GetAll().FirstOrDefault(p => p.Type == type && p.ID == id);
-
-            if (match == null)
-            {
+            SearchableItem item = this.Items.GetAll().FirstOrDefault(p => p.Type == type && p.ID == id);
+            if (item == null)
                 monitor.Log($"There's no {type} item with ID {id}.", LogLevel.Error);
-            }
+            return item;
         }
 
-        /// <summary>Finds a matching item by name.</summary>
+        /// <summary>Get a matching item by its name.</summary>
         /// <param name="monitor">Writes messages to the console and log file.</param>
         /// <param name="args">The command arguments.</param>
-        /// <param name="name">The item name.</param>
-        /// <param name="match">The matching item.</param>
-        private void FindItemByName(IMonitor monitor, ArgumentParser args, out SearchableItem match)
+        private SearchableItem FindItemByName(IMonitor monitor, ArgumentParser args)
         {
-            match = null;
-
             // read arguments
             if (!args.TryGet(1, "item name", out string name))
-                return;
+                return null;
 
             // find matching items
-            IEnumerable<SearchableItem> matching = this.Items.GetAll().Where(p => p.DisplayName.IndexOf(name, StringComparison.InvariantCultureIgnoreCase) != -1);
-            match = matching.FirstOrDefault(item => item.DisplayName.Equals(name, StringComparison.InvariantCultureIgnoreCase));
-
-            // handle unique requirement
-            if (match != null)
-            {
-                return;
-            }
-
-            int numberOfMatches = matching.Count();
-
-            if (numberOfMatches == 0)
-            {
-                monitor.Log($"There's no item with name '{name}'. You can use the 'list_items [name]' command to search for items.", LogLevel.Error);
-            }
-            else if (numberOfMatches == 1)
+            SearchableItem[] matches = this.Items.GetAll().Where(p => p.NameContains(name)).ToArray();
+            switch (matches.Length)
             {
-                monitor.Log($"There's no item with name '{name}'. Did you mean '{matching.ElementAt(0).DisplayName}'? If so, type 'player_add name {matching.ElementAt(0).DisplayName}'.", LogLevel.Error);
-            }
-            else
-            {
-                string options = this.GetTableString(matching, new string[] { "type", "name", "command" }, item => new string[] { item.Type.ToString(), item.DisplayName, $"player_add {item.Type} {item.ID}" });
-
-                monitor.Log($"Found multiple item names containing '{name}'. Type one of these commands for the one you want:", LogLevel.Error);
-                monitor.Log($"\n{options}", LogLevel.Info);
+                // none found
+                case 0:
+                    monitor.Log($"There's no item with name '{name}'. You can use the 'list_items [name]' command to search for items.", LogLevel.Error);
+                    return null;
+
+                // exact match
+                case 1 when matches[0].NameEquivalentTo(name):
+                    return matches[0];
+
+                // list matches
+                default:
+                    string options = this.GetTableString(
+                        data: matches,
+                        header: new[] { "type", "name", "command" },
+                        getRow: item => new[] { item.Type.ToString(), item.DisplayName, $"player_add {item.Type} {item.ID}" }
+                    );
+                    monitor.Log($"There's no item with name '{name}'. Do you mean one of these?\n\n{options}", LogLevel.Info);
+                    return null;
             }
         }
 
+        /// <summary>Get the command description.</summary>
         private static string GetDescription()
         {
             string[] typeValues = Enum.GetNames(typeof(ItemType));
             return "Gives the player an item.\n"
                 + "\n"
-                + "Usage: player_add <type> (<item>|<name>) [count] [quality]\n"
-                + $"- type: the item type (either Name or one of {string.Join(", ", typeValues)}).\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"
-                + "- name: the display name of the item (when using type Name).\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";
+                + "Usage: player_add name \"<name>\" [count] [quality]\n"
+                + "- name: the item name to search (use the 'list_items' command to see a list). This will add the item immediately if it's an exact match, else show a table of matching items.\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"
+                + "These examples both add the galaxy sword to your inventory:\n"
+                + "  player_add weapon 4\n"
+                + "  player_add name \"Galaxy Sword\"";
         }
     }
 }
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs
index 3eede413..b618a308 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemData/SearchableItem.cs
@@ -1,4 +1,5 @@
-using StardewValley;
+using System;
+using StardewValley;
 
 namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData
 {
@@ -37,5 +38,23 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData
             this.ID = id;
             this.Item = item;
         }
+
+        /// <summary>Get whether the item name contains a case-insensitive substring.</summary>
+        /// <param name="substring">The substring to find.</param>
+        public bool NameContains(string substring)
+        {
+            return
+                this.Name.IndexOf(substring, StringComparison.InvariantCultureIgnoreCase) != -1
+                || this.DisplayName.IndexOf(substring, StringComparison.InvariantCultureIgnoreCase) != -1;
+        }
+
+        /// <summary>Get whether the item name is exactly equal to a case-insensitive string.</summary>
+        /// <param name="name">The substring to find.</param>
+        public bool NameEquivalentTo(string name)
+        {
+            return
+                this.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)
+                || this.DisplayName.Equals(name, StringComparison.InvariantCultureIgnoreCase);
+        }
     }
 }
diff --git a/src/SMAPI/Framework/CommandManager.cs b/src/SMAPI/Framework/CommandManager.cs
index 0c48d8ec..f9651ed9 100644
--- a/src/SMAPI/Framework/CommandManager.cs
+++ b/src/SMAPI/Framework/CommandManager.cs
@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Text;
 
 namespace StardewModdingAPI.Framework
 {
@@ -103,31 +104,27 @@ namespace StardewModdingAPI.Framework
         /*********
         ** Private methods
         *********/
-        /// <summary>
-        /// Parses a string into an array of arguments.
-        /// </summary>
+        /// <summary>Parse a string into command arguments.</summary>
         /// <param name="input">The string to parse.</param>
         private string[] ParseArgs(string input)
         {
             bool inQuotes = false;
             IList<string> args = new List<string>();
-            IList<char> currentArg = new List<char>();
-            foreach (char c in input)
+            StringBuilder currentArg = new StringBuilder();
+            foreach (char ch in input)
             {
-                if (c == '"')
-                {
+                if (ch == '"')
                     inQuotes = !inQuotes;
-                }
-                else if (!inQuotes && char.IsWhiteSpace(c))
+                else if (!inQuotes && char.IsWhiteSpace(ch))
                 {
-                    args.Add(string.Concat(currentArg));
+                    args.Add(currentArg.ToString());
                     currentArg.Clear();
                 }
                 else
-                    currentArg.Add(c);
+                    currentArg.Append(ch);
             }
 
-            args.Add(string.Concat(currentArg));
+            args.Add(currentArg.ToString());
 
             return args.Where(item => !string.IsNullOrWhiteSpace(item)).ToArray();
         }
-- 
cgit