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;
}
}
}