diff --git a/docs/changelog.txt b/docs/changelog.txt index 3008dac024..7bd5fff2b6 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -54,6 +54,7 @@ Template for new versions: ## New Tools ## New Features +- `clean`: positional options to limit cleaning to a cuboid area. Options to remove grass or skip blood spatter removal. - `stockpiles`: add simple import/export dialogs to stockpile overlay panel ## Fixes @@ -69,10 +70,12 @@ Template for new versions: - Added example code for creating plugin RPC endpoints that can be used to extend the DFHack API ## API +- ``Maps::isPlantInBox``: function to determine if a plant is present in a cuboid. - ``Units::isUnitInBox``, ``Units::getUnitsInBox``: add versions accepting pos arguments - ``Units::getVisibleName``: when acting on a unit without an impersonated identity, returns the unit's name structure instead of the associated histfig's name structure ## Lua +- ``dfhack.maps.isPlantInBox``: function to determine if a plant is present in a cuboid. - ``dfhack.units.isUnitInBox``, ``dfhack.units.getUnitsInBox``: add versions accepting pos arguments - ``widgets.FilteredList``: search keys for list items can now be functions that return a string diff --git a/docs/dev/Lua API.rst b/docs/dev/Lua API.rst index 64bea03c3a..6c18807400 100644 --- a/docs/dev/Lua API.rst +++ b/docs/dev/Lua API.rst @@ -2307,6 +2307,13 @@ Maps module Returns the plant struct that owns the tile at the specified position. +* ``dfhack.maps.isPlantInBox(plant, pos1, pos2)`` +* ``dfhack.maps.isPlantInBox(plant,x1,y1,z1,x2,y2,z2)`` + + Returns true if the plant is within a box defined by the specified + coordinates. For trees, returns true if any part of the tree is within + the box. + * ``dfhack.maps.getWalkableGroup(pos)`` Returns the walkability group for the given tile position. A return value diff --git a/docs/plugins/cleaners.rst b/docs/plugins/cleaners.rst index 8b2e6f71f4..02a918ed26 100644 --- a/docs/plugins/cleaners.rst +++ b/docs/plugins/cleaners.rst @@ -5,57 +5,142 @@ cleaners ======== .. dfhack-tool:: - :summary: Provides commands for cleaning spatter from the map. - :tags: adventure fort armok fps items map units + :summary: Cleans spatter and/or grass from map tiles. + :tags: adventure fort armok fps items map plants units :no-command: .. dfhack-command:: clean :summary: Removes contaminants. .. dfhack-command:: spotclean - :summary: Remove all contaminants from the tile under the cursor. + :summary: Remove contaminants from the tile under the cursor. -This plugin provides commands that clean the splatter that get scattered all +This plugin provides commands that clean the spatter that gets scattered all over the map and that clings to your items and units. In an old fortress, -cleaning with this tool can significantly reduce FPS lag! It can also spoil your -!!FUN!!, so think before you use it. +cleaning with this tool can significantly reduce FPS lag! It can also spoil +your !!FUN!!, so think before you use it. + +This plugin can also be used to remove grass in an area you don't want it +(including those hard to reach areas like stairs and under buildings). +Use it if you messed up with the `regrass` tool, or grass grew in your +dining hall after a flooding mishap. Grass may eventually regrow if the tile +remains soil, so `gui/tiletypes` may come in handy to change it to an +appropriate stone type. Usage ----- :: - clean all|map|items|units|plants [] + clean [ []] [] spotclean -By default, cleaning the map leaves mud and snow alone. Note that cleaning units -includes hostiles, and that cleaning items removes poisons from weapons. +By default, cleaning the map leaves mud, snow, and item spatter (e.g., tree +droppings) alone. Note that cleaning units includes hostiles, and that +cleaning items removes poisons from weapons. + +Mud will not be cleaned out from under farm plots unless the tile is furrowed +soil, since that would render the plot inoperable. + +Operates on the entire map unless otherwise specified. Supplying a ``pos`` +argument can limit operation to a single tile. Supplying both can operate on +a cuboid region. ``pos`` should normally be in the form ``0,0,0``, without +spaces. The string ``here`` can be used in place of numeric coordinates to use +the position of the keyboard cursor, if active. The ``--zlevel`` option uses +the ``pos`` values differently. -``spotclean`` works like ``clean map snow mud``, removing all contaminants from -the tile under the keyboard cursor. This is ideal if you just want to clean a -specific tile but don't want the `clean ` command to remove all the -glorious blood from your entranceway. +``spotclean`` works like ``clean here --map --mud --snow``, removing all +contaminants from a specific tile quickly. Intended as a hotkey command. -Mud will not be cleaned out from under farm plots, since that would render the -plot inoperable. Examples -------- -``clean all`` - Clean everything that can be cleaned (except mud and snow). -``clean map mud item snow`` - Removes all spatter, including mud, leaves, and snow from map tiles. Farm - plots will retain their mud. +``clean --all`` + Clean most spatter from all map tiles (excluding mud, snow, and item + spatter), as well as all contaminants from units and items. + +``clean --map --mud --snow --item`` + Removes all spatter, including mud, snow, and tree droppings from map + tiles. Farm plots will retain their mud as needed. + +``clean --map --item --only --items`` + Remove only tree droppings from the map (leaving blood and other + contaminants untouched). Clean all items of their contaminants. + +``clean here -mdstu`` + Clean any sort of contaminant from the map tile under the keyboard cursor, + as well as any contaminant found on a unit there, but don't touch contaminants + on any item (e.g., the unit's poisoned weapon). + +``clean 0,0,90 0,0,120 -uiz`` + Clean all contaminants from units and items on z-levels 90 through 120. + Don't touch map spatter at all. + +``clean -mgxoz`` + Reduce all grass on the current z-level to barren soil. Don't touch + any contaminants. + +``clean 0,0,100 19,19,119 -adstg`` + Remove all contaminants of any type from the 20 x 20 x 20 cube defined + by the coords, including on any units, items, and ground spatter + (excluding mud for farms). Also remove any unused grass type events from + the affected map blocks, but don't remove any grass present. Options ------- -When cleaning the map, you can specify extra options for extra cleaning: - -``mud`` - Also remove mud. -``item`` - Also remove item spatter, like fallen leaves and flowers. -``snow`` - Also remove snow coverings. +``-a``, ``--all`` + Equivalent to ``--map --units --items``. +``-m``, ``--map`` + Clean selected map tiles. Cleans most spatter by default, but not mud, + snow, or item spatter. +``-d``, ``--mud`` + Also remove mud from map tiles. Excludes mud required for farm plots. +``-s``, ``--snow`` + Also remove snow coverings from map tiles. +``-t``, ``--item`` + Also remove item spatter (e.g., fallen leaves, flowers, and gatherable + tree fruit) from map tiles. Not to be confused with ``--items``, which + cleans contaminants *off* of items. +``-g``, ``--grass`` + Remove unused (entirely depleted) grass events from map blocks. DF will + create a grass event for each type of grass that grows in a block, but + doesn't remove them if you pave over the block or if the grass got + depleted and entirely replaced with a different type. Could possibly + improve FPS if you had a ton of unused grass events everywhere (a likely + outcome of using ``regrass --new``). Requires ``--map`` option to be + specified. + +``-x``, ``--desolate`` + Use with caution! Remove grass from the selected area. You probably don't + want to use this on the entire map, so make sure to use ``pos`` arguments. + Requires ``--map`` and ``--grass`` options to be specified. +``-o``, ``--only`` + Ignore most spatter (e.g., blood, vomit, and ooze) and focus only on the + other specified options. Requires ``--map`` and at least one of: ``--mud``, + ``--snow``, ``--item``, or ``--grass``. +``-u``, ``--units`` + Clean all contaminants off of units in the selected area. Not affected by + map options that specify spatter types (e.g., snow). Units will always be + completely cleaned. +``-i``, ``--items`` + Clean all contaminants off of items in the selected area (including those + held by units). Not affected by map options that specify spatter types. + Not to be confused with ``--item``, which removes tree droppings found on + the ground. +``-z``, ``--zlevel`` + Select entire z-levels. Will do all z-levels between ``pos`` arguments if + both are given, z-level of first ``pos`` if one is given, else z-level of + current view if no ``pos`` is given. + +Troubleshooting +--------------- + +Use ``debugfilter set Debug cleaners log`` (or +``debugfilter set Trace cleaners log`` for more detail) to help diagnose +issues. (Avoid cleaning large parts of the map using many options with +Trace enabled, as it could make the game unresponsive and flood the console +for a good minute.) + +Disable with ``debugfilter set Info cleaners log``. diff --git a/docs/plugins/regrass.rst b/docs/plugins/regrass.rst index 8d42fe5a77..f4134c13ee 100644 --- a/docs/plugins/regrass.rst +++ b/docs/plugins/regrass.rst @@ -9,6 +9,10 @@ This command can refresh the grass (and subterranean moss) growing on your map. Operates on floors, stairs, and ramps. Also works underneath shrubs, saplings, and tree trunks. Ignores furrowed soil and wet sand (beaches). +The `cleaners` tool can help remove grass if you messed up and suddenly there +are staring eyeballs growing all over your fort. `gui/tiletypes` can then be used +to change the soil back to stone. + Usage ----- @@ -74,7 +78,10 @@ Options ``-n``, ``--new`` Adds all biome-compatible grass types that were not originally present in the map block. Allows regrass to work in blocks that never had any grass to - begin with. Will still fail in incompatible biomes. + begin with. Will still fail in incompatible biomes. Note: This can add an + excessive number of grass events to your map, so it may be desirable to run + ``clean --map --grass --only`` (see: `cleaners`) afterwards to clean up any + unused events. ``-f``, ``--force`` Force a grass type on tiles with no compatible grass types. Unsets the ``no_grow`` flag on all tiles. The ``--new`` option takes precedence for diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index a5225c57c8..ca3a2d7eac 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -2556,6 +2556,29 @@ static int maps_getPlantAtTile(lua_State *L) return 1; } +static int maps_isPlantInBox(lua_State *L) +{ + auto plant = Lua::CheckDFObject(L, 1); + if (lua_gettop(L) > 3) + { + int x1 = luaL_checkint(L, 2); + int y1 = luaL_checkint(L, 3); + int z1 = luaL_checkint(L, 4); + int x2 = luaL_checkint(L, 5); + int y2 = luaL_checkint(L, 6); + int z2 = luaL_checkint(L, 7); + lua_pushboolean(L, Maps::isPlantInBox(plant, x1, y1, z1, x2, y2, z2)); + } + else + { + df::coord pos1, pos2; + Lua::CheckDFAssign(L, &pos1, 2); + Lua::CheckDFAssign(L, &pos2, 3); + lua_pushboolean(L, Maps::isPlantInBox(plant, pos1, pos2)); + } + return 1; +} + static int maps_getBiomeType(lua_State *L) { auto pos = CheckCoordXY(L, 1, true); @@ -2621,6 +2644,7 @@ static const luaL_Reg dfhack_maps_funcs[] = { { "getRegionBiome", maps_getRegionBiome }, { "getTileBiomeRgn", maps_getTileBiomeRgn }, { "getPlantAtTile", maps_getPlantAtTile }, + { "isPlantInBox", maps_isPlantInBox }, { "getBiomeType", maps_getBiomeType }, { "isTileAquifer", maps_isTileAquifer }, { "isTileHeavyAquifer", maps_isTileHeavyAquifer }, diff --git a/library/include/modules/Maps.h b/library/include/modules/Maps.h index f5a3be142c..b632bec16a 100644 --- a/library/include/modules/Maps.h +++ b/library/include/modules/Maps.h @@ -382,6 +382,11 @@ DFHACK_EXPORT bool canStepBetween(df::coord pos1, df::coord pos2); // Get the plant that owns the tile at the specified position. extern DFHACK_EXPORT df::plant *getPlantAtTile(int32_t x, int32_t y, int32_t z); inline df::plant *getPlantAtTile(df::coord pos) { return getPlantAtTile(pos.x, pos.y, pos.z); } +// Returns true if the plant is within a box defined by the specified coordinates, accounting for trees. +DFHACK_EXPORT bool isPlantInBox(df::plant *plant, const cuboid &bounds); +inline bool isPlantInBox(df::plant *plant, int16_t x1, int16_t y1, int16_t z1, + int16_t x2, int16_t y2, int16_t z2) { return isPlantInBox(plant, cuboid(x1, y1, z1, x2, y2, z2)); } +inline bool isPlantInBox(df::plant *plant, df::coord pos1, df::coord pos2) { return isPlantInBox(plant, cuboid(pos1, pos2)); } // Get the biome type at the given region coordinates. DFHACK_EXPORT df::enums::biome_type::biome_type getBiomeTypeWithRef(int16_t region_x, int16_t region_y, int16_t region_ref_y); diff --git a/library/lua/tile-material.lua b/library/lua/tile-material.lua index c0fe2e7cb6..dadbb38b43 100644 --- a/library/lua/tile-material.lua +++ b/library/lua/tile-material.lua @@ -172,33 +172,10 @@ end -- GetTreeMat returns the material of the tree at the given tile or nil if the tile does not have a -- tree or giant mushroom. --- Currently roots are ignored. function GetTreeMat(x, y, z) local pos = prepPos(x, y, z) - - local function coordInTree(pos, tree) - local x1 = tree.pos.x - math.floor(tree.tree_info.dim_x / 2) - local x2 = tree.pos.x + math.floor(tree.tree_info.dim_x / 2) - local y1 = tree.pos.y - math.floor(tree.tree_info.dim_y / 2) - local y2 = tree.pos.y + math.floor(tree.tree_info.dim_y / 2) - local z1 = tree.pos.z - local z2 = tree.pos.z + tree.tree_info.body_height - - if not ((pos.x >= x1 and pos.x <= x2) and (pos.y >= y1 and pos.y <= y2) and (pos.z >= z1 and pos.z <= z2)) then - return false - end - - return not tree.tree_info.body[pos.z - tree.pos.z]:_displace((pos.y - y1) * tree.tree_info.dim_x + (pos.x - x1)).blocked - end - - for _, tree in ipairs(df.global.world.plants.all) do - if tree.tree_info ~= nil then - if coordInTree(pos, tree) then - return dfhack.matinfo.decode(419, tree.material) - end - end - end - return nil + local plant = dfhack.maps.getPlantAtTile(pos) + return plant and plant.tree_info and dfhack.matinfo.decode(419, plant.material) or nil end -- GetShrubMat returns the material of the shrub at the given tile or nil if the tile does not @@ -289,7 +266,7 @@ BasicMats = { [df.tiletype_material.DRIFTWOOD] = GetLayerMat, [df.tiletype_material.POOL] = GetLayerMat, [df.tiletype_material.BROOK] = GetLayerMat, - [df.tiletype_material.ROOT] = GetLayerMat, + [df.tiletype_material.ROOT] = GetTreeMat, [df.tiletype_material.TREE] = GetTreeMat, [df.tiletype_material.MUSHROOM] = GetTreeMat, [df.tiletype_material.UNDERWORLD_GATE] = nil, -- I guess this is for the gates found in vaults? @@ -327,6 +304,7 @@ OnlyPlantMats = { [df.tiletype_material.GRASS_DRY] = GetGrassMat, [df.tiletype_material.GRASS_DEAD] = GetGrassMat, [df.tiletype_material.PLANT] = GetShrubMat, + [df.tiletype_material.ROOT] = GetTreeMat, [df.tiletype_material.TREE] = GetTreeMat, [df.tiletype_material.MUSHROOM] = GetTreeMat, } diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index c207360ac4..069b135168 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -938,6 +938,47 @@ df::plant *Maps::getPlantAtTile(int32_t x, int32_t y, int32_t z) return NULL; } +bool Maps::isPlantInBox(df::plant *plant, const cuboid &bounds) +{ + if (!bounds.isValid()) + return false; + else if (bounds.containsPos(plant->pos)) + return true; + else if (!plant->tree_info) + return false; + + auto &pos = plant->pos; + auto &t = *(plant->tree_info); + // Northwest x/y pos of tree bounds + int x_NW = pos.x - (t.dim_x >> 1); + int y_NW = pos.y - (t.dim_y >> 1); + + if (!cuboid(max(0, x_NW), max(0, y_NW), max(0, pos.z - t.roots_depth), + x_NW + t.dim_x, y_NW + t.dim_y, pos.z + t.body_height - 1).clamp(bounds).isValid()) + { // No intersection of tree bounds with cuboid + return false; + } + + int xy_size = t.dim_x * t.dim_y; + // Iterate tree body + for (int z_idx = 0; z_idx < t.body_height; z_idx++) + for (int xy_idx = 0; xy_idx < xy_size; xy_idx++) + if ((t.body[z_idx][xy_idx].whole & 0x7F) != 0 && // Any non-blocked + bounds.containsPos(x_NW + xy_idx % t.dim_x, y_NW + xy_idx / t.dim_x, pos.z + z_idx)) + { + return true; + } + // Iterate tree roots + for (int z_idx = 0; z_idx < t.roots_depth; z_idx++) + for (int xy_idx = 0; xy_idx < xy_size; xy_idx++) + if ((t.roots[z_idx][xy_idx].whole & 0x7F) != 0 && // Any non-blocked + bounds.containsPos(x_NW + xy_idx % t.dim_x, y_NW + xy_idx / t.dim_x, pos.z - z_idx - 1)) + { + return true; + } + return false; +} + /* * Biomes */ diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index ff71159e5e..5862e75259 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -56,7 +56,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(changevein changevein.cpp) #add_subdirectory(channel-safely) dfhack_plugin(cleanconst cleanconst.cpp) - dfhack_plugin(cleaners cleaners.cpp) + dfhack_plugin(cleaners cleaners.cpp LINK_LIBRARIES lua) dfhack_plugin(cleanowned cleanowned.cpp) dfhack_plugin(createitem createitem.cpp) dfhack_plugin(cursecheck cursecheck.cpp) diff --git a/plugins/burrow.cpp b/plugins/burrow.cpp index d06e95efb6..2882abbe5b 100644 --- a/plugins/burrow.cpp +++ b/plugins/burrow.cpp @@ -1,3 +1,5 @@ +// Quickly adjust burrow tiles and units. + #include "Debug.h" #include "LuaTools.h" #include "PluginManager.h" @@ -18,8 +20,8 @@ #include "df/unit.h" #include "df/world.h" -using std::vector; using std::string; +using std::vector; using namespace DFHack; DFHACK_PLUGIN("burrow"); @@ -44,7 +46,7 @@ static void init_diggers(color_ostream& out); static void jobStartedHandler(color_ostream& out, void* ptr); static void jobCompletedHandler(color_ostream& out, void* ptr); -DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { +DFhackCExport command_result plugin_init(color_ostream &out, vector &commands) { DEBUG(status, out).print("initializing %s\n", plugin_name); commands.push_back( PluginCommand("burrow", @@ -115,7 +117,7 @@ static void init_diggers(color_ostream& out) { return; } - std::vector pvec; + vector pvec; int start_id = 0; if (Job::listNewlyCreated(&pvec, &start_id)) { for (auto job : pvec) { @@ -144,16 +146,12 @@ static void jobStartedHandler(color_ostream& out, void* ptr) { static void add_walls_to_burrow(color_ostream &out, df::burrow* b, const df::coord & pos1, const df::coord & pos2) { - for (int z = pos1.z; z <= pos2.z; z++) { - for (int y = pos1.y; y <= pos2.y; y++) { - for (int x = pos1.x; x <= pos2.x; x++) { - df::coord pos(x,y,z); - df::tiletype *tt = Maps::getTileType(pos); - if (tt && isWallTerrain(*tt)) - Burrows::setAssignedTile(b, pos, true); - } - } - } + Maps::forCoord([&b](df::coord pos) { + auto tt = Maps::getTileType(pos); + if (tt && isWallTerrain(*tt)) + Burrows::setAssignedTile(b, pos, true); + return true; // next pos + }, pos1, pos2); } static void expand_burrows(color_ostream &out, const df::coord & pos, df::tiletype prev_tt, df::tiletype tt) { @@ -325,7 +323,7 @@ static void setTilesByDesignation(df::burrow *target, df::tile_designation d_mas } } -static bool setTilesByKeyword(df::burrow *target, std::string name, bool enable) { +static bool setTilesByKeyword(df::burrow *target, string name, bool enable) { CHECK_NULL_POINTER(target); df::tile_designation mask; diff --git a/plugins/cleaners.cpp b/plugins/cleaners.cpp index 6706bc92d8..b991bdc0c3 100644 --- a/plugins/cleaners.cpp +++ b/plugins/cleaners.cpp @@ -1,114 +1,289 @@ +// Cleans spatter and/or grass from map tiles. + +#include "Debug.h" +#include "LuaTools.h" #include "PluginManager.h" +#include "TileTypes.h" #include "modules/Buildings.h" +#include "modules/Items.h" +#include "modules/Gui.h" #include "modules/Maps.h" +#include "modules/Units.h" #include "df/block_square_event.h" +#include "df/block_square_event_grassst.h" +#include "df/block_square_event_item_spatterst.h" #include "df/block_square_event_material_spatterst.h" #include "df/building.h" #include "df/builtin_mats.h" #include "df/item_actual.h" #include "df/map_block.h" -#include "df/plant.h" -#include "df/plant_spatter.h" #include "df/spatter.h" #include "df/unit.h" #include "df/unit_spatter.h" #include "df/world.h" -using std::vector; using std::string; +using std::vector; using namespace DFHack; using namespace df::enums; DFHACK_PLUGIN("cleaners"); REQUIRE_GLOBAL(world); -REQUIRE_GLOBAL(cursor); -static void clean_mud_safely(df::block_square_event_material_spatterst *spatter, - const df::coord &block_pos, const df::coord &offset) +namespace DFHack { - df::coord pos = block_pos + offset; - auto bld = Buildings::findAtTile(pos); - if (!bld || bld->getType() != building_type::FarmPlot) - spatter->amount[offset.x][offset.y] = 0; + DBG_DECLARE(cleaners, log, DebugCategory::LINFO); } -command_result cleanmap (color_ostream &out, bool snow, bool mud, bool item_spatter) +struct clean_options { - // Invoked from clean(), already suspended - int num_blocks = 0; - for (auto block : world->map.map_blocks) + bool map = false; // Clean spatter from the ground + bool mud = false; // Clean mud when doing map + bool snow = false; // Clean snow when doing map + bool item_spat = false; // Clean item spatter when doing map + bool grass = false; // Delete surplus grass events when doing map + bool desolate = false; // Remove all grass when doing map. Requires --grass. Careful! + bool only = false; // Ignore blood/other when doing map, only do specified options + bool units = false; // Clean spatter from units + bool items = false; // Clean spatter from items + bool zlevel = false; // Operate on entire z-levels + + static struct_identity _identity; +}; +static const struct_field_info clean_options_fields[] = +{ + { struct_field_info::PRIMITIVE, "map", offsetof(clean_options, map), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "mud", offsetof(clean_options, mud), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "snow", offsetof(clean_options, snow), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "item_spat", offsetof(clean_options, item_spat), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "grass", offsetof(clean_options, grass), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "desolate", offsetof(clean_options, desolate), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "only", offsetof(clean_options, only), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "units", offsetof(clean_options, units), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "items", offsetof(clean_options, items), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "zlevel", offsetof(clean_options, zlevel), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::END } +}; +struct_identity clean_options::_identity(sizeof(clean_options), &df::allocator_fn, NULL, "clean_options", NULL, clean_options_fields); + +static bool clean_mud_safely(color_ostream &out, df::block_square_event_material_spatterst *spatter, const df::coord &pos) +{ // Avoid cleaning mud on farm tiles that need it, return true on success + auto &amt = spatter->amount[pos.x&15][pos.y&15]; + if (amt == 0) + return false; // Nothing cleaned + auto tt = Maps::getTileType(pos); + + if (tt && *tt != tiletype::FurrowedSoil) + { // Not furrowed soil, mud might be required + auto bld = Buildings::findAtTile(pos); + if (bld && bld->getType() == building_type::FarmPlot) + { // A farm needs the mud + DEBUG(log, out).print("Protecting mud at (%d,%d,%d)\n", pos.x, pos.y, pos.z); + return false; // Won't clean + } + } + amt = 0; + TRACE(log, out).print("Cleaned mud at (%d,%d,%d)\n", pos.x, pos.y, pos.z); + return true; +} + +static void degrass_tt(color_ostream &out, df::map_block *block, int tx, int ty) +{ // Convert grass to soil + auto &tt = block->tiletype[tx][ty]; + auto mat = tileMaterial(tt); + + if (mat < tiletype_material::GRASS_LIGHT || + mat > tiletype_material::GRASS_DEAD) + return; // Grass under a sapling, etc. + + auto shape = tileShape(tt); + df::tiletype new_tt = tiletype::Void; + + if (shape == tiletype_shape::FLOOR) + new_tt = findRandomVariant(tiletype::SoilFloor1); + else // Ramp or stairs + new_tt = findTileType(shape, tiletype_material::SOIL, tiletype_variant::NONE, tiletype_special::NONE, nullptr); + + TRACE(log, out).print("Degrass %s to %s\n", ENUM_KEY_STR(tiletype, tt).c_str(), ENUM_KEY_STR(tiletype, new_tt).c_str()); + if (new_tt != tiletype::Void) + tt = new_tt; // Assign new tiletype +} + +#define DEL_BLEV block->block_events.erase(block->block_events.begin() + i); delete blev; cleaned = true; + +static bool clean_block(color_ostream &out, df::map_block *block, const cuboid &inter, const clean_options &options) +{ // Perform cleaning on intersection of map block, return true if any tile cleaned + if (!block || !inter.isValid()) { - bool cleaned = false; - for(int x = 0; x < 16; x++) - { - for(int y = 0; y < 16; y++) - { - block->occupancy[x][y].bits.arrow_color = 0; - block->occupancy[x][y].bits.arrow_variant = 0; + DEBUG(log, out).print("Failed cleaning block <%p>\n", block); + return false; + } + DEBUG(log, out).print("Cleaning block at (%d,%d,%d)\n", + block->map_pos.x, block->map_pos.y, block->map_pos.z); + bool cleaned = false; + // Clean arrow debris + inter.forCoord([&](df::coord pos) { + auto &occ = block->occupancy[pos.x&15][pos.y&15]; + cleaned |= (bool)occ.bits.arrow_color; + + occ.bits.arrow_color = 0; + occ.bits.arrow_variant = 0; + return true; // Next pos + }); + // Knowing if block is fully inside cuboid helps optimize + bool full_block = (inter.x_max - inter.x_min) == 15 && (inter.y_max - inter.y_min) == 15; + TRACE(log, out).print("full_block = %d\n", full_block); + // Clean relevant spatter + for (size_t i = block->block_events.size(); i-- > 0;) + { // Iterate block events (blev) backwards + auto blev = block->block_events[i]; + if (auto ms_ev = virtual_cast(blev)) + { // Material spatter + if (ms_ev->mat_type == builtin_mats::MUD && + ms_ev->mat_state == matter_state::Solid) + { // Mud + if (!options.mud) + continue; // Not doing mud, skip + // Handle mud without messing up farms + inter.forCoord([&](df::coord pos) { + cleaned |= clean_mud_safely(out, ms_ev, pos); + return true; // Next pos + }); + // Only delete blev if empty + if (blev->isEmpty()) + { // Block was already empty of mud, or we just made it so + DEBUG(log, out).print("Deleting mud blev at index %lu\n", i); + DEL_BLEV + } + continue; // Next blev + } + else if (ms_ev->mat_type == builtin_mats::WATER && + ms_ev->mat_state == matter_state::Powder) + { // Snow + if (options.snow) + continue; // Not doing snow, skip } + else if (options.only) + continue; // Not doing blood/other, skip + + if (!full_block) + { // Non-mud ms_ev, partial clean + inter.forCoord([&](df::coord pos) { + auto &amt = ms_ev->amount[pos.x&15][pos.y&15]; + cleaned |= (bool)amt; + amt = 0; + return true; // Next pos + }); + // Will now check blev->isEmpty() + } + // else full_block, delete blev } - for (size_t j = 0; j < block->block_events.size(); j++) - { - df::block_square_event *evt = block->block_events[j]; - if (evt->getType() == block_square_event_type::material_spatter) - { - // type verified - recast to subclass - df::block_square_event_material_spatterst *spatter = (df::block_square_event_material_spatterst *)evt; - - // filter snow - if(!snow - && spatter->mat_type == builtin_mats::WATER - && spatter->mat_state == (short)matter_state::Powder) - continue; - // filter mud - if(!mud - && spatter->mat_type == builtin_mats::MUD - && spatter->mat_state == (short)matter_state::Solid) - continue; - // save the farm plots - if(mud - && spatter->mat_type == builtin_mats::MUD - && spatter->mat_state == (short)matter_state::Solid) - { - for (size_t x = 0; x < 16; ++x) - for (size_t y = 0; y < 16; ++y) - clean_mud_safely(spatter, block->map_pos, df::coord(x, y, 0)); - continue; - } + else if (auto is_ev = virtual_cast(blev)) + { // Item spatter + if (!options.item_spat) + continue; // Not doing item spatter, skip + else if (!full_block) + { // Partial clean + inter.forCoord([&](df::coord pos) { + auto &amt = is_ev->amount[pos.x&15][pos.y&15]; + cleaned |= (bool)amt; + amt = 0; + return true; // Next pos + }); + // Will now check blev->isEmpty() } - else if (evt->getType() == block_square_event_type::item_spatter) - { - if (!item_spatter) - continue; + // else full_block, delete blev + } + else if (auto gr_ev = virtual_cast(blev)) + { // Grass + if (!options.grass) + continue; // Not doing grass, skip + else if (options.desolate) + { // We're actively removing grass tiles + inter.forCoord([&](df::coord pos) { + auto &amt = gr_ev->amount[pos.x&15][pos.y&15]; + cleaned |= (bool)amt; + amt = 0; + degrass_tt(out, block, pos.x&15, pos.y&15); + return true; // Next pos + }); } - else - continue; + // Only delete blev if empty + if (blev->isEmpty()) + { // Block was already empty of grass type, or we just made it so + DEBUG(log, out).print("Deleting grass blev at index %lu\n", i); + DEL_BLEV + } + continue; // Next blev + } + else // Unhandled blev type + continue; // Skip - delete evt; - block->block_events.erase(block->block_events.begin() + j); - j--; - cleaned = true; + if (full_block || blev->isEmpty()) + { // Always delete a full block, else ensure blev empty + DEBUG(log, out).print("Deleting blev at index %lu\n", i); + DEL_BLEV } - num_blocks += cleaned; + // Next blev } + DEBUG(log, out).print("cleaned = %d\n", cleaned); + return cleaned; +} +#undef DEL_BLEV - if(num_blocks) - out.print("Cleaned %d of %zd map blocks.\n", num_blocks, world->map.map_blocks.size()); +command_result cleanmap(color_ostream &out, const cuboid &bounds, const clean_options &options) +{ + DEBUG(log, out).print("Cleaning map...\n"); + cuboid my_bounds; // Local copy + if (bounds.isValid()) + my_bounds = bounds; + else + { // Do full map + my_bounds.addPos(0, 0, 0); + my_bounds.addPos(world->map.x_count-1, + world->map.y_count-1, world->map.z_count-1); + DEBUG(log, out).print("Invalid cuboid, selecting full map.\n"); + } + int num_blocks = 0, max_blocks = 0; + + my_bounds.forBlock([&](df::map_block *block, cuboid inter) { + num_blocks += clean_block(out, block, inter, options); + max_blocks++; + return true; // Next block + }); + + if(num_blocks > 0) + out.print("Cleaned %d of %d selected map blocks.\n", num_blocks, max_blocks); return CR_OK; } -command_result cleanitems (color_ostream &out) +command_result cleanitems(color_ostream &out, const cuboid &bounds) { - // Invoked from clean(), already suspended + DEBUG(log, out).print("Cleaning items...\n"); + bool valid_cuboid = bounds.isValid(); // Allow for items outside map if false int cleaned_items = 0, cleaned_total = 0; - for (auto i : world->items.other.IN_PLAY) { - // currently, all item classes extend item_actual, so this should be safe - df::item_actual *item = virtual_cast(i); - if (item && item->contaminants && item->contaminants->size()) + for (auto i : world->items.other.IN_PLAY) + { + TRACE(log, out).print("Considering item #%d\n", i->id); + auto item = virtual_cast(i); + if (!item) { - std::vector saved; + out.printerr("Item #%d isn't item_actual!\n", i->id); + continue; + } + else if (valid_cuboid) + { // Check if item is inside cuboid + auto pos = Items::getPosition(item); + if (!pos.isValid() || !bounds.containsPos(pos)) + continue; + } + TRACE(log, out).print("Selected\n"); + + if (item->contaminants && !item->contaminants->empty()) + { + vector saved; for (size_t j = 0; j < item->contaminants->size(); j++) { auto obj = (*item->contaminants)[j]; @@ -120,146 +295,176 @@ command_result cleanitems (color_ostream &out) cleaned_items++; cleaned_total += item->contaminants->size() - saved.size(); item->contaminants->swap(saved); + DEBUG(log, out).print("Cleaned item #%d\n", item->id); } } - if (cleaned_total) + if (cleaned_total > 0) out.print("Removed %d contaminants from %d items.\n", cleaned_total, cleaned_items); return CR_OK; } -command_result cleanunits (color_ostream &out) +command_result cleanunits(color_ostream &out, const cuboid &bounds) { - // Invoked from clean(), already suspended + DEBUG(log, out).print("Cleaning units...\n"); + bool valid_cuboid = bounds.isValid(); // Allow for dead/inactive units if false int cleaned_units = 0, cleaned_total = 0; for (auto unit : world->units.active) { - if (unit->body.spatters.size()) + TRACE(log, out).print("Considering unit #%d\n", unit->id); + if (valid_cuboid) + { // Check if unit is inside cuboid + if (!Units::isActive(unit)) + continue; // Dead or off map + auto pos = Units::getPosition(unit); + if (!pos.isValid() || !bounds.containsPos(pos)) + continue; + } + TRACE(log, out).print("Selected\n"); + + if (!unit->body.spatters.empty()) { for (size_t j = 0; j < unit->body.spatters.size(); j++) delete unit->body.spatters[j]; cleaned_units++; cleaned_total += unit->body.spatters.size(); unit->body.spatters.clear(); + DEBUG(log, out).print("Cleaned unit #%d\n", unit->id); } } - if (cleaned_total) + if (cleaned_total > 0) out.print("Removed %d contaminants from %d creatures.\n", cleaned_total, cleaned_units); return CR_OK; } -command_result cleanplants (color_ostream &out) -{ - // Invoked from clean(), already suspended - int cleaned_plants = 0, cleaned_total = 0; - for (auto plant : world->plants.all) - { - if (plant->contaminants.size()) - { - for (size_t j = 0; j < plant->contaminants.size(); j++) - delete plant->contaminants[j]; - cleaned_plants++; - cleaned_total += plant->contaminants.size(); - plant->contaminants.clear(); - } - } - if (cleaned_total) - out.print("Removed %d contaminants from %d plants.\n", cleaned_total, cleaned_plants); - return CR_OK; -} - -command_result spotclean (color_ostream &out, vector & parameters) +command_result spotclean(color_ostream &out, vector ¶meters) { - if (cursor->x < 0) - { - out.printerr("The cursor is not active.\n"); - return CR_WRONG_USAGE; - } + DEBUG(log, out).print("Doing spotclean.\n"); if (!Maps::IsValid()) { out.printerr("Map is not available.\n"); return CR_FAILURE; } - df::map_block *block = Maps::getTileBlock(cursor->x, cursor->y, cursor->z); - if (block == NULL) + auto pos = Gui::getCursorPos(); + if (!pos.isValid()) + { + out.printerr("The keyboard cursor is not active.\n"); + return CR_WRONG_USAGE; + } + auto block = Maps::getTileBlock(pos); + if (!block) { out.printerr("Invalid map block selected!\n"); return CR_FAILURE; } + clean_options options; + options.mud = true; + options.snow = true; - for (auto evt : block->block_events) - { - if (evt->getType() != block_square_event_type::material_spatter) - continue; - // type verified - recast to subclass - df::block_square_event_material_spatterst *spatter = (df::block_square_event_material_spatterst *)evt; - clean_mud_safely(spatter, block->map_pos, df::coord(cursor->x % 16, cursor->y % 16, 0)); - } + clean_block(out, block, cuboid(pos), options); return CR_OK; } -command_result clean (color_ostream &out, vector & parameters) +command_result clean(color_ostream &out, vector ¶meters) { - bool map = false; - bool snow = false; - bool mud = false; - bool item_spatter = false; - bool units = false; - bool items = false; - bool plants = false; - for(size_t i = 0; i < parameters.size();i++) + clean_options options; + df::coord pos_1, pos_2; + cuboid bounds; + + if (!Lua::CallLuaModuleFunction(out, "plugins.cleaners", "parse_commandline", + std::make_tuple(&options, &pos_1, &pos_2, parameters))) + { + return CR_WRONG_USAGE; + } + + DEBUG(log, out).print("pos_1 = (%d, %d, %d)\npos_2 = (%d, %d, %d)\n", + pos_1.x, pos_1.y, pos_1.z, pos_2.x, pos_2.y, pos_2.z); + + bool map_target = options.mud || options.snow || options.item_spat || options.grass; + if (!options.map) { - if(parameters[i] == "map") - map = true; - else if(parameters[i] == "units") - units = true; - else if(parameters[i] == "items") - items = true; - else if(parameters[i] == "plants") - plants = true; - else if(parameters[i] == "all") + if (!options.units && !options.items) + { + out.printerr("Choose at least: --map, --units, or --items. Use --all for all.\n"); + return CR_WRONG_USAGE; + } + else if (options.item_spat && !options.items) { - map = true; - items = true; - units = true; - plants = true; + out.printerr("Must use --map (or --all) with --item. Did you mean --items?\n"); + return CR_WRONG_USAGE; } - else if(parameters[i] == "snow") - snow = true; - else if(parameters[i] == "mud") - mud = true; - else if(parameters[i] == "item") - item_spatter = true; - else + else if (map_target || options.only) + { + out.printerr("Must use --map (or --all) with --mud, --snow, --item, --grass, or --only.\n"); return CR_WRONG_USAGE; + } } - if(!map && !units && !items && !plants) + + if (options.desolate && !options.grass) + { + out.printerr("Must use --grass with --desolate. This kills grass!\n"); return CR_WRONG_USAGE; + } + else if (options.only && !map_target) + { + out.printerr("Specified --only for map, but there's nothing else to do.\n"); + return CR_WRONG_USAGE; + } + else if (!Maps::IsValid()) + { + out.printerr("Map not loaded!\n"); + return CR_FAILURE; + } + + if (options.zlevel) + { // Specified z-levels or viewport z + auto z1 = pos_1.isValid() ? pos_1.z : Gui::getViewportPos().z; + auto z2 = pos_2.isValid() ? pos_2.z : z1; + DEBUG(log, out).print("Selecting z-levels %d to %d\n", z1, z2); + bounds.addPos(0, 0, z1); + bounds.addPos(world->map.x_count-1, world->map.y_count-1, z2); + } + else if (pos_1.isValid()) + { // Point or cuboid + DEBUG(log, out).print("Selecting %s.\n", pos_2.isValid() ? "cuboid" : "point"); + bounds.addPos(pos_1); + bounds.addPos(pos_2); // Ignored if invalid + + if (!bounds.clampMap().isValid()) // Clamp to map, check selection + { // No intersection. Don't don't entire map, just fail + out.printerr("Invalid position!\n"); + return CR_FAILURE; + } + } + else + { // Entire map (plus units and items outside map edge) + DEBUG(log, out).print("Selecting entire map.\n"); + } + DEBUG(log, out).print("bounds = (%d:%d, %d:%d, %d:%d)\n", + bounds.x_min, bounds.x_max, bounds.y_min, bounds.y_max, bounds.z_min, bounds.z_max); - if(map) - cleanmap(out,snow,mud,item_spatter); - if(units) - cleanunits(out); - if(items) - cleanitems(out); - if(plants) - cleanplants(out); + if(options.map) + cleanmap(out, bounds, options); + if(options.units) + cleanunits(out, bounds); + if(options.items) + cleanitems(out, bounds); return CR_OK; } -DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) +DFhackCExport command_result plugin_init(color_ostream &out, vector &commands) { commands.push_back(PluginCommand( "clean", - "Remove contaminants from tiles, items, and creatures.", + "Removes contaminants.", clean)); commands.push_back(PluginCommand( "spotclean", - "Clean the map tile under the cursor.", - spotclean,Gui::cursor_hotkey)); + "Remove contaminants from the tile under the cursor.", + spotclean, Gui::cursor_hotkey)); return CR_OK; } -DFhackCExport command_result plugin_shutdown ( color_ostream &out ) +DFhackCExport command_result plugin_shutdown(color_ostream &out) { return CR_OK; } diff --git a/plugins/lua/cleaners.lua b/plugins/lua/cleaners.lua new file mode 100644 index 0000000000..30f6c9cacb --- /dev/null +++ b/plugins/lua/cleaners.lua @@ -0,0 +1,39 @@ +local _ENV = mkmodule('plugins.cleaners') + +local argparse = require('argparse') +local utils = require('utils') + +function parse_commandline(opts, pos_1, pos_2, args) + local positionals = argparse.processArgsGetopt(args, + { + {'a', 'all', handler=function() + opts.map = true + opts.units = true + opts.items = true + opts.plants = true end}, + {'m', 'map', handler=function() opts.map = true end}, + {'d', 'mud', handler=function() opts.mud = true end}, + {'s', 'snow', handler=function() opts.snow = true end}, + {'t', 'item', handler=function() opts.item_spat = true end}, + {'g', 'grass', handler=function() opts.grass = true end}, + {'x', 'desolate', handler=function() opts.desolate = true end}, + {'o', 'only', handler=function() opts.only = true end}, + {'u', 'units', handler=function() opts.units = true end}, + {'i', 'items', handler=function() opts.items = true end}, + {'z', 'zlevel', handler=function() opts.zlevel = true end}, + }) + + if #positionals > 2 then + qerror('Too many positionals!') + end + + if positionals[1] then + utils.assign(pos_1, argparse.coords(positionals[1], 'pos_1', true)) + end + + if positionals[2] then + utils.assign(pos_2, argparse.coords(positionals[2], 'pos_2', true)) + end +end + +return _ENV diff --git a/plugins/plant.cpp b/plugins/plant.cpp index 10046ccb2d..3e3c631896 100644 --- a/plugins/plant.cpp +++ b/plugins/plant.cpp @@ -17,9 +17,6 @@ #include "df/map_block_column.h" #include "df/plant.h" #include "df/plant_raw.h" -#include "df/plant_root_tile.h" -#include "df/plant_tree_info.h" -#include "df/plant_tree_tile.h" #include "df/world.h" using std::string; @@ -251,48 +248,6 @@ command_result df_createplant(color_ostream &out, const df::coord &pos, const pl return CR_OK; } -static bool plant_in_cuboid(const df::plant *plant, const cuboid &bounds) -{ // Will detect tree tiles - if (bounds.containsPos(plant->pos)) - return true; - else if (!plant->tree_info) - return false; - - auto &pos = plant->pos; - auto &t = *(plant->tree_info); - - // Northwest x/y pos of tree bounds - int x_NW = pos.x - (t.dim_x >> 1); - int y_NW = pos.y - (t.dim_y >> 1); - - if (!cuboid(std::max(0, x_NW), std::max(0, y_NW), std::max(0, pos.z - t.roots_depth), - x_NW + t.dim_x, y_NW + t.dim_y, pos.z + t.body_height - 1).clamp(bounds).isValid()) - { // No intersection of tree bounds with cuboid - return false; - } - - int xy_size = t.dim_x * t.dim_y; - // Iterate tree body - for (int z_idx = 0; z_idx < t.body_height; z_idx++) - for (int xy_idx = 0; xy_idx < xy_size; xy_idx++) - if ((t.body[z_idx][xy_idx].whole & 0x7F) != 0 && // Any non-blocked - bounds.containsPos(x_NW + xy_idx % t.dim_x, y_NW + xy_idx / t.dim_x, pos.z + z_idx)) - { - return true; - } - - // Iterate tree roots - for (int z_idx = 0; z_idx < t.roots_depth; z_idx++) - for (int xy_idx = 0; xy_idx < xy_size; xy_idx++) - if ((t.roots[z_idx][xy_idx].whole & 0x7F) != 0 && // Any non-blocked - bounds.containsPos(x_NW + xy_idx % t.dim_x, y_NW + xy_idx / t.dim_x, pos.z - z_idx - 1)) - { - return true; - } - - return false; -} - command_result df_grow(color_ostream &out, const cuboid &bounds, const plant_options &options, vector *filter = nullptr) { if (!bounds.isValid()) @@ -314,7 +269,7 @@ command_result df_grow(color_ostream &out, const cuboid &bounds, const plant_opt { if (ENUM_ATTR(plant_type, is_shrub, plant->type)) continue; // Shrub - else if (!plant_in_cuboid(plant, bounds)) + else if (!Maps::isPlantInBox(plant, bounds)) continue; // Outside cuboid else if (do_filter && (vector_contains(*filter, (int32_t)plant->material) == options.filter_ex)) continue; // Filtered out @@ -461,7 +416,7 @@ command_result df_removeplant(color_ostream &out, const cuboid &bounds, const pl continue; // Not removing saplings } - if (!plant_in_cuboid(&plant, bounds)) + if (!Maps::isPlantInBox(&plant, bounds)) continue; // Outside cuboid else if (do_filter && (vector_contains(*filter, (int32_t)plant.material) == options.filter_ex)) continue; // Filtered out @@ -577,7 +532,7 @@ command_result df_plant(color_ostream &out, vector ¶meters) DEBUG(log, out).print("pos_1 = (%d, %d, %d)\npos_2 = (%d, %d, %d)\n", pos_1.x, pos_1.y, pos_1.z, pos_2.x, pos_2.y, pos_2.z); - if (!Core::getInstance().isMapLoaded()) + if (!Maps::IsValid()) { out.printerr("Map not loaded!\n"); return CR_FAILURE; diff --git a/plugins/regrass.cpp b/plugins/regrass.cpp index d40e1bc6dc..65151b9041 100644 --- a/plugins/regrass.cpp +++ b/plugins/regrass.cpp @@ -458,7 +458,7 @@ command_result df_regrass(color_ostream &out, vector ¶meters) out.printerr("Invalid pos for --block (or used more than one!)\n"); return CR_WRONG_USAGE; } - else if (!Core::getInstance().isMapLoaded()) { + else if (!Maps::IsValid()) { out.printerr("Map not loaded!\n"); return CR_FAILURE; }