using System; using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.Xna.Framework; using StardewValley; using StardewValley.Locations; using StardewValley.Objects; using StardewValley.TerrainFeatures; using SObject = StardewValley.Object; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World { /// A command which clears in-game objects. [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Loaded using reflection")] internal class ClearCommand : ConsoleCommand { /********* ** Fields *********/ /// The valid types that can be cleared. private readonly string[] ValidTypes = { "crops", "debris", "fruit-trees", "furniture", "grass", "trees", "removable", "everything" }; /// The resource clump IDs to consider debris. private readonly int[] DebrisClumps = { ResourceClump.stumpIndex, ResourceClump.hollowLogIndex, ResourceClump.meteoriteIndex, ResourceClump.boulderIndex }; /********* ** Public methods *********/ /// Construct an instance. public ClearCommand() : base( name: "world_clear", description: "Clears in-game entities in a given location.\n\n" + "Usage: world_clear \n" + " - location: the location name for which to clear objects (like Farm), or 'current' for the current location.\n" + " - object type: the type of object clear. You can specify 'crops', 'debris' (stones/twigs/weeds and dead crops), 'furniture', 'grass', and 'trees' / 'fruit-trees'. You can also specify 'removable' (remove everything that can be removed or destroyed during normal gameplay) or 'everything' (remove everything including permanent bushes)." ) { } /// Handle the command. /// Writes messages to the console and log file. /// The command name. /// The command arguments. public override void Handle(IMonitor monitor, string command, ArgumentParser args) { // check context if (!Context.IsWorldReady) { monitor.Log("You need to load a save to use this command.", LogLevel.Error); return; } // parse arguments if (!args.TryGet(0, "location", out string? locationName, required: true)) return; if (!args.TryGet(1, "object type", out string? type, required: true, oneOf: this.ValidTypes)) return; // get target location GameLocation? location = Game1.locations.FirstOrDefault(p => p.Name != null && p.Name.Equals(locationName, StringComparison.OrdinalIgnoreCase)); if (location == null && locationName == "current") location = Game1.currentLocation; if (location == null) { string[] locationNames = (from loc in Game1.locations where !string.IsNullOrWhiteSpace(loc.Name) orderby loc.Name select loc.Name).ToArray(); monitor.Log($"Could not find a location with that name. Must be one of [{string.Join(", ", locationNames)}].", LogLevel.Error); return; } // apply switch (type) { case "crops": { int removed = this.RemoveTerrainFeatures(location, p => p is HoeDirt) + this.RemoveResourceClumps(location, p => p is GiantCrop); monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info); break; } case "debris": { int removed = 0; foreach (var pair in location.terrainFeatures.Pairs.ToArray()) { TerrainFeature feature = pair.Value; if (feature is HoeDirt dirt && dirt.crop?.dead == true) { dirt.crop = null; removed++; } } removed += this.RemoveObjects(location, obj => obj is not Chest && ( obj.Name is "Weeds" or "Stone" || obj.ParentSheetIndex is 294 or 295 ) ) + this.RemoveResourceClumps(location, clump => this.DebrisClumps.Contains(clump.parentSheetIndex.Value)); monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info); break; } case "fruit-trees": { int removed = this.RemoveTerrainFeatures(location, feature => feature is FruitTree); monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info); break; } case "furniture": { int removed = this.RemoveFurniture(location, _ => true); monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info); break; } case "grass": { int removed = this.RemoveTerrainFeatures(location, feature => feature is Grass); monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info); break; } case "trees": { int removed = this.RemoveTerrainFeatures(location, feature => feature is Tree); monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info); break; } case "removable": case "everything": { bool everything = type == "everything"; int removed = this.RemoveFurniture(location, _ => true) + this.RemoveObjects(location, _ => true) + this.RemoveTerrainFeatures(location, _ => true) + this.RemoveLargeTerrainFeatures(location, p => everything || p is not Bush bush || bush.isDestroyable(location, p.currentTileLocation)) + this.RemoveResourceClumps(location, _ => true); monitor.Log($"Done! Removed {removed} entities from {location.Name}.", LogLevel.Info); break; } default: monitor.Log($"Unknown type '{type}'. Must be one [{string.Join(", ", this.ValidTypes)}].", LogLevel.Error); break; } } /********* ** Private methods *********/ /// Remove objects from a location matching a lambda. /// The location to search. /// Whether an entity should be removed. /// Returns the number of removed entities. private int RemoveObjects(GameLocation location, Func shouldRemove) { int removed = 0; foreach ((Vector2 tile, SObject? obj) in location.Objects.Pairs.ToArray()) { if (shouldRemove(obj)) { location.Objects.Remove(tile); removed++; } } return removed; } /// Remove terrain features from a location matching a lambda. /// The location to search. /// Whether an entity should be removed. /// Returns the number of removed entities. private int RemoveTerrainFeatures(GameLocation location, Func shouldRemove) { int removed = 0; foreach ((Vector2 tile, TerrainFeature? feature) in location.terrainFeatures.Pairs.ToArray()) { if (shouldRemove(feature)) { location.terrainFeatures.Remove(tile); removed++; } } return removed; } /// Remove large terrain features from a location matching a lambda. /// The location to search. /// Whether an entity should be removed. /// Returns the number of removed entities. private int RemoveLargeTerrainFeatures(GameLocation location, Func shouldRemove) { int removed = 0; foreach (LargeTerrainFeature feature in location.largeTerrainFeatures.ToArray()) { if (shouldRemove(feature)) { location.largeTerrainFeatures.Remove(feature); removed++; } } return removed; } /// Remove resource clumps from a location matching a lambda. /// The location to search. /// Whether an entity should be removed. /// Returns the number of removed entities. private int RemoveResourceClumps(GameLocation location, Func shouldRemove) { int removed = 0; foreach (ResourceClump clump in location.resourceClumps.Where(shouldRemove).ToArray()) { location.resourceClumps.Remove(clump); removed++; } if (location is Woods woods) { foreach (ResourceClump clump in woods.stumps.Where(shouldRemove).ToArray()) { woods.stumps.Remove(clump); removed++; } } return removed; } /// Remove furniture from a location matching a lambda. /// The location to search. /// Whether an entity should be removed. /// Returns the number of removed entities. private int RemoveFurniture(GameLocation location, Func shouldRemove) { int removed = 0; foreach (Furniture furniture in location.furniture.ToArray()) { if (shouldRemove(furniture)) { location.furniture.Remove(furniture); removed++; } } return removed; } } }