diff --git a/docs/Tags.rst b/docs/Tags.rst index 0856bb88ed..55bfde977b 100644 --- a/docs/Tags.rst +++ b/docs/Tags.rst @@ -42,7 +42,7 @@ for the tag assignment spreadsheet. - `labors `: Tools that deal with labor assignment. - `map `: Tools that interact with the game map. - `military `: Tools that interact with the military. -- `plants `: Tools that interact with trees, shrubs, and crops. +- `plants `: Tools that interact with grass, trees, shrubs, and crops. - `stockpiles `: Tools that interact with stockpiles. - `units `: Tools that interact with units. - `workorders `: Tools that interact with workorders. diff --git a/docs/about/Removed.rst b/docs/about/Removed.rst index 924a470310..8f03e7ac48 100644 --- a/docs/about/Removed.rst +++ b/docs/about/Removed.rst @@ -283,6 +283,12 @@ petcapRemover ============= Renamed to `pet-uncapper`. +.. _plants: + +plants +====== +Renamed to `plant`. + .. _resume: resume diff --git a/docs/changelog.txt b/docs/changelog.txt index dfacd74899..14bf954ca0 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -52,13 +52,16 @@ Template for new versions: # Future ## New Tools +- `plant`: (reinstated) tool for creating/growing/removing plants ## New Features - `tweak`: ``named-codices``: display book titles instead of a material description in the stocks/trade screens +- `plant`: can now ``remove`` shrubs and saplings; ``list`` all valid shrub/sapling raw IDs; ``grow`` can make mature trees older; many new command options ## Fixes - `suspendmanager`: stop suspending single tile stair constructions - ``Gui::makeAnnouncement``, ``Gui::autoDFAnnouncement``: fix skipping index 0 when iterating reports vector +- `regrass`: don't remove mud on regrass, consistent with vanilla behavior - `seedwatch`: display a limit of ``-`` instead of ``0`` for a seed that is present in inventory but not being watched - `tiletypes`: make aquifers functional when adding the ``aquifer`` property - `seedwatch`: do not include unplantable tree seeds in its status report @@ -70,6 +73,7 @@ Template for new versions: - `blueprint`: capture track carving designations in addition to already-carved tracks - `changevein`: affect connected veins even outside of the selected map block - `logistics`: new ability to automatically forbid or claim items brought to a stockpile (or a dump within a stockpile) +- `regrass`: now accepts numerical IDs for grass raws; ``regrass --list`` replaces ``regrass --plant ""`` - `tiletypes`: performance improvements when affecting tiles over a large range - `tiletypes`: support for heavy aquifers - Dreamfort: add a full complement of beds and chests to both barracks @@ -109,8 +113,8 @@ Template for new versions: - ``plugins.tiletypes.tiletypes_setTile``: can now accept a table with for access to previously unavailable options - ``dialogs.showYesNoPrompt``: extend options so the standard dialog can be used for `gui/confirm`-style confirmation prompts - ## Removed +- `plants`: renamed to `plant` - ``gui.FramedScreen``: this class is now deprecated; please use ``gui.ZScreen`` and ``widgets.Window`` instead # 50.13-r2 diff --git a/docs/plugins/plant.rst b/docs/plugins/plant.rst new file mode 100644 index 0000000000..260524ada4 --- /dev/null +++ b/docs/plugins/plant.rst @@ -0,0 +1,163 @@ +plant +===== + +.. dfhack-tool:: + :summary: Grow and remove shrubs or trees. + :tags: adventure fort armok map plants + +Grow and remove shrubs or trees. Modes are ``list``, ``create``, ``grow``, +and ``remove``. ``list`` prints a list of all valid shrub and sapling raw IDs. +``create`` allows the creation of new shrubs and saplings. ``grow`` adjusts +the age of saplings and trees, allowing them to grow instantly. ``remove`` can +remove existing shrubs and saplings. + +Usage +----- + +Provide a mode (including a ``plant_id`` for ``create``) followed by optional +``pos`` arguments and options. The ``pos`` arguments can limit operation of +``grow`` or ``remove`` to a single tile or a cuboid. ``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. + +:: + + plant list + +Prints a list of all shrub and sapling raw IDs for use with the other modes. +Both numerical and string IDs are provided. + +:: + + plant create [] [] + +Creates a new plant of the specified type at ``pos`` or the cursor position. +The target must be a floor tile, consisting of soil, grass, ashes, or +non-smooth muddy (layer, obsidian, or ore) stone. ``plant_id`` is not +case-sensitive, but must be enclosed in quotes if spaces exist. (No unmodded +shrub or sapling raw IDs have spaces.) A numerical ID can be used in place of a +string. Use ``plant list`` for a list of valid IDs. + +:: + + plant grow [ []] [] + +Grows saplings (including dead ones) into trees. Will default to all saplings +on the map if no ``pos`` arguments are used. Saplings will die and fail to grow +if they are blocked by another tree. + +:: + + plant remove [ []] [] + +Remove plants from the map (or area defined by ``pos`` arguments). By default, +it only removes invalid plants that exist on non-plant tiles (due to +:bug:`12868`). The ``--shrubs`` and ``--saplings`` options allow normal plants +to be targeted instead. Removal of fully-grown trees isn't currently supported. + +Examples +-------- + +``plant list`` + List all valid shrub and sapling raw IDs. +``plant create tower_cap`` + Create a Tower Cap sapling at the cursor. +``plant create 203 -c -a tree`` + Create a Willow sapling at the cursor, even away from water features, + ready to mature into a tree. +``plant create single-grain_wheat 70,70,140`` + Create a Single-grain Wheat shrub at (70, 70, 140). +``plant grow`` + Attempt to grow all saplings on the map into trees. +``plant grow -z -f maple,200,sand_pear`` + Attempt to grow all Maple, Acacia, and Sand Pear saplings on the current + z-level into trees. +``plant grow 0,0,100 19,19,119 -a 10`` + Set the age of all saplings and trees (with their original sapling tile) + in the defined 20 x 20 x 20 cube to at least 10 years. +``plant remove`` + Remove all invalid plants from the map. +``plant remove here -sp`` + Remove the shrub or sapling at the cursor. +``plant remove -spd`` + Remove all dead shrubs and saplings from the map. +``plant remove 0,0,49 0,0,51 -pz -e nether_cap`` + Remove all saplings on z-levels 49 to 51, excluding Nether Cap. + +Create Options +-------------- + +``-c``, ``--force`` + Create plant even on tiles flagged ``no_grow`` and unset the flag. This + flag is set on tiles that were originally boulders or pebbles, as well + as on some tiles found in deserts, etc. Also allow non-``[DRY]`` plants + (e.g., willow) to grow away (3+ tiles) from water features (i.e., pools, + brooks, and rivers), and non-``[WET]`` plants (e.g., prickle berry) to + grow near them. +``-a``, ``--age `` + Set the created plant to a specific age (in years). ``value`` can be a + non-negative integer, or one of the strings ``tree``/``1x1`` (3 years), + ``2x2`` (201 years), or ``3x3`` (401 years). ``value`` will be capped at + 1250. Defaults to 0 if option is unused. Only a few tree types grow wider + than 1x1, but many may grow taller. (Going directly to higher years will + stunt height. It may be more desirable to instead use ``plant grow`` in + stages, or just spawn full trees using `gui/sandbox`.) + +Grow Options +------------ + +``-a``, ``--age `` + Define the age (in years) to set saplings to. ``value`` can be a + non-negative integer, or one of the strings ``tree``/``1x1`` (3 years), + ``2x2`` (201 years), or ``3x3`` (401 years). ``value`` will be capped at + 1250. Defaults to 3 if option is unused. If a ``value`` larger than 3 is + used, it will make sure even fully-grown trees have an age of at least the + given value, allowing them to grow larger. (Going directly to higher years + will stunt tree height. It may be more desirable to grow in stages rather + than all at once. Trees grow taller every 10 years.) +``-f``, ``--filter [,...]`` + Define a filter list of plant raw IDs to target, ignoring all other tree + types. ``plant_id`` is not case-sensitive, but must be enclosed in quotes + if spaces exist. (No unmodded tree raw IDs have spaces.) A numerical ID + can be used in place of a string. Use ``plant list`` and check under + ``Saplings`` for a list of valid IDs. +``-e``, ``--exclude [,...]`` + Same as ``--filter``, but target everything except these. Cannot be used + with ``--filter``. +``-z``, ``--zlevel`` + Operate on a range of z-levels instead of default targeting. Will do all + z-levels between ``pos`` arguments if both are given (instead of cuboid), + z-level of first ``pos`` if one is given (instead of single tile), else + z-level of current view if no ``pos`` is given (instead of entire map). +``-n``, ``--dry-run`` + Don't actually grow plants. Just print the total number of plants that + would be grown. + +Remove Options +-------------- + +``-s``, ``--shrubs`` + Target shrubs for removal. +``-p``, ``--saplings`` + Target saplings for removal. +``-d``, ``--dead`` + Only target dead plants for removal. Can't be used without ``--shrubs`` + or ``--saplings``. +``-f``, ``--filter [,...]`` + Define a filter list of plant raw IDs to target, ignoring all other plant + types. This applies after ``--shrubs`` and ``--saplings`` are targeted, + and can't be used without one of those options. ``plant_id`` is not + case-sensitive, but must be enclosed in quotes if spaces exist. (No + unmodded shrub or sapling raw IDs have spaces.) A numerical ID can be + used in place of a string. Use ``plant list`` for a list of valid IDs. +``-e``, ``--exclude [,...]`` + Same as ``--filter``, but target everything except these. Cannot be used + with ``--filter``. +``-z``, ``--zlevel`` + Operate on a range of z-levels instead of default targeting. Will do all + z-levels between ``pos`` arguments if both are given (instead of cuboid), + z-level of first ``pos`` if one is given (instead of single tile), else + z-level of current view if no ``pos`` is given (instead of entire map). +``-n``, ``--dryrun`` + Don't actually remove plants. Just print the total number of plants that + would be removed. diff --git a/docs/plugins/plants.rst b/docs/plugins/plants.rst deleted file mode 100644 index d35579e1d4..0000000000 --- a/docs/plugins/plants.rst +++ /dev/null @@ -1,33 +0,0 @@ -.. _plant: - -plants -====== - -.. dfhack-tool:: - :summary: Provides commands that interact with plants. - :tags: unavailable - :no-command: - -.. dfhack-command:: plant - :summary: Create a plant or make an existing plant grow up. - -Usage ------ - -``plant create `` - Creates a new plant of the specified type at the active cursor position. - The cursor must be on a dirt or grass floor tile. -``plant grow`` - Grows saplings into trees. If the cursor is active, it only affects the - sapling under the cursor. If no cursor is active, it affect all saplings - on the map. - -To see the full list of plant ids, run the following command:: - - devel/query --table df.global.world.raws.plants.all --search ^id --maxdepth 1 - -Example -------- - -``plant create TOWER_CAP`` - Create a Tower Cap sapling at the cursor position. diff --git a/docs/plugins/regrass.rst b/docs/plugins/regrass.rst index 2bafc82bcf..525a20c6a1 100644 --- a/docs/plugins/regrass.rst +++ b/docs/plugins/regrass.rst @@ -3,7 +3,7 @@ regrass .. dfhack-tool:: :summary: Regrow surface grass and cavern moss. - :tags: adventure fort armok animals map + :tags: adventure fort armok animals map plants This command can refresh the grass (and subterranean moss) growing on your map. Operates on floors, stairs, and ramps. Also works underneath shrubs, saplings, @@ -24,9 +24,45 @@ can be used in place of numeric coordinates to use the position of the keyboard cursor, if active. The ``--block`` and ``--zlevel`` options use the ``pos`` values differently. +Examples +-------- + +``regrass`` + Regrass the entire map, refilling existing and depleted grass. +``regrass here`` + Regrass the selected tile, refilling existing and depleted grass. +``regrass here 0,0,90 --zlevel`` + Regrass all z-levels including the selected tile's z-level through z-level + 90, refilling existing and depleted grass. +``regrass 0,0,100 19,19,119 --ashes --mud`` + Regrass tiles in the 20 x 20 x 20 cube defined by the coords, refilling + existing and depleted grass, and converting ashes and muddy stone (if + respective blocks ever had grass). +``regrass 10,10,100 -baudnm`` + Regrass the block that contains the given coord; converting ashes, muddy + stone, and tiles under buildings; adding all compatible grass types, and + filling each grass type to max. +``regrass -f`` + Regrass the entire map, refilling existing and depleted grass, else filling + with a randomly selected grass type if non-existent. +``regrass -l`` + Print all valid grass raw IDs for use with ``--plant``. Both numerical and + string IDs are provided. This ignores all other options and skips regrass. +``regrass -zf -p 128`` + Regrass the current z-level, refilling existing and depleted grass, else + filling with ``underlichen`` if non-existent. +``regrass here -bnf -p "dog's tooth grass"`` + Regrass the selected block, adding all compatible grass types to block data, + ``dog's tooth grass`` if no compatible types exist. Refill existing grass + on each tile, else select one of the block's types if depleted or + previously non-existent. + Options ------- +``-l``, ``--list`` + List all available grass raw IDs that you can later pass to the ``--plant`` + option. The map will not be affected when running with this option. ``-m``, ``--max`` Maxes out every grass type in the tile, giving extra grazing time. Not normal DF behavior. Tile will appear to be the first type of grass @@ -44,12 +80,13 @@ Options forced instead. By default, a single random grass type is selected from the world's raws. The ``--plant`` option allows a specific grass type to be specified. -``-p ``, ``--plant `` +``-p``, ``--plant `` Specify a grass type for the ``--force`` option. ``grass_id`` is not - case-sensitive, but must be enclosed in quotes if spaces exist. Providing - an empty string with "" will print all available IDs and skip regrass. + case-sensitive, but must be enclosed in quotes if spaces exist. A numerical + ID can also be used. ``-a``, ``--ashes`` - Regrass tiles that've been burnt to ash. + Regrass tiles that've been burnt to ash. Usually ash must revert to soil + first before grass can grow. ``-d``, ``--buildings`` Regrass tiles under certain passable building tiles including stockpiles, planned buildings, workshops, and farms. (Farms will convert grass tiles to @@ -65,40 +102,8 @@ Options `devel/block-borders` can be used to visualize map blocks. ``-z``, ``--zlevel`` Regrass entire z-levels. Will do all z-levels between ``pos`` arguments if - both given, z-level of first ``pos`` if one given, else z-level of - viewscreen if no ``pos`` given. - -Examples --------- - -``regrass`` - Regrass the entire map, refilling existing and depleted grass. -``regrass here`` - Regrass the selected tile, refilling existing and depleted grass. -``regrass here 0,0,90 --zlevel`` - Regrass all z-levels including the selected tile's z-level through z-level - 90, refilling existing and depleted grass. -``regrass 0,0,100 19,19,119 --ashes --mud`` - Regrass tiles in the 20x20x20 cube defined by the coords, refilling - existing and depleted grass, and converting ashes and muddy stone (if - respective blocks ever had grass.) -``regrass 10,10,100 -baudnm`` - Regrass the block that contains the given coord; converting ashes, muddy - stone, and tiles under buildings; adding all compatible grass types, and - filling each grass type to max. -``regrass -f`` - Regrass the entire map, refilling existing and depleted grass, else filling - with a randomly selected grass type if non-existent. -``regrass -p ""`` - Print all valid grass raw ids. Don't regrass. -``regrass -zf -p underlichen`` - Regrass the current z-level, refilling existing and depleted grass, else - filling with ``underlichen`` if non-existent. -``regrass here -bnf -p "dog's tooth grass"`` - Regrass the selected block, adding all compatible grass types to block data, - ``dog's tooth grass`` if no compatible types exist. Refill existing grass - on each tile, else select one of the block's types if depleted or - previously non-existent. + both are given, z-level of first ``pos`` if one is given, else z-level of + current view if no ``pos`` is given. Troubleshooting --------------- diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index dfa84e181a..a76bd79332 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -141,7 +141,7 @@ if(BUILD_SUPPORTED) dfhack_plugin(overlay overlay.cpp LINK_LIBRARIES lua) dfhack_plugin(pathable pathable.cpp LINK_LIBRARIES lua) dfhack_plugin(pet-uncapper pet-uncapper.cpp) - #dfhack_plugin(plants plants.cpp) + dfhack_plugin(plant plant.cpp LINK_LIBRARIES lua) dfhack_plugin(preserve-tombs preserve-tombs.cpp) dfhack_plugin(probe probe.cpp LINK_LIBRARIES lua) dfhack_plugin(prospector prospector.cpp LINK_LIBRARIES lua) diff --git a/plugins/lua/plant.lua b/plugins/lua/plant.lua new file mode 100644 index 0000000000..2002e787f8 --- /dev/null +++ b/plugins/lua/plant.lua @@ -0,0 +1,124 @@ +local _ENV = mkmodule('plugins.plant') + +local argparse = require('argparse') +local utils = require('utils') + +local function search_str(s) + return dfhack.upperCp437(dfhack.toSearchNormalized(s)) +end + +local function find_plant_idx(s) --find plant raw index by id string + local id_str = search_str(s) + for k, v in ipairs(df.global.world.raws.plants.all) do + if search_str(v.id) == id_str then + return k + end + end + + qerror('Plant raw not found: "'..s..'"') +end + +local function find_plant(s) --accept index string or match id string + if tonumber(s) then + return argparse.nonnegativeInt(s, 'plant_id') + else + return find_plant_idx(s) + end +end + +local function build_filter(vec, s) + if #vec > 0 then + qerror('Filter already defined!') + end + + local set = {} + for _,id in ipairs(argparse.stringList(s, 'list')) do + if id ~= '' then + set[find_plant(id)] = true + end + end + + for idx,_ in pairs(set) do + vec:insert('#', idx) --add plant raw indices to vector + end +end + +local year_table = +{ + tree = 3, --sapling_to_tree_threshold + ["1x1"] = 3, + ["2x2"] = 201, --kapok, ginkgo, highwood + ["3x3"] = 401, --highwood (tower-cap is bugged) +} + +local function plant_age(s) --tree stage or numerical value + local n + if tonumber(s) then + n = argparse.nonnegativeInt(s, 'age') + else + n = year_table[s:lower()] + end + + if n then + n = math.min(n, 1250) --grow_counter stops at 1250 years + return math.max(0, 40320*n-1) --years to tens of ticks - 1; correct for n = 0 + end + + qerror('Invalid age: "'..s..'"') +end + +function parse_commandline(opts, pos_1, pos_2, filter_vec, args) + local positionals = argparse.processArgsGetopt(args, + { + {'c', 'force', handler=function() opts.force = true end}, + {'s', 'shrubs', handler=function() opts.shrubs = true end}, + {'p', 'saplings', handler=function() opts.saplings = true end}, + {'t', 'trees', handler=function() opts.trees = true end}, + {'d', 'dead', handler=function() opts.dead = true end}, + {'a', 'age', hasArg=true, handler=function(optarg) + opts.age = plant_age(optarg) end}, + {'f', 'filter', hasArg=true, handler=function(optarg) + build_filter(filter_vec, optarg) end}, + {'e', 'exclude', hasArg=true, handler=function(optarg) + opts.filter_ex = true + build_filter(filter_vec, optarg) end}, + {'z', 'zlevel', handler=function() opts.zlevel = true end}, + {'n', 'dry-run', handler=function() opts.dry_run = true end}, + }) + + if #positionals > 3 then + qerror('Too many positionals!') + end + + local p1 = positionals[1] + if not p1 then + qerror('Specify mode: list, create, grow, or remove!') + elseif p1 == 'list' then + opts.plant_idx = -2 --will print all non-grass IDs + elseif p1 == 'create' then + opts.create = true + + if positionals[2] then + opts.plant_idx = find_plant(positionals[2]) + else + qerror('Must specify plant_id for create!') + end + elseif p1 == 'grow' then + opts.grow = true + elseif p1 == 'remove' then + opts.del = true + else + qerror('Invalid mode: "'..p1..'"! Must be list, create, grow, or remove!') + end + + local n = opts.create and 3 or 2 + if positionals[n] then + utils.assign(pos_1, argparse.coords(positionals[n], 'pos_1', true)) + end + + if not opts.create and positionals[3] then + utils.assign(pos_2, argparse.coords(positionals[3], 'pos_2', true)) + end +end + +return _ENV diff --git a/plugins/lua/regrass.lua b/plugins/lua/regrass.lua index 63205b5a33..38fd2ddacb 100644 --- a/plugins/lua/regrass.lua +++ b/plugins/lua/regrass.lua @@ -7,7 +7,7 @@ local function search_str(s) return dfhack.upperCp437(dfhack.toSearchNormalized(s)) end -local function find_grass_idx(s) +local function find_grass_idx(s) --find plant raw index by id string local id_str = search_str(s) for _,grass in ipairs(df.global.world.raws.plants.grasses) do if search_str(grass.id) == id_str then @@ -15,13 +15,22 @@ local function find_grass_idx(s) end end - return -1 + qerror('Plant raw not found: "'..s..'"') +end + +local function find_grass(s) --accept index string or match id string + if tonumber(s) then + return argparse.nonnegativeInt(s, 'grass_id') + else + return find_grass_idx(s) + end end function parse_commandline(opts, pos_1, pos_2, args) - local plant_str + local plant_str, do_list local positionals = argparse.processArgsGetopt(args, { + {'l', 'list', handler=function() do_list = true end}, {'m', 'max', handler=function() opts.max_grass = true end}, {'n', 'new', handler=function() opts.new_grass = true end}, {'a', 'ashes', handler=function() opts.ashes = true end}, @@ -33,13 +42,13 @@ function parse_commandline(opts, pos_1, pos_2, args) {'p', 'plant', hasArg=true, handler=function(optarg) plant_str = optarg end}, }) - if plant_str then - if plant_str == '' then --will print all ids - opts.forced_plant = -2 - elseif not opts.force then + if do_list then + opts.forced_plant = -2 --will print all grass IDs + elseif plant_str then + if not opts.force then qerror('Use of --plant without --force!') else - opts.forced_plant = find_grass_idx(plant_str) + opts.forced_plant = find_grass(plant_str) end elseif opts.force then local grasses = df.global.world.raws.plants.grasses diff --git a/plugins/plant.cpp b/plugins/plant.cpp new file mode 100644 index 0000000000..741ff00892 --- /dev/null +++ b/plugins/plant.cpp @@ -0,0 +1,734 @@ +// Grow and remove shrubs or trees. + +#include "Debug.h" +#include "LuaTools.h" +#include "PluginManager.h" +#include "TileTypes.h" + +#include "modules/Gui.h" +#include "modules/Maps.h" + +#include "df/block_square_event_grassst.h" +#include "df/block_square_event_material_spatterst.h" +#include "df/builtin_mats.h" +#include "df/map_block.h" +#include "df/map_block_column.h" +#include "df/plant.h" +#include "df/plant_raw.h" +#include "df/world.h" + +using std::vector; +using std::string; +using namespace DFHack; + +DFHACK_PLUGIN("plant"); +REQUIRE_GLOBAL(world); + +namespace DFHack +{ + DBG_DECLARE(plant, log, DebugCategory::LINFO); +} + +struct cuboid +{ + int16_t x_min = -1; + int16_t x_max = -1; + int16_t y_min = -1; + int16_t y_max = -1; + int16_t z_min = -1; + int16_t z_max = -1; + + bool isValid() const + { // True if all max >= min >= 0 + return (x_min >= 0 && y_min >= 0 && z_min >= 0 && + x_max >= x_min && y_max >= y_min && z_max >= z_min); + } + + bool addPos(int16_t x, int16_t y, int16_t z) + { // Expand cuboid to include point. Return true if bounds changed + if (x < 0 || y < 0 || z < 0 || (isValid() && containsPos(x, y, z))) + return false; + + x_min = (x_min < 0 || x < x_min) ? x : x_min; + x_max = (x_max < 0 || x > x_max) ? x : x_max; + + y_min = (y_min < 0 || y < y_min) ? y : y_min; + y_max = (y_max < 0 || y > y_max) ? y : y_max; + + z_min = (z_min < 0 || z < z_min) ? z : z_min; + z_max = (z_max < 0 || z > z_max) ? z : z_max; + + return true; + } + inline bool addPos(const df::coord &pos) { return addPos(pos.x, pos.y, pos.z); } + + bool containsPos(int16_t x, int16_t y, int16_t z) const + { // Return true if point inside cuboid. Make sure cuboid is valid first! + return x >= x_min && x <= x_max && + y >= y_min && y <= y_max && + z >= z_min && z <= z_max; + } + inline bool containsPos(const df::coord &pos) const { return containsPos(pos.x, pos.y, pos.z); } +}; + +struct plant_options +{ + bool create = false; // Create a plant + bool grow = false; // Grow saplings into trees + bool del = false; // Remove plants + bool force = false; // Create plants on no_grow or incompatible wet/dry + bool shrubs = false; // Remove shrubs + bool saplings = false; // Remove saplings + bool trees = false; // Remove grown trees + bool dead = false; // Remove only dead plants + bool filter_ex = false; // Filter vector excludes if true, else requires its plants + bool zlevel = false; // Operate on entire z-levels + bool dry_run = false; // Don't actually grow or remove anything + + int32_t plant_idx = -1; // Plant raw index of plant to create; -2 means print all non-grass IDs + int32_t age = -1; // Set plant to this age for grow/create; -1 for default + + static struct_identity _identity; +}; +static const struct_field_info plant_options_fields[] = +{ + { struct_field_info::PRIMITIVE, "create", offsetof(plant_options, create), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "grow", offsetof(plant_options, grow), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "del", offsetof(plant_options, del), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "force", offsetof(plant_options, force), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "shrubs", offsetof(plant_options, shrubs), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "saplings", offsetof(plant_options, saplings), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "trees", offsetof(plant_options, trees), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "dead", offsetof(plant_options, dead), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "filter_ex", offsetof(plant_options, filter_ex), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "zlevel", offsetof(plant_options, zlevel), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "dry_run", offsetof(plant_options, dry_run), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "plant_idx", offsetof(plant_options, plant_idx), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::PRIMITIVE, "age", offsetof(plant_options, age), &df::identity_traits::identity, 0, 0 }, + { struct_field_info::END } +}; +struct_identity plant_options::_identity(sizeof(plant_options), &df::allocator_fn, NULL, "plant_options", NULL, plant_options_fields); + +const int32_t sapling_to_tree_threshold = 120 * 28 * 12 * 3 - 1; // 3 years minus 1; let the game handle the actual growing-up + +static bool tile_muddy(const df::coord &pos) +{ // True if tile has mud spatter + auto block = Maps::getTileBlock(pos); + if (!block) + return false; + + for (auto blev : block->block_events) + { + if (blev->getType() != block_square_event_type::material_spatter) + continue; + + auto &ms_ev = *(df::block_square_event_material_spatterst *)blev; + if (ms_ev.mat_type == builtin_mats::MUD) + return ms_ev.amount[pos.x&15][pos.y&15] > 0; + } + + return false; +} + +static bool tile_watery(const df::coord &pos) +{ // Determines if plant should be in wet or dry vector + int32_t x = pos.x, y = pos.y, z = pos.z - 1; + + for (int32_t dx = -2; dx <= 2; dx++) + { // Check 5x5 area under tile, skipping corners + for (int32_t dy = -2; dy <= 2; dy++) + { + if (abs(dx) == 2 && abs(dy) == 2) + continue; // Skip corners + + auto tt = Maps::getTileType(x+dx, y+dy, z); + if (!tt) + continue; // Invalid tile + + auto mat = tileMaterial(*tt); + if (mat == tiletype_material::POOL || + mat == tiletype_material::RIVER || + tileShape(*tt) == tiletype_shape::BROOK_BED) + { + return true; + } + } + } + + return false; +} + +static bool plant_shrub(const df::plant &plant) +{ + return plant.type == df::plant_type::DRY_PLANT || plant.type == df::plant_type::WET_PLANT; +} + +static bool plant_shrub(const df::plant *plant) +{ + return plant_shrub(*plant); +} + +command_result df_createplant(color_ostream &out, const df::coord &pos, const plant_options &options) +{ + auto col = Maps::getBlockColumn((pos.x / 48)*3, (pos.y / 48)*3); + auto tt = Maps::getTileType(pos); + if (!tt || !col) + { + out.printerr("No map block at pos!\n"); + return CR_FAILURE; + } + + if (tileShape(*tt) != tiletype_shape::FLOOR) + { + out.printerr("Can't create plant: Not floor!\n"); + return CR_FAILURE; + } + + auto occ = Maps::getTileOccupancy(pos); + CHECK_NULL_POINTER(occ); + if (occ->bits.building > tile_building_occ::None) + { + out.printerr("Can't create plant: Building present!\n"); + return CR_FAILURE; + } + else if (!options.force && occ->bits.no_grow) + { + out.printerr("Can't create plant: Tile flagged no_grow and not --force!\n"); + return CR_FAILURE; + } + + auto des = Maps::getTileDesignation(pos); + CHECK_NULL_POINTER(des); + if (des->bits.flow_size > (des->bits.liquid_type == tile_liquid::Magma ? 0 : 3)) + { + out.printerr("Can't create plant: Too much liquid!\n"); + return CR_FAILURE; + } + + auto spec = tileSpecial(*tt); + auto mat = tileMaterial(*tt); + switch (mat) + { + case tiletype_material::SOIL: + case tiletype_material::GRASS_LIGHT: + case tiletype_material::GRASS_DARK: + case tiletype_material::ASHES: + break; + case tiletype_material::STONE: + case tiletype_material::LAVA_STONE: + case tiletype_material::MINERAL: + if (spec == tiletype_special::SMOOTH || spec == tiletype_special::TRACK) + { + out.printerr("Can't create plant: Smooth stone!\n"); + return CR_FAILURE; + } + else if (!tile_muddy(pos)) + { + out.printerr("Can't create plant: Non-muddy stone!\n"); + return CR_FAILURE; + } + + break; + default: + out.printerr("Can't create plant: Wrong tile material!\n"); + return CR_FAILURE; + } + + auto p_raw = vector_get(world->raws.plants.all, options.plant_idx); + if (!p_raw) + { + out.printerr("Can't create plant: Plant raw not found!\n"); + return CR_FAILURE; + } + + bool is_watery = tile_watery(pos); + if (!options.force) + { // Check if plant compatible with wet/dry + if ((is_watery && !p_raw->flags.is_set(plant_raw_flags::WET)) || + (!is_watery && !p_raw->flags.is_set(plant_raw_flags::DRY))) + { + out.printerr("Can't create plant: Plant type can't grow this %s water feature!\n" + "Override with --force\n", is_watery ? "close to" : "far from"); + return CR_FAILURE; + } + } + + auto plant = df::allocate(); + bool is_shrub = true; + if (p_raw->flags.is_set(plant_raw_flags::TREE)) + { + plant->type = is_watery ? df::plant_type::WET_TREE : df::plant_type::DRY_TREE; + plant->hitpoints = 400000; + is_shrub = false; + } + else + { + plant->type = is_watery ? df::plant_type::WET_PLANT : df::plant_type::DRY_PLANT; + plant->hitpoints = 100000; + } + + plant->material = options.plant_idx; + plant->pos = pos; + plant->grow_counter = options.age < 0 ? 0 : options.age; + plant->update_order = rand() % 10; + + world->plants.all.push_back(plant); + if (is_shrub) + { + if (is_watery) + world->plants.shrub_wet.push_back(plant); + else + world->plants.shrub_dry.push_back(plant); + } + else + { + if (is_watery) + world->plants.tree_wet.push_back(plant); + else + world->plants.tree_dry.push_back(plant); + } + + col->plants.push_back(plant); + if (is_shrub) + *tt = tiletype::Shrub; + else + *tt = tiletype::Sapling; + + occ->bits.no_grow = false; + + return CR_OK; +} + +command_result df_grow(color_ostream &out, const cuboid &bounds, const plant_options &options, vector *filter = nullptr) +{ + if (!bounds.isValid()) + { + out.printerr("Invalid cuboid! (%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); + return CR_FAILURE; + } + + bool do_filter = filter && !filter->empty(); + if (do_filter) // Sort filter vector + std::sort(filter->begin(), filter->end()); + + int32_t age = options.age < 0 ? sapling_to_tree_threshold : options.age; // Enforce default + bool do_trees = age > sapling_to_tree_threshold; + + int grown = 0, grown_trees = 0; + for (auto plant : world->plants.all) + { + if (plant_shrub(plant)) + continue; // Shrub + else if (!bounds.containsPos(plant->pos)) + continue; // Outside cuboid + else if (do_filter && (vector_contains(*filter, (int32_t)plant->material) == options.filter_ex)) + continue; // Filtered out + else if (plant->tree_info) + { // Tree + if (do_trees && !plant->damage_flags.bits.dead && plant->grow_counter < age) + { + if (!options.dry_run) + plant->grow_counter = age; + grown_trees++; + } + continue; // Next plant + } + + auto tt = Maps::getTileType(plant->pos); + if (!tt || tileShape(*tt) != tiletype_shape::SAPLING) + { + out.printerr("Invalid sapling tiletype at (%d, %d, %d): %s!\n", + plant->pos.x, plant->pos.y, plant->pos.z, + tt ? ENUM_KEY_STR(tiletype, *tt).c_str() : "No map block!"); + continue; // Bad tiletype + } + else if (*tt == tiletype::SaplingDead) + *tt = tiletype::Sapling; // Revive sapling + + if (!options.dry_run) + { + plant->damage_flags.bits.dead = false; + plant->grow_counter = age; + } + grown++; + } + + if (do_trees) + out.print("%d saplings and %d trees%s set to grow.\n", grown, grown_trees, options.dry_run ? " would be" : ""); + else + out.print("%d saplings%s set to grow.\n", grown, options.dry_run ? " would be" : ""); + + return CR_OK; +} + +static bool uncat_plant(df::plant *plant) +{ // Remove plant from extra vectors + vector *vec = NULL; + + switch (plant->type) + { + case df::plant_type::DRY_PLANT: vec = &world->plants.shrub_dry; break; + case df::plant_type::WET_PLANT: vec = &world->plants.shrub_wet; break; + case df::plant_type::DRY_TREE: vec = &world->plants.tree_dry; break; + case df::plant_type::WET_TREE: vec = &world->plants.tree_wet; break; + } + + for (size_t i = vec->size(); i-- > 0;) + { // Not sorted, but more likely near end + if ((*vec)[i] == plant) + { + vec->erase(vec->begin() + i); + break; + } + } + + auto col = Maps::getBlockColumn((plant->pos.x / 48)*3, (plant->pos.y / 48)*3); + if (!col) + return false; + + vec = &col->plants; + for (size_t i = vec->size(); i-- > 0;) + { // Not sorted, but more likely near end + if ((*vec)[i] == plant) + { + vec->erase(vec->begin() + i); + break; + } + } + + return true; +} + +static bool has_grass(df::map_block *block, int tx, int ty) +{ // Block tile has grass + for (auto blev : block->block_events) + { + if (blev->getType() != block_square_event_type::grass) + continue; + + auto &g_ev = *(df::block_square_event_grassst *)blev; + if (g_ev.amount[tx][ty] > 0) + return true; + } + + return false; +} + +static void set_tt(const df::coord &pos) +{ // Set tiletype to grass or soil floor + auto block = Maps::getTileBlock(pos); + if (!block) + return; + + int tx = pos.x & 15, ty = pos.y & 15; + if (has_grass(block, tx, ty)) + block->tiletype[tx][ty] = findRandomVariant((rand() & 1) ? tiletype::GrassLightFloor1 : tiletype::GrassDarkFloor1); + else + block->tiletype[tx][ty] = findRandomVariant(tiletype::SoilFloor1); +} + +command_result df_removeplant(color_ostream &out, const cuboid &bounds, const plant_options &options, vector *filter = nullptr) +{ + if (!bounds.isValid()) + { + out.printerr("Invalid cuboid! (%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); + return CR_FAILURE; + } + + bool by_type = options.shrubs || options.saplings || options.trees; + bool do_filter = by_type && filter && !filter->empty(); + if (do_filter) // Sort filter vector + std::sort(filter->begin(), filter->end()); + + + int count = 0, count_bad = 0; + auto &vec = world->plants.all; + for (size_t i = vec.size(); i-- > 0;) + { + auto &plant = *vec[i]; + auto tt = Maps::getTileType(plant.pos); + + if (plant.tree_info) // TODO: handle trees + continue; // Not implemented + else if (by_type) + { + if (options.dead && !plant.damage_flags.bits.dead && tt && tileSpecial(*tt) != tiletype_special::DEAD) + continue; // Not removing living + /*else if (plant->tree_info && !options.trees) + continue; // Not removing trees*/ + else if (plant_shrub(plant)) + { + if (!options.shrubs) + continue; // Not removing shrubs + } + else if (!options.saplings) + continue; // Not removing saplings + } + + if (!bounds.containsPos(plant.pos)) + continue; // Outside cuboid + else if (do_filter && (vector_contains(*filter, (int32_t)plant.material) == options.filter_ex)) + continue; // Filtered out + + bool bad_tt = false; + if (tt) + { + if (plant_shrub(plant)) + { + if (tileShape(*tt) != tiletype_shape::SHRUB) + { + out.printerr("Bad shrub tiletype at (%d, %d, %d): %s\n", + plant.pos.x, plant.pos.y, plant.pos.z, + ENUM_KEY_STR(tiletype, *tt).c_str()); + bad_tt = true; + } + } + else if (!plant.tree_info) + { + if (tileShape(*tt) != tiletype_shape::SAPLING) + { + out.printerr("Bad sapling tiletype at (%d, %d, %d): %s\n", + plant.pos.x, plant.pos.y, plant.pos.z, + ENUM_KEY_STR(tiletype, *tt).c_str()); + bad_tt = true; + } + } + // TODO: trees + } + else + { + out.printerr("Bad plant tiletype at (%d, %d, %d): No map block!\n", + plant.pos.x, plant.pos.y, plant.pos.z); + bad_tt = true; + } + + if (!by_type && !bad_tt) + continue; // Only remove bad plants + + count++; + if (bad_tt) + count_bad++; + + if (!options.dry_run) + { + if (!uncat_plant(&plant)) + out.printerr("Remove plant: No block column at (%d, %d)!\n", plant.pos.x, plant.pos.y); + + if (!bad_tt) // TODO: trees + set_tt(plant.pos); + + vec.erase(vec.begin() + i); + delete &plant; + } + } + + out.print("Plants%s removed: %d (%d bad)\n", options.dry_run ? " that would be" : "", count, count_bad); + return CR_OK; +} + +command_result df_plant(color_ostream &out, vector ¶meters) +{ + plant_options options; + cuboid bounds; + df::coord pos_1, pos_2; + vector filter; // Unsorted + + CoreSuspender suspend; + + if (!Lua::CallLuaModuleFunction(out, "plugins.plant", "parse_commandline", + std::make_tuple(&options, &pos_1, &pos_2, &filter, parameters))) + { + return CR_WRONG_USAGE; + } + else if (options.plant_idx == -2) + { // Print all non-grass raw IDs ("plant list") + out.print("--- Shrubs ---\n"); + for (auto p_raw : world->raws.plants.bushes) + out.print("%d: %s\n", p_raw->index, p_raw->id.c_str()); + + out.print("\n--- Saplings ---\n"); + for (auto p_raw : world->raws.plants.trees) + out.print("%d: %s\n", p_raw->index, p_raw->id.c_str()); + + return CR_OK; + } + else if (options.force && !options.create) + { + out.printerr("Can't use --force without create!\n"); + return CR_WRONG_USAGE; + } + + bool by_type = options.shrubs || options.saplings || options.trees; // Remove invalid plants otherwise + if (!options.del && (by_type || options.dead)) + { // Don't use remove options outside remove + out.printerr("Can't use remove's options without remove!\n"); + return CR_WRONG_USAGE; + } + else if (!by_type && options.dead) + { // Don't target dead plants while fixing invalid + out.printerr("Can't use --dead without targeting shrubs/saplings!\n"); // TODO: trees + return CR_WRONG_USAGE; + } + else if (options.del && options.age >= 0) + { // Can't set age with remove + out.printerr("Can't use --age with remove!\n"); + return CR_WRONG_USAGE; + } + else if (options.trees) + { // TODO: implement + out.printerr("--trees not implemented!\n"); + return CR_FAILURE; + } + + 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()) + { + out.printerr("Map not loaded!\n"); + return CR_FAILURE; + } + + if (options.create) + { // Check improper options and plant raw + if (options.zlevel || options.dry_run) + { + out.printerr("Cannot use --zlevel or --dry-run with create!\n"); + return CR_FAILURE; + } + else if (!filter.empty()) + { + out.printerr("Cannot use filter/exclude with create!\n"); + return CR_FAILURE; + } + + if (!pos_1.isValid()) + { // Attempt to use cursor for pos if active + Gui::getCursorCoords(pos_1); + DEBUG(log, out).print("Try to use cursor (%d, %d, %d) for pos_1.\n", + pos_1.x, pos_1.y, pos_1.z); + + if (!pos_1.isValid()) + { + out.printerr("Invalid pos for create! Make sure keyboard cursor is active if not entering pos manually!\n"); + return CR_WRONG_USAGE; + } + } + + DEBUG(log, out).print("plant_idx = %d\n", options.plant_idx); + auto p_raw = vector_get(world->raws.plants.all, options.plant_idx); + if (p_raw) + { + DEBUG(log, out).print("Plant raw: %s\n", p_raw->id.c_str()); + if (p_raw->flags.is_set(plant_raw_flags::GRASS)) + { + out.printerr("Plant raw was grass: %d (%s)\n", options.plant_idx, p_raw->id.c_str()); + return CR_FAILURE; + } + } + else + { + out.printerr("Plant raw not found for create: %d\n", options.plant_idx); + return CR_FAILURE; + } + } + else // options.grow || options.remove + { // Check filter and setup cuboid + if (!filter.empty()) + { // Validate filter plant raws + if (!by_type && options.del) + { + out.printerr("Filter/exclude set, but not targeting shrubs/saplings!\n"); // TODO: trees + return CR_WRONG_USAGE; + } + + for (auto idx : filter) + { + DEBUG(log, out).print("Filter/exclude test idx: %d\n", idx); + auto p_raw = vector_get(world->raws.plants.all, idx); + if (p_raw) + { + DEBUG(log, out).print("Filter/exclude raw: %s\n", p_raw->id.c_str()); + if (p_raw->flags.is_set(plant_raw_flags::GRASS)) + { + out.printerr("Filter/exclude plant raw was grass: %d (%s)\n", idx, p_raw->id.c_str()); + return CR_FAILURE; + } + else if (options.grow && !p_raw->flags.is_set(plant_raw_flags::TREE)) + { // User might copy-paste filters between grow and remove, so just log this + DEBUG(log, out).print("Filter/exclude shrub with grow: %d (%s)\n", idx, p_raw->id.c_str()); + } + } + else + { + out.printerr("Plant raw not found for filter/exclude: %d\n", idx); + return CR_FAILURE; + } + } + } + + if (options.zlevel) + { // Adjusted cuboid + if (!pos_1.isValid()) + { + DEBUG(log, out).print("pos_1 invalid and --zlevel. Using viewport.\n"); + pos_1.z = Gui::getViewportPos().z; + } + + if (!pos_2.isValid()) + { + DEBUG(log, out).print("pos_2 invalid and --zlevel. Using pos_1.\n"); + pos_2.z = pos_1.z; + } + + bounds.addPos(0, world->map.y_count-1, pos_1.z); + bounds.addPos(world->map.x_count-1, 0, pos_2.z); + } + else if (pos_1.isValid()) + { // Cuboid or single point + bounds.addPos(pos_1); + bounds.addPos(pos_2); // Point if invalid + } + else // Entire map + { + bounds.addPos(0, 0, world->map.z_count-1); + bounds.addPos(world->map.x_count-1, world->map.y_count-1, 0); + } + + 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 (!bounds.isValid()) + { + out.printerr("Invalid cuboid! (%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); + return CR_FAILURE; + } + } + + if (options.create) + return df_createplant(out, pos_1, options); + else if (options.grow) + return df_grow(out, bounds, options, &filter); + else if (options.del) + return df_removeplant(out, bounds, options, &filter); + + return CR_WRONG_USAGE; +} + +DFhackCExport command_result plugin_init(color_ostream &out, vector &commands) +{ + commands.push_back(PluginCommand( + "plant", + "Grow and remove shrubs or trees.", + df_plant)); + + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown(color_ostream &out) +{ + return CR_OK; +} diff --git a/plugins/plants.cpp b/plugins/plants.cpp deleted file mode 100644 index 16880c56e5..0000000000 --- a/plugins/plants.cpp +++ /dev/null @@ -1,213 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include "Core.h" -#include "Console.h" -#include "Export.h" -#include "PluginManager.h" -#include "modules/Maps.h" -#include "modules/Gui.h" -#include "TileTypes.h" -#include "modules/MapCache.h" - -#include "df/plant.h" -#include "df/world.h" - -using std::vector; -using std::string; -using namespace DFHack; - -DFHACK_PLUGIN("plants"); -REQUIRE_GLOBAL(world); - -const uint32_t sapling_to_tree_threshold = 120 * 28 * 12 * 3 - 1; // 3 years minus 1 - let the game handle the actual growing-up - -command_result df_grow (color_ostream &out, vector & parameters) -{ - for(size_t i = 0; i < parameters.size();i++) - { - if(parameters[i] == "help" || parameters[i] == "?") - { - return CR_WRONG_USAGE; - } - } - - CoreSuspender suspend; - - if (!Maps::IsValid()) - { - out.printerr("Map is not available!\n"); - return CR_FAILURE; - } - MapExtras::MapCache map; - int32_t x,y,z; - int grown = 0; - if(Gui::getCursorCoords(x,y,z)) - { - for(size_t i = 0; i < world->plants.all.size(); i++) - { - df::plant * tree = world->plants.all[i]; - if(tree->pos.x == x && tree->pos.y == y && tree->pos.z == z) - { - if(tileShape(map.tiletypeAt(DFCoord(x,y,z))) == tiletype_shape::SAPLING && - tileSpecial(map.tiletypeAt(DFCoord(x,y,z))) != tiletype_special::DEAD) - { - tree->grow_counter = sapling_to_tree_threshold; - grown++; - } - break; - } - } - } - else - { - for(size_t i = 0 ; i < world->plants.all.size(); i++) - { - df::plant *p = world->plants.all[i]; - df::tiletype ttype = map.tiletypeAt(df::coord(p->pos.x,p->pos.y,p->pos.z)); - bool is_shrub = plant->type == df::plant_type::DRY_PLANT || plant->type == df::plant_type::WET_PLANT; - if(!is_shrub && tileShape(ttype) == tiletype_shape::SAPLING && tileSpecial(ttype) != tiletype_special::DEAD) - { - p->grow_counter = sapling_to_tree_threshold; - grown++; - } - } - } - if (grown) - out.print("%i plants grown.\n", grown); - else - out.printerr("No plant(s) found!\n"); - - return CR_OK; -} - -command_result df_createplant (color_ostream &out, vector & parameters) -{ - if ((parameters.size() != 1) || (parameters[0] == "help" || parameters[0] == "?")) - { - return CR_WRONG_USAGE; - } - - CoreSuspender suspend; - - if (!Maps::IsValid()) - { - out.printerr("Map is not available!\n"); - return CR_FAILURE; - } - - int32_t x,y,z; - if(!Gui::getCursorCoords(x,y,z)) - { - out.printerr("No cursor detected - please place the cursor over the location in which you wish to create a new plant.\n"); - return CR_FAILURE; - } - df::map_block *map = Maps::getTileBlock(x, y, z); - df::map_block_column *col = Maps::getBlockColumn((x / 48) * 3, (y / 48) * 3); - if (!map || !col) - { - out.printerr("Invalid location selected!\n"); - return CR_FAILURE; - } - int tx = x & 15, ty = y & 15; - int mat = tileMaterial(map->tiletype[tx][ty]); - if ((tileShape(map->tiletype[tx][ty]) != tiletype_shape::FLOOR) || ((mat != tiletype_material::SOIL) && (mat != tiletype_material::GRASS_DARK) && (mat != tiletype_material::GRASS_LIGHT))) - { - out.printerr("Plants can only be placed on dirt or grass floors!\n"); - return CR_FAILURE; - } - - int plant_id = -1; - df::plant_raw *plant_raw = NULL; - for (size_t i = 0; i < world->raws.plants.all.size(); i++) - { - plant_raw = world->raws.plants.all[i]; - if (plant_raw->id == parameters[0]) - { - plant_id = i; - break; - } - } - if (plant_id == -1) - { - out.printerr("Invalid plant ID specified!\n"); - return CR_FAILURE; - } - if (plant_raw->flags.is_set(plant_raw_flags::GRASS)) - { - out.printerr("You cannot plant grass using this command.\n"); - return CR_FAILURE; - } - - df::plant *plant = df::allocate(); - if (plant_raw->flags.is_set(plant_raw_flags::TREE)) - plant->hitpoints = 400000; - else - { - plant->hitpoints = 100000; - plant->type = df::plant_type::DRY_PLANT; - } - // for now, always set to WET_TREE for WET-permitted plants, even if they're spawned away from water - // the proper method would be to actually look for nearby water features, but it's not clear exactly how that works - if (plant_raw->flags.is_set(plant_raw_flags::WET)) { - if (plant_raw->flags.is_set(plant_raw_flags::TREE)) - plant->type = df::plant_type::WET_TREE; - else - plant->type = df::plant_type::WET_PLANT; - } - plant->material = plant_id; - plant->pos.x = x; - plant->pos.y = y; - plant->pos.z = z; - plant->update_order = rand() % 10; - - world->plants.all.push_back(plant); - switch (plant->flags.whole & 3) - { - case 0: world->plants.tree_dry.push_back(plant); break; - case 1: world->plants.tree_wet.push_back(plant); break; - case 2: world->plants.shrub_dry.push_back(plant); break; - case 3: world->plants.shrub_wet.push_back(plant); break; - } - col->plants.push_back(plant); - if (plant->type == df::plant_type::DRY_PLANT || plant->type == df::plant_type::WET_PLANT) - map->tiletype[tx][ty] = tiletype::Shrub; - else - map->tiletype[tx][ty] = tiletype::Sapling; - - return CR_OK; -} - -command_result df_plant (color_ostream &out, vector & parameters) -{ - if (parameters.size() >= 1) - { - if (parameters[0] == "grow") { - parameters.erase(parameters.begin()); - return df_grow(out, parameters); - } else if (parameters[0] == "create") { - parameters.erase(parameters.begin()); - return df_createplant(out, parameters); - } - } - return CR_WRONG_USAGE; -} - -DFhackCExport command_result plugin_init ( color_ostream &out, std::vector &commands) -{ - commands.push_back(PluginCommand( - "plant", - "Grow shrubs or trees.", - df_plant)); - - return CR_OK; -} - -DFhackCExport command_result plugin_shutdown ( color_ostream &out ) -{ - return CR_OK; -} diff --git a/plugins/regrass.cpp b/plugins/regrass.cpp index 23ffb3f406..a9e39a83b7 100644 --- a/plugins/regrass.cpp +++ b/plugins/regrass.cpp @@ -64,10 +64,10 @@ struct_identity regrass_options::_identity(sizeof(regrass_options), &df::allocat command_result df_regrass(color_ostream &out, vector ¶meters); -static bool valid_tile(color_ostream &out, regrass_options options, df::map_block *block, int x, int y) +static bool valid_tile(color_ostream &out, regrass_options options, df::map_block *block, int tx, int ty) { // Is valid tile for regrass - auto des = block->designation[x][y]; - auto tt = block->tiletype[x][y]; + auto des = block->designation[tx][ty]; + auto tt = block->tiletype[tx][ty]; auto shape = tileShape(tt); auto mat = tileMaterial(tt); auto spec = tileSpecial(tt); @@ -82,7 +82,7 @@ static bool valid_tile(color_ostream &out, regrass_options options, df::map_bloc else if (tt == tiletype::TreeTrunkPillar || tt == tiletype::TreeTrunkInterior || (tt >= tiletype::TreeTrunkThickN && tt <= tiletype::TreeTrunkThickSE)) { // Trees can have grass for ground level tiles - auto p = df::coord(block->map_pos.x + x, block->map_pos.y + y, block->map_pos.z); + auto p = df::coord(block->map_pos.x + tx, block->map_pos.y + ty, block->map_pos.z); auto plant = Maps::getPlantAtTile(p); if (plant && plant->pos.z == p.z) { @@ -107,14 +107,14 @@ static bool valid_tile(color_ostream &out, regrass_options options, df::map_bloc DEBUG(log, out).print("Invalid tile: Shape\n"); return false; } - else if (block->occupancy[x][y].bits.building > + else if (block->occupancy[tx][ty].bits.building > (options.buildings ? tile_building_occ::Passable : tile_building_occ::None)) { // Avoid stockpiles and planned/passable buildings unless enabled DEBUG(log, out).print("Invalid tile: Building (%s)\n", - ENUM_KEY_STR(tile_building_occ, block->occupancy[x][y].bits.building).c_str()); + ENUM_KEY_STR(tile_building_occ, block->occupancy[tx][ty].bits.building).c_str()); return false; } - else if (!options.force && block->occupancy[x][y].bits.no_grow) + else if (!options.force && block->occupancy[tx][ty].bits.no_grow) { DEBUG(log, out).print("Invalid tile: no_grow\n"); return false; @@ -158,7 +158,7 @@ static bool valid_tile(color_ostream &out, regrass_options options, df::map_bloc auto &ms_ev = *(df::block_square_event_material_spatterst *)blev; if (ms_ev.mat_type == builtin_mats::MUD) { - if (ms_ev.amount[x][y] > 0) + if (ms_ev.amount[tx][ty] > 0) { DEBUG(log, out).print("Valid tile: Muddy stone\n"); return true; @@ -176,18 +176,18 @@ static bool valid_tile(color_ostream &out, regrass_options options, df::map_bloc return false; } -static vector grasses_for_tile(color_ostream &out, df::map_block *block, int x, int y) +static vector grasses_for_tile(color_ostream &out, df::map_block *block, int tx, int ty) { // Return sorted vector of valid grass ids vector grasses; - if (block->occupancy[x][y].bits.no_grow) + if (block->occupancy[tx][ty].bits.no_grow) { DEBUG(log, out).print("Skipping grass collection: no_grow\n"); return grasses; } DEBUG(log, out).print("Collecting grasses...\n"); - if (block->designation[x][y].bits.subterranean) + if (block->designation[tx][ty].bits.subterranean) { for (auto p_raw : world->raws.plants.grasses) { // Sorted by df::plant_raw::index @@ -200,7 +200,7 @@ static vector grasses_for_tile(color_ostream &out, df::map_block *block } else // Above ground { - auto rgn_pos = Maps::getBlockTileBiomeRgn(block, df::coord2d(x, y)); // x&15 is okay + auto rgn_pos = Maps::getBlockTileBiomeRgn(block, df::coord2d(tx, ty)); if (!rgn_pos.isValid()) { // No biome (happens in sky) @@ -233,9 +233,9 @@ static vector grasses_for_tile(color_ostream &out, df::map_block *block return grasses; } -static bool regrass_events(color_ostream &out, const regrass_options &options, df::map_block *block, int x, int y) +static bool regrass_events(color_ostream &out, const regrass_options &options, df::map_block *block, int tx, int ty) { // Modify grass block events - if (!valid_tile(out, options, block, x, y)) + if (!valid_tile(out, options, block, tx, ty)) return false; bool success = false; @@ -248,23 +248,23 @@ static bool regrass_events(color_ostream &out, const regrass_options &options, d if (options.max_grass) { // Refill all - gr_ev.amount[x][y] = 100; + gr_ev.amount[tx][ty] = 100; success = true; } - else if (gr_ev.amount[x][y] > 0) + else if (gr_ev.amount[tx][ty] > 0) { // Refill first non-zero grass - gr_ev.amount[x][y] = 100; + gr_ev.amount[tx][ty] = 100; DEBUG(log, out).print("Refilled existing grass.\n"); return true; } } - auto valid_grasses = grasses_for_tile(out, block, x, y); + auto valid_grasses = grasses_for_tile(out, block, tx, ty); if (options.force && valid_grasses.empty()) { DEBUG(log, out).print("Forcing grass.\n"); valid_grasses.push_back(options.forced_plant); - block->occupancy[x][y].bits.no_grow = false; + block->occupancy[tx][ty].bits.no_grow = false; } if (options.force || (options.new_grass && !valid_grasses.empty())) @@ -286,7 +286,7 @@ static bool regrass_events(color_ostream &out, const regrass_options &options, d if (options.max_grass) { // Initialize tile as full - gr_ev->amount[x][y] = 100; + gr_ev->amount[tx][ty] = 100; success = true; } } @@ -313,7 +313,7 @@ static bool regrass_events(color_ostream &out, const regrass_options &options, d auto gr_ev = vector_get_random(temp); if (gr_ev) { - gr_ev->amount[x][y] = 100; + gr_ev->amount[tx][ty] = 100; DEBUG(log, out).print("Random regrass plant index %d\n", gr_ev->plant_index); return true; } @@ -322,20 +322,20 @@ static bool regrass_events(color_ostream &out, const regrass_options &options, d return false; } -int regrass_tile(color_ostream &out, const regrass_options &options, df::map_block *block, int x, int y) +int regrass_tile(color_ostream &out, const regrass_options &options, df::map_block *block, int tx, int ty) { // Regrass single tile. Return 1 if tile success, else 0 CHECK_NULL_POINTER(block); - if (!is_valid_tile_coord(df::coord2d(x, y))) + if (!is_valid_tile_coord(df::coord2d(tx, ty))) { - out.printerr("(%d, %d) not in range 0-15!\n", x, y); + out.printerr("(%d, %d) not in range 0-15!\n", tx, ty); return 0; } - DEBUG(log, out).print("Regrass tile (%d, %d, %d)\n", block->map_pos.x + x, block->map_pos.y + y, block->map_pos.z); - if (!regrass_events(out, options, block, x, y)) + DEBUG(log, out).print("Regrass tile (%d, %d, %d)\n", block->map_pos.x + tx, block->map_pos.y + ty, block->map_pos.z); + if (!regrass_events(out, options, block, tx, ty)) return 0; - auto tt = block->tiletype[x][y]; + auto tt = block->tiletype[tx][ty]; auto mat = tileMaterial(tt); auto shape = tileShape(tt); @@ -347,7 +347,7 @@ int regrass_tile(color_ostream &out, const regrass_options &options, df::map_blo DEBUG(log, out).print("Tiletype no change.\n"); return 1; } - else if (mat == tiletype_material::STONE || + /*else if (mat == tiletype_material::STONE || // DF doesn't seem to remove mud mat == tiletype_material::LAVA_STONE || mat == tiletype_material::MINERAL) { // Muddy non-feature stone @@ -359,24 +359,24 @@ int regrass_tile(color_ostream &out, const regrass_options &options, df::map_blo auto &ms_ev = *(df::block_square_event_material_spatterst *)blev; if (ms_ev.mat_type == builtin_mats::MUD) { - ms_ev.amount[x][y] = 0; + ms_ev.amount[tx][ty] = 0; DEBUG(log, out).print("Removed tile mud.\n"); break; } } - } + }*/ if (shape == tiletype_shape::FLOOR) { // Handle random variant, ashes DEBUG(log, out).print("Tiletype to random grass floor.\n"); - block->tiletype[x][y] = findRandomVariant((rand() & 1) ? tiletype::GrassLightFloor1 : tiletype::GrassDarkFloor1); + block->tiletype[tx][ty] = findRandomVariant((rand() & 1) ? tiletype::GrassLightFloor1 : tiletype::GrassDarkFloor1); } else { auto new_mat = (rand() & 1) ? tiletype_material::GRASS_LIGHT : tiletype_material::GRASS_DARK; auto new_tt = findTileType(shape, new_mat, tiletype_variant::NONE, tiletype_special::NONE, nullptr); DEBUG(log, out).print("Tiletype to %s.\n", ENUM_KEY_STR(tiletype, new_tt).c_str()); - block->tiletype[x][y] = new_tt; + block->tiletype[tx][ty] = new_tt; } return 1; @@ -387,10 +387,10 @@ int regrass_block(color_ostream &out, const regrass_options &options, df::map_bl CHECK_NULL_POINTER(block); int count = 0; - for (int x = 0; x < 16; x++) + for (int tx = 0; tx < 16; tx++) { - for (int y = 0; y < 16; y++) - count += regrass_tile(out, options, block, x, y); + for (int ty = 0; ty < 16; ty++) + count += regrass_tile(out, options, block, tx, ty); } return count; @@ -493,7 +493,7 @@ command_result df_regrass(color_ostream &out, vector ¶meters) else if (options.forced_plant == -2) { // Print all grass raw ids for (auto p_raw : world->raws.plants.grasses) - out.print("%s\n", p_raw->id.c_str()); + out.print("%d: %s\n", p_raw->index, p_raw->id.c_str()); return CR_OK; } @@ -503,12 +503,12 @@ command_result df_regrass(color_ostream &out, vector ¶meters) if (options.block && options.zlevel) { - out.printerr("Choose only block or zlevel!\n"); + out.printerr("Choose only --block or --zlevel!\n"); return CR_WRONG_USAGE; } else if (options.block && (!pos_1.isValid() || pos_2.isValid())) { - out.printerr("Attempt to regrass block with inappropriate pos!\n"); + out.printerr("Invalid pos for --block (or used more than one!)\n"); return CR_WRONG_USAGE; } else if (!Core::getInstance().isMapLoaded()) @@ -522,10 +522,17 @@ command_result df_regrass(color_ostream &out, vector ¶meters) DEBUG(log, out).print("forced_plant = %d\n", options.forced_plant); auto p_raw = vector_get(world->raws.plants.all, options.forced_plant); if (p_raw) - DEBUG(log, out).print("Forced plant_raw = %s\n", p_raw->id.c_str()); + { + DEBUG(log, out).print("Forced plant raw: %s\n", p_raw->id.c_str()); + if (!p_raw->flags.is_set(plant_raw_flags::GRASS)) + { + out.printerr("Plant raw wasn't grass: %d (%s)\n", options.forced_plant, p_raw->id.c_str()); + return CR_FAILURE; + } + } else { - out.printerr("Plant raw not found for force regrass!\n"); + out.printerr("Plant raw not found for --force: %d\n", options.forced_plant); return CR_FAILURE; } }