diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs index 0bebd2b7..6b3b27cd 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs @@ -1,27 +1,23 @@ +using System; +using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Reflection; +using System.Text; using StardewValley; +using StardewValley.GameData; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player { /// A command which changes the player's farm type. internal class SetFarmTypeCommand : ConsoleCommand { - /********* - ** Fields - *********/ - /// The vanilla farm type IDs. - private static readonly ISet VanillaFarmTypes = new HashSet( - Enumerable.Range(0, Farm.layout_max + 1) - ); - - /********* ** Public methods *********/ /// Construct an instance. public SetFarmTypeCommand() - : base("set_farm_type", $"Sets the current player's farm type.\n\nUsage: set_farm_type \n- farm type: one of {string.Join(", ", SetFarmTypeCommand.VanillaFarmTypes.Select(id => $"{id} ({SetFarmTypeCommand.GetFarmLabel(id)})"))}.") { } + : base("set_farm_type", "Sets the current player's farm type.\n\nUsage: set_farm_type \n- farm type: the farm type to set. Enter `set_farm_type list` for a list of available farm types.") { } /// Handle the command. /// Writes messages to the console and log file. @@ -29,47 +25,141 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// The command arguments. public override void Handle(IMonitor monitor, string command, ArgumentParser args) { - // validation checks + // validate if (!Context.IsWorldReady) { monitor.Log("You must load a save to use this command.", LogLevel.Error); return; } - // parse argument - if (!args.TryGetInt(0, "farm type", out int farmType, min: 0, max: Farm.layout_max)) + // parse arguments + if (!args.TryGet(0, "farm type", out string farmType)) return; + bool isVanillaId = int.TryParse(farmType, out int vanillaId) && vanillaId is (>= 0 and < Farm.layout_max); - // handle - if (Game1.whichFarm == farmType) - { - monitor.Log($"Your current farm is already set to {farmType} ({SetFarmTypeCommand.GetFarmLabel(farmType)}).", LogLevel.Info); - return; - } - - this.SetFarmType(farmType); - monitor.Log($"Your current farm has been converted to {farmType} ({SetFarmTypeCommand.GetFarmLabel(farmType)}).", LogLevel.Warn); - monitor.Log("Saving and reloading is recommended to make sure everything is updated for the change.", LogLevel.Warn); + // handle argument + if (farmType == "list") + this.HandleList(monitor); + else if (isVanillaId) + this.HandleVanillaFarmType(vanillaId, monitor); + else + this.HandleCustomFarmType(farmType, monitor); } /********* ** Private methods *********/ - /// Change the farm type to the given value. - /// The farm type ID. - private void SetFarmType(int type) + /**** + ** Handlers + ****/ + /// Print a list of available farm types. + /// Writes messages to the console and log file. + private void HandleList(IMonitor monitor) { - Game1.whichFarm = type; + StringBuilder result = new(); + // list vanilla types + result.AppendLine("The farm type can be one of these vanilla types:"); + foreach (var type in this.GetVanillaFarmTypes()) + result.AppendLine($" - {type.Key} ({type.Value})"); + result.AppendLine(); + + // list custom types + { + var customTypes = this.GetCustomFarmTypes(); + if (customTypes.Any()) + { + result.AppendLine("Or one of these custom farm types:"); + foreach (var type in customTypes.Values.OrderBy(p => p.ID)) + result.AppendLine($" - {type.ID} ({this.GetCustomName(type)})"); + } + else + result.AppendLine("Or a custom farm type (though none is loaded currently)."); + } + + // print + monitor.Log(result.ToString(), LogLevel.Info); + } + + /// Set a vanilla farm type. + /// The farm type. + /// Writes messages to the console and log file. + private void HandleVanillaFarmType(int type, IMonitor monitor) + { + if (Game1.whichFarm == type) + { + monitor.Log($"Your current farm is already set to {type} ({this.GetVanillaName(type)}).", LogLevel.Info); + return; + } + + this.SetFarmType(type, null); + this.PrintSuccess(monitor, $"{type} ({this.GetVanillaName(type)}"); + } + + /// Set a custom farm type. + /// The farm type ID. + /// Writes messages to the console and log file. + private void HandleCustomFarmType(string id, IMonitor monitor) + { + if (Game1.whichModFarm?.ID == id) + { + monitor.Log($"Your current farm is already set to {id} ({this.GetCustomName(Game1.whichModFarm)}).", LogLevel.Info); + return; + } + + if (!this.GetCustomFarmTypes().TryGetValue(id, out ModFarmType customFarmType)) + { + monitor.Log($"Invalid farm type '{id}'. Enter `help set_farm_type` for more info.", LogLevel.Error); + return; + } + + this.SetFarmType(Farm.mod_layout, customFarmType); + this.PrintSuccess(monitor, $"{id} ({this.GetCustomName(customFarmType)})"); + } + + /// Change the farm type. + /// The farm type ID. + /// The custom farm type data, if applicable. + private void SetFarmType(int type, ModFarmType customFarmData) + { + // set flags + Game1.whichFarm = type; + Game1.whichModFarm = customFarmData; + + // update farm map Farm farm = Game1.getFarm(); farm.mapPath.Value = $@"Maps\{Farm.getMapNameFromTypeInt(Game1.whichFarm)}"; farm.reloadMap(); + + // clear spouse area cache to avoid errors + FieldInfo cacheField = farm.GetType().GetField("_baseSpouseAreaTiles", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + if (cacheField == null) + throw new InvalidOperationException("Failed to access '_baseSpouseAreaTiles' field to clear spouse area cache."); + if (cacheField.GetValue(farm) is not IDictionary cache) + throw new InvalidOperationException($"The farm's '_baseSpouseAreaTiles' field didn't match the expected {nameof(IDictionary)} type."); + cache.Clear(); } + private void PrintSuccess(IMonitor monitor, string label) + { + StringBuilder result = new(); + result.AppendLine($"Your current farm has been converted to {label}. Saving and reloading is recommended to make sure everything is updated for the change."); + result.AppendLine(); + result.AppendLine("This doesn't move items that are out of bounds on the new map. If you need to clean up, you can..."); + result.AppendLine(" - temporarily switch back to the previous farm type;"); + result.AppendLine(" - or use a mod like Noclip Mode: https://www.nexusmods.com/stardewvalley/mods/3900 ;"); + result.AppendLine(" - or use the world_clear console command (enter `help world_clear` for details)."); + + monitor.Log(result.ToString(), LogLevel.Warn); + } + + /**** + ** Vanilla farm types + ****/ /// Get the display name for a vanilla farm type. /// The farm type. - private static string GetFarmLabel(int type) + private string GetVanillaName(int type) { string translationKey = type switch { @@ -87,5 +177,45 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player ? Game1.content.LoadString(@$"Strings\UI:{translationKey}").Split('_')[0] : type.ToString(); } + + /// Get the available vanilla farm types by ID. + private IDictionary GetVanillaFarmTypes() + { + IDictionary farmTypes = new Dictionary(); + + foreach (int id in Enumerable.Range(0, Farm.layout_max)) + farmTypes[id] = this.GetVanillaName(id); + + return farmTypes; + } + + /**** + ** Custom farm types + ****/ + /// Get the display name for a custom farm type. + /// The custom farm type. + private string GetCustomName(ModFarmType farmType) + { + if (string.IsNullOrWhiteSpace(farmType?.TooltipStringPath)) + return farmType?.ID; + + return Game1.content.LoadString(farmType.TooltipStringPath)?.Split('_')[0] ?? farmType.ID; + } + + /// Get the available custom farm types by ID. + private IDictionary GetCustomFarmTypes() + { + IDictionary farmTypes = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (ModFarmType farmType in Game1.content.Load>("Data\\AdditionalFarms")) + { + if (string.IsNullOrWhiteSpace(farmType.ID)) + continue; + + farmTypes[farmType.ID] = farmType; + } + + return farmTypes; + } } } diff --git a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj index 52439c41..e3db8b47 100644 --- a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj @@ -16,6 +16,7 @@ +