diff --git a/docs/Lua API.rst b/docs/Lua API.rst index ca54f3922..a67fe9cf6 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -4043,6 +4043,16 @@ Lua plugin classes - ``shuffle()``: shuffles the sequence of numbers - ``next()``: returns next number in the sequence +dig-now +======= + +The dig-now plugin exposes the following functions to Lua: + +* ``dig_now_tile(pos)`` or ``dig_now_tile(x,y,z)``: Runs dig-now for the + specified tile coordinate. Default options apply, as if you were running the + command ``dig-now ``. See the `dig-now` documentation for details + on default settings. + .. _eventful: eventful diff --git a/docs/Plugins.rst b/docs/Plugins.rst index 4d6316245..eb1ca6f1b 100644 --- a/docs/Plugins.rst +++ b/docs/Plugins.rst @@ -2835,6 +2835,70 @@ Options: :building: Subsequent items will become part of the currently selected building. Good for loading traps; do not use with workshops (or deconstruct to use the item). +.. _dig-now: + +dig-now +======= + +Instantly completes non-marker dig designations, modifying tile shapes and +creating boulders, ores, and gems as if a miner were doing the mining or +engraving. By default, the entire map is processed and boulder generation +follows standard game rules, but the behavior is configurable. + +Note that no units will get mining or engraving experience for the dug/engraved +tiles. + +Trees and roots are not currently handled by this plugin and will be skipped. +Requests for engravings are also skipped since they would depend on the skill +and creative choices of individual engravers. Other types of engraving (i.e. +smoothing and track carving) are handled. + +Usage:: + + dig-now [ []] [] + +Where the optional ```` pair can be used to specify the coordinate bounds +within which ``dig-now`` will operate. If they are not specified, ``dig-now`` +will scan the entire map. If only one ```` is specified, only the tile at +that coordinate is processed. + +Any ```` parameters can either be an ``,,`` triple (e.g. +``35,12,150``) or the string ``here``, which means the position of the active +game cursor should be used. + +Examples: + +``dig-now`` + Dig designated tiles according to standard game rules. + +``dig-now --clean`` + Dig designated tiles, but don't generate any boulders, ores, or gems. + +``dig-now --dump here`` + Dig tiles and dump all generated boulders, ores, and gems at the tile under + the game cursor. + +Options: + +:``-c``, ``--clean``: + Don't generate any boulders, ores, or gems. Equivalent to + ``--percentages 0,0,0,0``. +:``-d``, ``--dump ``: + Dump any generated items at the specified coordinates. If the tile at those + coordinates is open space or is a wall, items will be generated on the + closest walkable tile below. +:``-e``, ``--everywhere``: + Generate a boulder, ore, or gem for every tile that can produce one. + Equivalent to ``--percentages 100,100,100,100``. +:``-h``, ``--help``: + Show quick usage help text. +:``-p``, ``--percentages ,,,``: + Set item generation percentages for each of the tile categories. The + ``vein`` category includes both the large oval clusters and the long stringy + mineral veins. Default is ``25,33,100,100``. +:``-z``, ``--cur-zlevel``: + Restricts the bounds to the currently visible z-level. + .. _diggingInvaders: diggingInvaders diff --git a/docs/changelog.txt b/docs/changelog.txt index 7d9eb9265..fab594d6f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -33,9 +33,15 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: # Future +## New Plugins +- `dig-now`: instantly completes dig designations (including smoothing and carving tracks) + ## Misc Improvements - `tiletypes-here`, `tiletypes-here-point`: add --cursor and --quiet options to support non-interactive use cases +## Documentation +- `quickfort-library-guide`: updated dreamfort documentation and added screenshots + # 0.47.05-r2 ## Fixes diff --git a/docs/guides/quickfort-library-guide.rst b/docs/guides/quickfort-library-guide.rst index 0eb04f73f..d0b308747 100644 --- a/docs/guides/quickfort-library-guide.rst +++ b/docs/guides/quickfort-library-guide.rst @@ -25,20 +25,98 @@ Dreamfort Dreamfort is a fully functional, self-sustaining fortress with defenses, farming, a complete set of workshops, self-managing quantum stockpiles, a grand -dining hall, hospital, jail, fresh water well system, guildhalls, and bedrooms -for hundreds of dwarves. It also comes with manager work orders to automate -basic fort needs, such as food and booze production. It can function by itself -or as the core of a larger, more ambitious fortress. Read the high-level +dining hall, hospital, jail, fresh water well system, guildhalls, noble suites, +and bedrooms for hundreds of dwarves. It also comes with manager work orders to +automate basic fort needs, such as food and booze production. It can function by +itself or as the core of a larger, more ambitious fortress. Read the high-level walkthrough by running ``quickfort run library/dreamfort.csv`` and list the -walkthroughs for the individual levels by running ``quickfort list dreamfort -l --m notes`` or by opening the ``quickfort gui`` dialog, enabling the library -with :kbd:`Alt`:kbd:`l`, and setting the filter to ``dreamfort notes``. +walkthroughs for the individual levels by running ``quickfort list -l dreamfort +-m notes`` or ``quickfort gui -l dreamfort notes``. Dreamfort blueprints are also available for easy viewing and copying `online `__. -The Quick Fortresses -~~~~~~~~~~~~~~~~~~~~ +The online spreadsheets also include `embark profile suggestions +`__ +and a complete `example embark profile +`__. + +Visual overview +``````````````` + +Here are some annotated screenshots of the major levels (or click `here +`__ +for the slideshow interface). + +Surface level +\\\\\\\\\\\\\ + +.. image:: https://drive.google.com/uc?export=download&id=1YL_vQJLB2YnUEFrAg9y3HEdFq3Wpw9WP + :alt: Annotated screenshot of the dreamfort surface level + :target: https://drive.google.com/file/d/1YL_vQJLB2YnUEFrAg9y3HEdFq3Wpw9WP + :align: center + +Farming level +\\\\\\\\\\\\\ + +.. image:: https://drive.google.com/uc?export=download&id=1fBC3G5Y888l4tVe5REAyAd_zeojADVme + :alt: Annotated screenshot of the dreamfort farming level + :target: https://drive.google.com/file/d/1fBC3G5Y888l4tVe5REAyAd_zeojADVme + :align: center + +Industry level +\\\\\\\\\\\\\\ + +.. image:: https://drive.google.com/uc?export=download&id=1emMaHHCaUPcdRbkLQqvr-0ZCs2tdM5X7 + :alt: Annotated screenshot of the dreamfort industry level + :target: https://drive.google.com/file/d/1emMaHHCaUPcdRbkLQqvr-0ZCs2tdM5X7 + +Services level +\\\\\\\\\\\\\\ + +.. image:: https://drive.google.com/uc?export=download&id=13vDIkTVOZGkM84tYf4O5nmRs4VZdE1gh + :alt: Annotated screenshot of the dreamfort services level + :target: https://drive.google.com/file/d/13vDIkTVOZGkM84tYf4O5nmRs4VZdE1gh + :align: center +.. image:: https://drive.google.com/uc?export=download&id=1jlGr6tAhS8i-XFTz8gowTZBhXcfjfL_L + :alt: Annotated screenshot of the dreamfort cistern + :target: https://drive.google.com/file/d/1jlGr6tAhS8i-XFTz8gowTZBhXcfjfL_L + :align: center + +Example plumbing to fill cisterns +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +.. image:: https://drive.google.com/uc?export=download&id=1GvhX_pVDOlmqTi2OujoBqCG_qX36ExAv + :alt: Annotated screenshot of an example aqueduct addition to the dreamfort cistern + :target: https://drive.google.com/file/d/1GvhX_pVDOlmqTi2OujoBqCG_qX36ExAv + :align: center + +Guildhall level +\\\\\\\\\\\\\\\ + +.. image:: https://drive.google.com/uc?export=download&id=17jHiCKeZm6FSS-CI4V0r0GJZh09nzcO_ + :alt: Annotated screenshot of the dreamfort guildhall level + :target: https://drive.google.com/file/d/17jHiCKeZm6FSS-CI4V0r0GJZh09nzcO_ + :align: center + +Noble suites +\\\\\\\\\\\\ + +.. image:: https://drive.google.com/uc?export=download&id=1IBqCf6fF3lw7sHiBE_15Euubysl5AAiS + :alt: Annotated screenshot of the dreamfort noble suites + :target: https://drive.google.com/file/d/1IBqCf6fF3lw7sHiBE_15Euubysl5AAiS + :align: center + +Apartments +\\\\\\\\\\ + +.. image:: https://drive.google.com/uc?export=download&id=1mDQQXG8BnXqasRGFC9R5N6xNALiswEyr + :alt: Annotated screenshot of the dreamfort apartments + :target: https://drive.google.com/file/d/1mDQQXG8BnXqasRGFC9R5N6xNALiswEyr + :align: center + +The Quick Fortress +~~~~~~~~~~~~~~~~~~ The Quick Fortress is an updated version of the example fortress that came with `Python Quickfort 2.0 `__ (the program diff --git a/library/include/modules/Maps.h b/library/include/modules/Maps.h index 50aca8e9d..bc6298601 100644 --- a/library/include/modules/Maps.h +++ b/library/include/modules/Maps.h @@ -260,8 +260,10 @@ extern DFHACK_EXPORT bool GetGlobalFeature(t_feature &feature, int32_t index); * BLOCK DATA */ -/// get size of the map in tiles +/// get size of the map in blocks extern DFHACK_EXPORT void getSize(uint32_t& x, uint32_t& y, uint32_t& z); +/// get size of the map in tiles +extern DFHACK_EXPORT void getTileSize(uint32_t& x, uint32_t& y, uint32_t& z); /// get the position of the map on world map extern DFHACK_EXPORT void getPosition(int32_t& x, int32_t& y, int32_t& z); diff --git a/library/modules/Maps.cpp b/library/modules/Maps.cpp index b4289ddf6..da1a775f3 100644 --- a/library/modules/Maps.cpp +++ b/library/modules/Maps.cpp @@ -106,7 +106,7 @@ bool Maps::IsValid () return (world->map.block_index != NULL); } -// getter for map size +// getter for map size in blocks void Maps::getSize (uint32_t& x, uint32_t& y, uint32_t& z) { if (!IsValid()) @@ -119,6 +119,14 @@ void Maps::getSize (uint32_t& x, uint32_t& y, uint32_t& z) z = world->map.z_count_block; } +// getter for map size in tiles +void Maps::getTileSize (uint32_t& x, uint32_t& y, uint32_t& z) +{ + getSize(x, y, z); + x *= 16; + y *= 16; +} + // getter for map position void Maps::getPosition (int32_t& x, int32_t& y, int32_t& z) { diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 92e2e9dbd..25ea7d150 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -111,6 +111,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(deramp deramp.cpp) dfhack_plugin(debug debug.cpp LINK_LIBRARIES jsoncpp_lib_static) dfhack_plugin(dig dig.cpp) + dfhack_plugin(dig-now dig-now.cpp LINK_LIBRARIES lua) dfhack_plugin(digFlood digFlood.cpp) add_subdirectory(diggingInvaders) dfhack_plugin(dwarfvet dwarfvet.cpp) diff --git a/plugins/dig-now.cpp b/plugins/dig-now.cpp new file mode 100644 index 000000000..0abf935a4 --- /dev/null +++ b/plugins/dig-now.cpp @@ -0,0 +1,933 @@ +/* + * Simulates completion of dig designations. + */ + +#include "DataFuncs.h" +#include "PluginManager.h" +#include "TileTypes.h" +#include "LuaTools.h" + +#include "modules/Buildings.h" +#include "modules/Gui.h" +#include "modules/Maps.h" +#include "modules/MapCache.h" +#include "modules/Random.h" +#include "modules/Units.h" +#include "modules/World.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +DFHACK_PLUGIN("dig-now"); +REQUIRE_GLOBAL(ui); +REQUIRE_GLOBAL(world); + +using namespace DFHack; + +struct boulder_percent_options { + // percent chance ([0..100]) for creating a boulder for the given rock type + uint32_t layer; + uint32_t vein; + uint32_t small_cluster; + uint32_t deep; + + // defaults from + // https://dwarffortresswiki.org/index.php/DF2014:Mining + boulder_percent_options() : + layer(25), vein(33), small_cluster(100), deep(100) { } + + static struct_identity _identity; +}; +static const struct_field_info boulder_percent_options_fields[] = { + { struct_field_info::PRIMITIVE, "layer", offsetof(boulder_percent_options, layer), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "vein", offsetof(boulder_percent_options, vein), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "small_cluster", offsetof(boulder_percent_options, small_cluster), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "deep", offsetof(boulder_percent_options, deep), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::END } +}; +struct_identity boulder_percent_options::_identity(sizeof(boulder_percent_options), &df::allocator_fn, NULL, "boulder_percents", NULL, boulder_percent_options_fields); + +struct dig_now_options { + bool help; // whether to show the short help + + DFCoord start; // upper-left coordinate, min z-level + DFCoord end; // lower-right coordinate, max z-level + + boulder_percent_options boulder_percents; + + // if set to the pos of a walkable tile (or somewhere above such a tile), + // will dump generated boulders at this position instead of at their dig + // locations + DFCoord dump_pos; + + static DFCoord getMapSize() { + uint32_t endx, endy, endz; + Maps::getTileSize(endx, endy, endz); + return DFCoord(endx - 1, endy - 1, endz - 1); + } + + dig_now_options() : help(false), start(0, 0, 0), end(getMapSize()) { } + + static struct_identity _identity; +}; +static const struct_field_info dig_now_options_fields[] = { + { struct_field_info::PRIMITIVE, "help", offsetof(dig_now_options, help), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::SUBSTRUCT, "start", offsetof(dig_now_options, start), &df::coord::_identity, 0, 0 }, + { struct_field_info::SUBSTRUCT, "end", offsetof(dig_now_options, end), &df::coord::_identity, 0, 0 }, + { struct_field_info::SUBSTRUCT, "boulder_percents", offsetof(dig_now_options, boulder_percents), &boulder_percent_options::_identity, 0, 0 }, + { struct_field_info::SUBSTRUCT, "dump_pos", offsetof(dig_now_options, dump_pos), &df::coord::_identity, 0, 0 }, + { struct_field_info::END } +}; +struct_identity dig_now_options::_identity(sizeof(dig_now_options), &df::allocator_fn, NULL, "dig_now_options", NULL, dig_now_options_fields); + +// propagate light, outside, and subterranean flags to open tiles below this one +static void propagate_vertical_flags(MapExtras::MapCache &map, + const DFCoord &pos) { + df::tile_designation td = map.designationAt(pos); + + if (!map.ensureBlockAt(DFCoord(pos.x, pos.y, pos.z+1))) { + // only the sky above + td.bits.light = true; + td.bits.outside = true; + td.bits.subterranean = false; + } + + int32_t zlevel = pos.z; + df::tiletype_shape shape = + tileShape(map.tiletypeAt(DFCoord(pos.x, pos.y, zlevel))); + while ((shape == df::tiletype_shape::EMPTY + || shape == df::tiletype_shape::RAMP_TOP) + && map.ensureBlockAt(DFCoord(pos.x, pos.y, --zlevel))) { + DFCoord pos_below(pos.x, pos.y, zlevel); + df::tile_designation td_below = map.designationAt(pos_below); + if (td_below.bits.light == td.bits.light + && td_below.bits.outside == td.bits.outside + && td_below.bits.subterranean == td.bits.subterranean) + break; + td_below.bits.light = td.bits.light; + td_below.bits.outside = td.bits.outside; + td_below.bits.subterranean = td.bits.subterranean; + map.setDesignationAt(pos_below, td_below); + shape = tileShape(map.tiletypeAt(pos_below)); + } +} + +static bool can_dig_default(df::tiletype tt) { + df::tiletype_shape shape = tileShape(tt); + return shape == df::tiletype_shape::WALL || + shape == df::tiletype_shape::FORTIFICATION || + shape == df::tiletype_shape::RAMP || + shape == df::tiletype_shape::STAIR_UP || + shape == df::tiletype_shape::STAIR_UPDOWN; +} + +static bool can_dig_channel(df::tiletype tt) { + df::tiletype_shape shape = tileShape(tt); + return shape != df::tiletype_shape::EMPTY && + shape != df::tiletype_shape::ENDLESS_PIT && + shape != df::tiletype_shape::NONE && + shape != df::tiletype_shape::RAMP_TOP && + shape != df::tiletype_shape::TRUNK_BRANCH; +} + +static bool can_dig_up_stair(df::tiletype tt) { + df::tiletype_shape shape = tileShape(tt); + return shape == df::tiletype_shape::WALL || + shape == df::tiletype_shape::FORTIFICATION; +} + +static bool can_dig_down_stair(df::tiletype tt) { + df::tiletype_shape shape = tileShape(tt); + return shape == df::tiletype_shape::BOULDER || + shape == df::tiletype_shape::BROOK_BED || + shape == df::tiletype_shape::BROOK_TOP || + shape == df::tiletype_shape::FLOOR || + shape == df::tiletype_shape::FORTIFICATION || + shape == df::tiletype_shape::PEBBLES || + shape == df::tiletype_shape::RAMP || + shape == df::tiletype_shape::SAPLING || + shape == df::tiletype_shape::SHRUB || + shape == df::tiletype_shape::TWIG || + shape == df::tiletype_shape::WALL; +} + +static bool can_dig_up_down_stair(df::tiletype tt) { + df::tiletype_shape shape = tileShape(tt); + return shape == df::tiletype_shape::WALL || + shape == df::tiletype_shape::FORTIFICATION || + shape == df::tiletype_shape::STAIR_UP; +} + +static bool can_dig_ramp(df::tiletype tt) { + df::tiletype_shape shape = tileShape(tt); + return shape == df::tiletype_shape::WALL || + shape == df::tiletype_shape::FORTIFICATION; +} + +static void dig_type(MapExtras::MapCache &map, const DFCoord &pos, + df::tiletype tt) { + auto blk = map.BlockAtTile(pos); + if (!blk) + return; + + map.setTiletypeAt(pos, tt); + + // digging a tile should revert it to the layer soil/stone material + if (!blk->setStoneAt(pos, tt, map.layerMaterialAt(pos))) + blk->setSoilAt(pos, tt, map.layerMaterialAt(pos)); +} + +static df::tiletype get_target_type(df::tiletype tt, df::tiletype_shape shape) { + tt = findSimilarTileType(tt, shape); + + // un-smooth dug tiles + tt = findTileType(tileShape(tt), tileMaterial(tt), tileVariant(tt), + df::tiletype_special::NORMAL, tileDirection(tt)); + + return findRandomVariant(tt); +} + +static void dig_shape(MapExtras::MapCache &map, const DFCoord &pos, + df::tiletype tt, df::tiletype_shape shape) { + dig_type(map, pos, get_target_type(tt, shape)); +} + +static void remove_ramp_top(MapExtras::MapCache &map, const DFCoord &pos) { + if (!map.ensureBlockAt(pos)) + return; + + if (tileShape(map.tiletypeAt(pos)) == df::tiletype_shape::RAMP_TOP) + dig_type(map, pos, df::tiletype::OpenSpace); +} + +static bool is_wall(MapExtras::MapCache &map, const DFCoord &pos) { + if (!map.ensureBlockAt(pos)) + return false; + return tileShape(map.tiletypeAt(pos)) == df::tiletype_shape::WALL; +} + +static void clean_ramp(MapExtras::MapCache &map, const DFCoord &pos) { + if (!map.ensureBlockAt(pos)) + return; + + df::tiletype tt = map.tiletypeAt(pos); + if (tileShape(tt) != df::tiletype_shape::RAMP) + return; + + if (is_wall(map, DFCoord(pos.x-1, pos.y, pos.z)) || + is_wall(map, DFCoord(pos.x+1, pos.y, pos.z)) || + is_wall(map, DFCoord(pos.x, pos.y-1, pos.z)) || + is_wall(map, DFCoord(pos.x, pos.y+1, pos.z))) + return; + + remove_ramp_top(map, DFCoord(pos.x, pos.y, pos.z+1)); + dig_shape(map,pos, tt, df::tiletype_shape::FLOOR); +} + +// removes self and/or orthogonally adjacent ramps that are no longer adjacent +// to a wall +static void clean_ramps(MapExtras::MapCache &map, const DFCoord &pos) { + clean_ramp(map, pos); + clean_ramp(map, DFCoord(pos.x-1, pos.y, pos.z)); + clean_ramp(map, DFCoord(pos.x+1, pos.y, pos.z)); + clean_ramp(map, DFCoord(pos.x, pos.y-1, pos.z)); + clean_ramp(map, DFCoord(pos.x, pos.y+1, pos.z)); +} + +// destroys any colonies located at pos +static void destroy_colony(const DFCoord &pos) { + auto same_pos = [&](df::vermin *colony){ return colony->pos == pos; }; + + auto &colonies = world->vermin.colonies; + auto found_colony = std::find_if(begin(colonies), end(colonies), same_pos); + if (found_colony == end(colonies)) + return; + colonies.erase(found_colony); + + auto &all_vermin = world->vermin.all; + all_vermin.erase( + std::find_if(begin(all_vermin), end(all_vermin), same_pos)); +} + +struct dug_tile_info { + DFCoord pos; + df::tiletype_material tmat; + df::item_type itype; + int32_t imat; // mat idx of boulder/gem potentially generated at this pos + + dug_tile_info(MapExtras::MapCache &map, const DFCoord &pos) { + this->pos = pos; + + df::tiletype tt = map.tiletypeAt(pos); + tmat = tileMaterial(tt); + + switch (map.BlockAtTile(pos)->veinTypeAt(pos)) { + case df::inclusion_type::CLUSTER_ONE: + case df::inclusion_type::CLUSTER_SMALL: + itype = df::item_type::ROUGH; + break; + default: + itype = df::item_type::BOULDER; + } + + imat = -1; + if (tileShape(tt) == df::tiletype_shape::WALL + && (tmat == df::tiletype_material::STONE + || tmat == df::tiletype_material::MINERAL + || tmat == df::tiletype_material::FEATURE)) + imat = map.baseMaterialAt(pos).mat_index; + } +}; + +static bool is_diggable(MapExtras::MapCache &map, const DFCoord &pos, + df::tiletype tt) { + df::tiletype_material mat = tileMaterial(tt); + switch (mat) { + case df::tiletype_material::CONSTRUCTION: + case df::tiletype_material::POOL: + case df::tiletype_material::RIVER: + case df::tiletype_material::TREE: + case df::tiletype_material::ROOT: + case df::tiletype_material::LAVA_STONE: + case df::tiletype_material::MAGMA: + case df::tiletype_material::HFS: + case df::tiletype_material::UNDERWORLD_GATE: + return false; + default: + break; + } + + if (mat == df::tiletype_material::FEATURE) { + // adamantine is the only is diggable feature + t_feature feature; + return map.BlockAtTile(pos)->GetLocalFeature(&feature) + && feature.type == feature_type::deep_special_tube; + } + + return true; +} + +static bool dig_tile(color_ostream &out, MapExtras::MapCache &map, + const DFCoord &pos, df::tile_dig_designation designation, + std::vector &dug_tiles) { + df::tiletype tt = map.tiletypeAt(pos); + + if (!is_diggable(map, pos, tt)) + return false; + + df::tiletype target_type = df::tiletype::Void; + switch(designation) { + case df::tile_dig_designation::Default: + if (can_dig_default(tt)) { + df::tiletype_shape shape = tileShape(tt); + df::tiletype_shape target_shape = df::tiletype_shape::FLOOR; + if (shape == df::tiletype_shape::STAIR_UPDOWN) + target_shape = df::tiletype_shape::STAIR_DOWN; + else if (shape == df::tiletype_shape::RAMP) + remove_ramp_top(map, DFCoord(pos.x, pos.y, pos.z+1)); + target_type = get_target_type(tt, target_shape); + } + break; + case df::tile_dig_designation::Channel: + { + DFCoord pos_below(pos.x, pos.y, pos.z-1); + if (can_dig_channel(tt) && map.ensureBlockAt(pos_below) + && is_diggable(map, pos_below, map.tiletypeAt(pos_below))) { + target_type = df::tiletype::OpenSpace; + DFCoord pos_above(pos.x, pos.y, pos.z+1); + if (map.ensureBlockAt(pos_above)) + remove_ramp_top(map, pos_above); + if (dig_tile(out, map, pos_below, + df::tile_dig_designation::Ramp, dug_tiles)) { + clean_ramps(map, pos_below); + // if we successfully dug out the ramp below, that took care + // of adding the ramp top here + return true; + } + } + break; + } + case df::tile_dig_designation::UpStair: + if (can_dig_up_stair(tt)) + target_type = get_target_type(tt, df::tiletype_shape::STAIR_UP); + break; + case df::tile_dig_designation::DownStair: + if (can_dig_down_stair(tt)) { + target_type = + get_target_type(tt, df::tiletype_shape::STAIR_DOWN); + + } + break; + case df::tile_dig_designation::UpDownStair: + if (can_dig_up_down_stair(tt)) { + target_type = + get_target_type(tt, df::tiletype_shape::STAIR_UPDOWN); + } + break; + case df::tile_dig_designation::Ramp: + { + if (can_dig_ramp(tt)) { + target_type = get_target_type(tt, df::tiletype_shape::RAMP); + DFCoord pos_above(pos.x, pos.y, pos.z+1); + if (target_type != tt && map.ensureBlockAt(pos_above) + && is_diggable(map, pos, map.tiletypeAt(pos_above))) { + // only capture the tile info of pos_above if we didn't get + // here via the Channel case above + if (dug_tiles.size() == 0) + dug_tiles.push_back(dug_tile_info(map, pos_above)); + destroy_colony(pos_above); + // set tile type directly instead of calling dig_shape + // because we need to use *this* tile's material, not the + // material of the tile above + map.setTiletypeAt(pos_above, + get_target_type(tt, df::tiletype_shape::RAMP_TOP)); + remove_ramp_top(map, DFCoord(pos.x, pos.y, pos.z+2)); + } + } + break; + } + case df::tile_dig_designation::No: + default: + out.printerr( + "unhandled dig designation for tile (%d, %d, %d): %d\n", + pos.x, pos.y, pos.z, designation); + } + + // fail if unhandled or no change to tile + if (target_type == df::tiletype::Void || target_type == tt) + return false; + + dug_tiles.push_back(dug_tile_info(map, pos)); + dig_type(map, pos, target_type); + + // let light filter down to newly exposed tiles + propagate_vertical_flags(map, pos); + + return true; +} + +static bool is_smooth_wall(MapExtras::MapCache &map, const DFCoord &pos) { + if (!map.ensureBlockAt(pos)) + return false; + df::tiletype tt = map.tiletypeAt(pos); + return tileSpecial(tt) == df::tiletype_special::SMOOTH + && tileShape(tt) == df::tiletype_shape::WALL; +} + +static bool is_smooth_wall_or_door(MapExtras::MapCache &map, + const DFCoord &pos) { + if (is_smooth_wall(map, pos)) + return true; + + df::building *bld = Buildings::findAtTile(pos); + return bld && bld->getType() == df::building_type::Door; +} + +// adds adjacent smooth walls and doors to the given tdir +static TileDirection get_adjacent_smooth_walls(MapExtras::MapCache &map, + const DFCoord &pos, + TileDirection tdir) { + if (is_smooth_wall_or_door(map, DFCoord(pos.x, pos.y-1, pos.z))) + tdir.north = 1; + if (is_smooth_wall_or_door(map, DFCoord(pos.x, pos.y+1, pos.z))) + tdir.south = 1; + if (is_smooth_wall_or_door(map, DFCoord(pos.x-1, pos.y, pos.z))) + tdir.west = 1; + if (is_smooth_wall_or_door(map, DFCoord(pos.x+1, pos.y, pos.z))) + tdir.east = 1; + return tdir; +} + +// ensure we have at least two directions enabled (or 0) so we can find a +// matching tiletype. The game chooses to curve "end piece" walls into +// orthogonally adjacent hidden tiles, or uses a pillar if there are no such +// tiles. we take the easier, but not quite conformant, path here and always use +// a pillar for end pieces. If we want to become faithful to how the game does +// it, this code should be moved to the post-processing phase after hidden tiles +// have been revealed. We would also have to scan for wall ends that are no +// longer adjacent to hidden tiles and convert them to pillars when we dig two +// tiles away from such a wall end and reveal their adjacent hidden tile. +static TileDirection ensure_valid_tdir(TileDirection tdir) { + if (tdir.sum() < 2) + tdir.whole = 0; + return tdir; +} + +// connects adjacent smooth walls to our new smooth wall +static TileDirection BLANK_TILE_DIRECTION; +static bool adjust_smooth_wall_dir(MapExtras::MapCache &map, + const DFCoord &pos, + TileDirection tdir = BLANK_TILE_DIRECTION) { + if (!is_smooth_wall(map, pos)) + return false; + + tdir = ensure_valid_tdir(get_adjacent_smooth_walls(map, pos, tdir)); + + df::tiletype tt = map.tiletypeAt(pos); + tt = findTileType(tileShape(tt), tileMaterial(tt), tileVariant(tt), + tileSpecial(tt), tdir); + if (tt == df::tiletype::Void) + return false; + + map.setTiletypeAt(pos, tt); + return true; +} + +static void refresh_adjacent_smooth_walls(MapExtras::MapCache &map, + const DFCoord &pos) { + adjust_smooth_wall_dir(map, DFCoord(pos.x, pos.y-1, pos.z)); + adjust_smooth_wall_dir(map, DFCoord(pos.x, pos.y+1, pos.z)); + adjust_smooth_wall_dir(map, DFCoord(pos.x-1, pos.y, pos.z)); + adjust_smooth_wall_dir(map, DFCoord(pos.x+1, pos.y, pos.z)); +} + +// assumes that if the game let you designate a tile for smoothing, it must be +// valid to do so. +static bool smooth_tile(color_ostream &out, MapExtras::MapCache &map, + const DFCoord &pos) { + df::tiletype tt = map.tiletypeAt(pos); + + TileDirection tdir; + if (tileShape(tt) == df::tiletype_shape::WALL) { + if (adjust_smooth_wall_dir(map, DFCoord(pos.x, pos.y-1, pos.z), + TileDirection(0, 1, 0, 0))) + tdir.north = 1; + if (adjust_smooth_wall_dir(map, DFCoord(pos.x, pos.y+1, pos.z), + TileDirection(1, 0, 0, 0))) + tdir.south = 1; + if (adjust_smooth_wall_dir(map, DFCoord(pos.x-1, pos.y, pos.z), + TileDirection(0, 0, 0, 1))) + tdir.west = 1; + if (adjust_smooth_wall_dir(map, DFCoord(pos.x+1, pos.y, pos.z), + TileDirection(0, 0, 1, 0))) + tdir.east = 1; + tdir = ensure_valid_tdir(tdir); + } + + tt = findTileType(tileShape(tt), tileMaterial(tt), tileVariant(tt), + df::tiletype_special::SMOOTH, tdir); + if (tt == df::tiletype::Void) + return false; + + map.setTiletypeAt(pos, tt); + return true; +} + +// assumes that if the game let you designate a tile for track carving, it must +// be valid to do so. +static bool carve_tile(MapExtras::MapCache &map, + const DFCoord &pos, df::tile_occupancy &to) { + df::tiletype tt = map.tiletypeAt(pos); + TileDirection tdir = tileDirection(tt); + + if (to.bits.carve_track_north) + tdir.north = 1; + if (to.bits.carve_track_east) + tdir.east = 1; + if (to.bits.carve_track_south) + tdir.south = 1; + if (to.bits.carve_track_west) + tdir.west = 1; + + tt = findTileType(tileShape(tt), tileMaterial(tt), tileVariant(tt), + df::tiletype_special::TRACK, tdir); + if (tt == df::tiletype::Void) + return false; + + map.setTiletypeAt(pos, tt); + return true; +} + +static bool produces_item(const boulder_percent_options &options, + MapExtras::MapCache &map, Random::MersenneRNG &rng, + const dug_tile_info &info) { + uint32_t probability; + if (info.tmat == df::tiletype_material::FEATURE) + probability = options.deep; + else { + switch (map.BlockAtTile(info.pos)->veinTypeAt(info.pos)) { + case df::inclusion_type::CLUSTER: + case df::inclusion_type::VEIN: + probability = options.vein; + break; + case df::inclusion_type::CLUSTER_ONE: + case df::inclusion_type::CLUSTER_SMALL: + probability = options.small_cluster; + break; + default: + probability = options.layer; + break; + } + } + + return rng.random(100) < probability; +} + +typedef std::map, std::vector> + item_coords_t; + +static void do_dig(color_ostream &out, std::vector &dug_coords, + item_coords_t &item_coords, const dig_now_options &options) { + MapExtras::MapCache map; + Random::MersenneRNG rng; + + rng.init(); + + // go down levels instead of up so stacked ramps behave as expected + for (int16_t z = options.end.z; z >= options.start.z; --z) { + for (int16_t y = options.start.y; y <= options.end.y; ++y) { + for (int16_t x = options.start.x; x <= options.end.x; ++x) { + // this will return NULL if the map block hasn't been allocated + // yet, but that means there aren't any designations anyway. + if (!Maps::getTileBlock(x, y, z)) + continue; + + DFCoord pos(x, y, z); + df::tile_designation td = map.designationAt(pos); + df::tile_occupancy to = map.occupancyAt(pos); + if (td.bits.dig != df::tile_dig_designation::No && + !to.bits.dig_marked) { + std::vector dug_tiles; + if (dig_tile(out, map, pos, td.bits.dig, dug_tiles)) { + td = map.designationAt(pos); + td.bits.dig = df::tile_dig_designation::No; + map.setDesignationAt(pos, td); + for (auto info : dug_tiles) { + dug_coords.push_back(info.pos); + refresh_adjacent_smooth_walls(map, info.pos); + if (info.imat < 0) + continue; + if (produces_item(options.boulder_percents, + map, rng, info)) { + auto k = std::make_pair(info.itype, info.imat); + item_coords[k].push_back(info.pos); + } + } + } + } else if (td.bits.smooth == 1) { + if (smooth_tile(out, map, pos)) { + to = map.occupancyAt(pos); + td.bits.smooth = 0; + map.setDesignationAt(pos, td); + } + } else if (to.bits.carve_track_north == 1 + || to.bits.carve_track_east == 1 + || to.bits.carve_track_south == 1 + || to.bits.carve_track_west == 1) { + if (carve_tile(map, pos, to)) { + to = map.occupancyAt(pos); + to.bits.carve_track_north = 0; + to.bits.carve_track_east = 0; + to.bits.carve_track_south = 0; + to.bits.carve_track_west = 0; + map.setOccupancyAt(pos, to); + } + } + } + } + } + + map.WriteAll(); +} + +// if pos is empty space, teleport to a floor somewhere below +// if we fall out of the world (e.g. empty space or walls all the way down), +// returned position will be invalid +static DFCoord simulate_fall(const DFCoord &pos) { + DFCoord resting_pos(pos); + + while (Maps::ensureTileBlock(resting_pos)) { + df::tiletype tt = *Maps::getTileType(resting_pos); + df::tiletype_shape_basic basic_shape = tileShapeBasic(tileShape(tt)); + if (isWalkable(tt) && basic_shape != df::tiletype_shape_basic::Open) + break; + --resting_pos.z; + } + + return resting_pos; +} + +static void create_boulders(color_ostream &out, + const item_coords_t &item_coords, + const dig_now_options &options) { + df::unit *unit = world->units.active[0]; + df::historical_entity *civ = df::historical_entity::find(unit->civ_id); + df::world_site *site = World::isFortressMode() ? + df::world_site::find(ui->site_id) : NULL; + + std::vector in_reag; + std::vector in_items; + + DFCoord dump_pos; + if (Maps::isValidTilePos(options.dump_pos)) { + dump_pos = simulate_fall(options.dump_pos); + if (!Maps::ensureTileBlock(dump_pos)) + out.printerr("Invalid dump tile coordinates! Ensure the --dump" + " option specifies an open, non-wall tile."); + } + + for (auto entry : item_coords) { + df::reaction_product_itemst *prod = + df::allocate(); + const std::vector &coords = entry.second; + + prod->item_type = entry.first.first; + prod->item_subtype = -1; + prod->mat_type = 0; + prod->mat_index = entry.first.second; + prod->probability = 100; + prod->product_dimension = 1; + + std::vector out_products; + std::vector out_items; + + size_t remaining_items = coords.size(); + while (remaining_items > 0) { + int16_t batch_size = min(remaining_items, + static_cast(INT16_MAX)); + prod->count = batch_size; + remaining_items -= batch_size; + prod->produce(unit, &out_products, &out_items, &in_reag, &in_items, + 1, job_skill::NONE, 0, civ, site, NULL); + } + + size_t num_items = out_items.size(); + if (num_items != coords.size()) { + MaterialInfo material; + material.decode(prod->mat_type, prod->mat_index); + out.printerr("unexpected number of %s %s produced: expected %zd," + " got %zd.\n", + material.toString().c_str(), + ENUM_KEY_STR(item_type, prod->item_type).c_str(), + coords.size(), num_items); + num_items = min(num_items, entry.second.size()); + } + + for (size_t i = 0; i < num_items; ++i) { + DFCoord pos = Maps::isValidTilePos(dump_pos) ? + dump_pos : simulate_fall(coords[i]); + if (!Maps::ensureTileBlock(pos)) { + out.printerr( + "unable to place boulder generated at (%d, %d, %d)\n", + coords[i].x, coords[i].y, coords[i].z); + continue; + } + out_items[i]->moveToGround(pos.x, pos.y, pos.z); + } + + delete(prod); + } +} + +static void flood_unhide(color_ostream &out, const DFCoord &pos) { + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); + + if (!lua_checkstack(L, 2) + || !Lua::PushModulePublic(out, L, "plugins.reveal", "unhideFlood")) + return; + + Lua::Push(L, pos); + Lua::SafeCall(out, L, 1, 0); +} + +static bool needs_unhide(const DFCoord &pos) { + return !Maps::ensureTileBlock(pos) + || Maps::getTileDesignation(pos)->bits.hidden; +} + +static bool needs_flood_unhide(const DFCoord &pos) { + return needs_unhide(pos) + || needs_unhide(DFCoord(pos.x-1, pos.y-1, pos.z)) + || needs_unhide(DFCoord(pos.x, pos.y-1, pos.z)) + || needs_unhide(DFCoord(pos.x+1, pos.y-1, pos.z)) + || needs_unhide(DFCoord(pos.x-1, pos.y, pos.z)) + || needs_unhide(DFCoord(pos.x+1, pos.y, pos.z)) + || needs_unhide(DFCoord(pos.x-1, pos.y+1, pos.z)) + || needs_unhide(DFCoord(pos.x, pos.y+1, pos.z)) + || needs_unhide(DFCoord(pos.x+1, pos.y+1, pos.z)); +} + +static void post_process_dug_tiles(color_ostream &out, + const std::vector &dug_coords) { + for (DFCoord pos : dug_coords) { + if (needs_flood_unhide(pos)) { + // set current tile to hidden to allow flood_unhide to work on tiles + // that were already visible but that reveal hidden tiles when dug. + Maps::getTileDesignation(pos)->bits.hidden = true; + flood_unhide(out, pos); + } + + df::tile_occupancy &to = *Maps::getTileOccupancy(pos); + if (to.bits.unit || to.bits.item) { + DFCoord resting_pos = simulate_fall(pos); + if (resting_pos == pos) + continue; + + if (!Maps::ensureTileBlock(resting_pos)) { + out.printerr("No valid tile beneath (%d, %d, %d); can't move" + " units and items to floor", + pos.x, pos.y, pos.z); + continue; + } + + if (to.bits.unit) { + std::vector units; + Units::getUnitsInBox(units, pos.x, pos.y, pos.z, + pos.x, pos.y, pos.z); + for (auto unit : units) + Units::teleport(unit, resting_pos); + } + + if (to.bits.item) { + for (auto item : world->items.other.IN_PLAY) { + if (item->pos == pos && item->flags.bits.on_ground) + item->moveToGround( + resting_pos.x, resting_pos.y, resting_pos.z); + } + } + } + + // refresh block metadata and flows + Maps::enableBlockUpdates(Maps::getTileBlock(pos), true, true); + } +} + +static bool get_options(color_ostream &out, + dig_now_options &opts, + const std::vector ¶meters) { + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); + + if (!lua_checkstack(L, parameters.size() + 2) || + !Lua::PushModulePublic( + out, L, "plugins.dig-now", "parse_commandline")) { + out.printerr("Failed to load dig-now Lua code\n"); + return false; + } + + Lua::Push(L, &opts); + + for (const std::string ¶m : parameters) + Lua::Push(L, param); + + if (!Lua::SafeCall(out, L, parameters.size() + 1, 0)) + return false; + + return true; +} + +static void print_help(color_ostream &out) { + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); + + if (!lua_checkstack(L, 1) || + !Lua::PushModulePublic(out, L, "plugins.dig-now", "print_help") || + !Lua::SafeCall(out, L, 0, 0)) { + out.printerr("Failed to load dig-now Lua code\n"); + } +} + +bool dig_now_impl(color_ostream &out, const dig_now_options &options) { + if (!Maps::IsValid()) { + out.printerr("Map is not available!\n"); + return false; + } + + // required for boulder generation + if (world->units.active.size() == 0) { + out.printerr("At least one unit must be alive!\n"); + return false; + } + + // track which positions were modified and where to produce items + std::vector dug_coords; + item_coords_t item_coords; + + do_dig(out, dug_coords, item_coords, options); + create_boulders(out, item_coords, options); + post_process_dug_tiles(out, dug_coords); + + // force the game to recompute its walkability cache + world->reindex_pathfinding = true; + + return true; +} + +command_result dig_now(color_ostream &out, std::vector ¶ms) { + CoreSuspender suspend; + + dig_now_options options; + if (!get_options(out, options, params) || options.help) + { + print_help(out); + return options.help ? CR_OK : CR_FAILURE; + } + + return dig_now_impl(out, options) ? CR_OK : CR_FAILURE; +} + +DFhackCExport command_result plugin_init(color_ostream &, + std::vector &commands) { + commands.push_back(PluginCommand( + "dig-now", "Instantly complete dig designations", dig_now, false)); + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown(color_ostream &) { + return CR_OK; +} + +// Lua API + +// runs dig-now for the specified tile coordinate. default options apply. +static int dig_now_tile(lua_State *L) +{ + DFCoord pos; + if (lua_gettop(L) <= 1) + Lua::CheckDFAssign(L, &pos, 1); + else + pos = DFCoord(luaL_checkint(L, 1), luaL_checkint(L, 2), + luaL_checkint(L, 3)); + + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + + dig_now_options options; + options.start = pos; + options.end = pos; + lua_pushboolean(L, dig_now_impl(*out, options)); + + return 1; +} + +static int link_adjacent_smooth_walls(lua_State *L) +{ + DFCoord pos; + if (lua_gettop(L) <= 1) + Lua::CheckDFAssign(L, &pos, 1); + else + pos = DFCoord(luaL_checkint(L, 1), luaL_checkint(L, 2), + luaL_checkint(L, 3)); + + MapExtras::MapCache map; + adjust_smooth_wall_dir(map, pos); + refresh_adjacent_smooth_walls(map, pos); + map.WriteAll(); + return 0; +} + +DFHACK_PLUGIN_LUA_COMMANDS { + DFHACK_LUA_COMMAND(dig_now_tile), + DFHACK_LUA_COMMAND(link_adjacent_smooth_walls), + DFHACK_LUA_END +}; diff --git a/plugins/lua/dig-now.lua b/plugins/lua/dig-now.lua new file mode 100644 index 000000000..2d7ae40d7 --- /dev/null +++ b/plugins/lua/dig-now.lua @@ -0,0 +1,101 @@ +local _ENV = mkmodule('plugins.dig-now') + +local argparse = require('argparse') +local guidm = require('gui.dwarfmode') +local utils = require('utils') + +local short_help_text = [=[ + +dig-now +======= + +Instantly completes dig designations, modifying map tiles and creating boulders, +ores, and gems as if a miner were doing the mining or engraving. By default, all +dig designations on the map are completed and boulder generation follows +standard game rules, but the behavior is configurable. + +Usage: + + dig-now [ []] [] + +Examples: + +dig-now + Dig all designated tiles according to standard game rules. + +dig-now --clean + Dig designated tiles, but don't generate any boulders, ores, or gems. + +dig-now --dump here + Dig tiles and dump all generated boulders, ores, and gems at the tile under + the game cursor. + +See the online DFHack documentation for details on all options. +]=] + +function print_help() print(short_help_text) end + +local function parse_coords(opts, configname, arg) + local cursor = argparse.coords(arg, configname) + utils.assign(opts[configname], cursor) +end + +local function parse_percentages(opts, arg) + local nums = argparse.numberList(arg, 'percentages', 4) + for _,percentage in ipairs(nums) do + if percentage < 0 or percentage > 100 then + qerror(('invalid percentages: "%s"; expected format is ",' .. + ',,", where each number is'.. + ' between 0 and 100, inclusive (e.g. "0,33,100,100")') + :format(arg)) + end + end + local config = opts.boulder_percents + config.layer, config.vein, config.small_cluster, config.deep = + nums[1], nums[2], nums[3], nums[4] +end + +local function min_to_max(...) + local args = {...} + table.sort(args, function(a, b) return a < b end) + return table.unpack(args) +end + +function parse_commandline(opts, ...) + local use_zlevel = false + local positionals = argparse.processArgsGetopt({...}, { + {'c', 'clean', + handler=function() parse_percentages(opts, '0,0,0,0') end}, + {'d', 'dump', hasArg=true, + handler=function(arg) parse_coords(opts, 'dump_pos', arg) end}, + {'e', 'everywhere', + handler=function() parse_percentages(opts, '100,100,100,100') end}, + {'h', 'help', handler=function() opts.help = true end}, + {'p', 'percentages', hasArg=true, + handler=function(arg) parse_percentages(opts, arg) end}, + {'z', 'cur-zlevel', handler=function() use_zlevel = true end}, + }) + + if positionals[1] == 'help' then opts.help = true end + if opts.help then return end + + if use_zlevel then + local x, y, z = df.global.world.map.x_count - 1, + df.global.world.map.y_count - 1, + df.global.window_z + parse_coords(opts, 'start', ('0,0,%d'):format(z)) + parse_coords(opts, 'end', ('%d,%d,%d'):format(x, y, z)) + elseif #positionals >= 1 then + parse_coords(opts, 'start', positionals[1]) + if #positionals >= 2 then + parse_coords(opts, 'end', positionals[2]) + opts.start.x, opts['end'].x = min_to_max(opts.start.x,opts['end'].x) + opts.start.y, opts['end'].y = min_to_max(opts.start.y,opts['end'].y) + opts.start.z, opts['end'].z = min_to_max(opts.start.z,opts['end'].z) + else + utils.assign(opts['end'], opts.start) + end + end +end + +return _ENV