Closed Rekov3D closed 4 years ago
I'm not entirely familiar with this, so to clarify - are the dwarves collecting growths (other than berries) or nothing at all? Does designating the bilberry bushes for gathering manually lead to the same result? If that's the case, I'm not sure if there's a way to tell in advance whether a plant will produce berries when harvested, but we can investigate.
No. If you manually designate the bilberry bushes with d - p they aren't selected unless there is a fruit there to collect. Similarly if you put these plants in a gather zone, dwarves will not attempt to harvest them unless there's a fruit or whatever.
From the wiki:
Edible or otherwise usable growths should have [STOCKPILE_PLANT_GROWTH] in their material definitions for proper stockpiling. This also lets them be collected from plant gathering and farming jobs.
I don't really know if they are 'collecting' the leaf/flower growths to be honest. Nothing is produced out of it either way.
I'm roughly trying to repeat what Patrik Lundell told me on the forum. His explanation might be more helpful.
Aha, thanks for the forum link. So getplants is making designations that aren't even possible to make in vanilla, then. I'm not sure how complicated this is to address, but yeah, it's definitely a DFHack bug in that case.
I've thought a bit about the problem, and came up with a few complications that will have to be considered. The Bilberry case is reasonably straightforward, as the basic plant can't be used for anything, and thus there's no vanilla reason to pick a Bilberry at that stage. However:
It seems to me that the [STOCKPILE_PLANT_GROWTH] raw tag referenced above gets translated into the stockpile_growths and stockpile_growth_flags arrays in the raw structures inside DF. However, these arrays do not include the structural part of the plant, so the determination of whether to pick that part has to use other info.
Just located the code DF itself uses for checking if a plant is harvestable (and whether it will allow you to designate it), and it appears to use the following criteria:
I guess the best approach would be to apply the same rules DF uses and leave it up to the players to issue the command at the correct times if they want to get specific time limited parts (just as with the vanilla DF designations, it will lead to confusion for things like strawberries, but at least it's a standard confusion...).
Edit:
I've tried to implement the logic quietust provided, and while I think it works (I've performed some tests, comparing its selections with those made by DF, it's not exactly pretty to match up "growths" with "material" using loops and string comparisons so it should probably be improved/corrected.
I've added a couple of #includes, for "df/plant_growth.h" and #include "modules/Materials.h", if I remember correctly (more lines than those are marked as "changed", but I don't think there's any net change in them).
REQUIRE_GLOBAL(cur_year_tick);
bool markPlant(const df::plant *plant)
{
const df::plant_raw *plant_raw = world->raws.plants.all[plant->material];
const DFHack::MaterialInfo basic_mat = DFHack::MaterialInfo(plant_raw->material_defs.type_basic_mat, plant_raw->material_defs.idx_basic_mat);
if (plant_raw->flags.is_set(plant_raw_flags::TREE))
{
return Designations::markPlant(plant);
}
if (basic_mat.material->flags.is_set(material_flags::EDIBLE_RAW) ||
basic_mat.material->flags.is_set(material_flags::EDIBLE_COOKED))
{
return Designations::markPlant(plant);
}
if (plant_raw->flags.is_set(plant_raw_flags::THREAD) ||
plant_raw->flags.is_set(plant_raw_flags::MILL) ||
plant_raw->flags.is_set(plant_raw_flags::EXTRACT_VIAL) ||
plant_raw->flags.is_set(plant_raw_flags::EXTRACT_BARREL) ||
plant_raw->flags.is_set(plant_raw_flags::EXTRACT_STILL_VIAL))
{
return Designations::markPlant(plant);
}
if (basic_mat.material->reaction_product.id.size() > 0 ||
basic_mat.material->reaction_class.size() > 0)
{
return Designations::markPlant(plant);
}
for (auto i = 0; i < plant_raw->material.size(); i++)
{
if (plant_raw->material[i]->flags.is_set(material_flags::SEED_MAT) &&
(plant_raw->material[i]->flags.is_set(material_flags::EDIBLE_RAW) ||
plant_raw->material[i]->flags.is_set(material_flags::EDIBLE_COOKED)))
{
for (auto k = 0; k < plant_raw->growths.size(); k++)
{
if (plant_raw->growths[k]->behavior.bits.has_seed &&
*cur_year_tick >= plant_raw->growths[k]->timing_1 &&
(plant_raw->growths[k]->timing_2 == -1 ||
*cur_year_tick <= plant_raw->growths[k]->timing_2)) {
return Designations::markPlant(plant);
}
}
}
if (plant_raw->material[i]->flags.is_set(material_flags::LEAF_MAT)) // The flag really means STOCKPILE_PLANT_GROWTH
{
for (auto k = 0; k < plant_raw->growths.size(); k++)
{
if (plant_raw->growths[k]->id == plant_raw->material[i]->id &&
*cur_year_tick >= plant_raw->growths[k]->timing_1 &&
(plant_raw->growths[k]->timing_2 == -1 ||
*cur_year_tick <= plant_raw->growths[k]->timing_2)) {
return Designations::markPlant(plant);
}
}
}
}
return false;
}
has been added after "REQUIRE_GLOBAL(world);", and the new function is called instead of "Designations::markPlants" towards the end of the "df_getplants" function.
By the way: I guess there's no way to introduce an alias of "STOCKPILE_PLANT_GROWTH" for "LEAF_MAT" to get a named constant with a suitable name beside the obsolete one in the "material_flags" namespace, since it seems to be too cumbersome to replace the obsolete constant? It feels wrong to introduce new usages of the inappropriately named constant...
Aliases are not possible, but renaming it is easy. What isn't easy is ensuring that everything that used the old name is updated, but if the old name was wrong anyway, I'm fine with renaming it.
Well, just renaming the constant in the definition doesn't count as performing a renaming in my book: The task should involve locating and changing usages as well.
Something that bothers me with the implementation above is that it doesn't actually make use of the plant's growth state, which may or may not match how DF treats it, i.e. if a shrub is spawned, it assumes the appropriate time-of-year growths are present immediately as well. There's a "grow_counter" field in the individual plant, but I don't know if it is used for shrubs (it is for trees). If there is no actual dependency on the plant itself, it might be better to make the evaluation of whether it is ready for picking on the species level (there are a few hundred plants, but potentially thousands of shrubs on an embark).
Edit: A text search of the dfhack git download (by Windows, which isn't to be trusted), found "LEAF_MAT" only in the defining XML file, my modified getplants, and files generated by the XML processing, so a name change should be trivial. I tried to find matches in the hack directory of my LNP DF installation as well, and found nothing there either (as expected). Hm, I was sure I've seen quietust indicate changing the flag was too cumbersome somewhere, but am unable to locate it. Regardless, it seems quietust did actually change the flag 3 days ago, based on the editing history of the post above, so I guess we can close this silly tangent.
Looks like there are a couple occurrences in scripts: https://github.com/DFHack/scripts/search?utf8=%E2%9C%93&q=leaf_mat&type= (but I only see the definition in df-structures, and nothing in dfhack)
Your plant growth checks aren't quite right - the seed one shouldn't look at SEED_MAT or behavior.bits.has_seed, but should instead check the growth's item_type (and compare to SEEDS / PLANT_GROWTH) and then load the material specified by mat_type/mat_index (which might not even belong to that plant) to check the necessary flags.
It's probably best to iterate across the growths array once (to avoid duplicating the time validity check), since that's what DF itself does.
Also, regarding LEAF_MAT, that's what it used to be called back in version 0.34.11 - Toady renamed it when he added plant growths in 0.40.
Thanks for the feedback, quietust.
I usually assume the need for a change of a name to be caused by DF changing the use, rather than the person originally naming it having made an error. That's why I try to use the term "obsolete" rather than "incorrect".
Edit:
I probably misunderstand quietust, because checking a growth's item_type for SEEDS causes the checks to fail to find FRUIT/POD containing edible seeds (all growths seem to be of the item_type PLANT_GROWTH in the small sample I've looked at).
The trouble plants seem to be:
BAMBARA_GROUND_NUT, STRING_BEAN, BROAD_BEAN, LENTIL, MUNG_BEAN, PEA, PEANUT, RED_BEAN, SOYBEAN, and URAD_BEAN, (I have missed some in that list, e.g. COWPEA), i.e. the ones that have inedible growths containing edible seeds. I recall having heard of trouble with collection of those kinds of plants, but as you can't make booze out of them I haven't thought about it further, so I don't know if DF can't collect them, can't designate them (but collect them once designated by DFHack), or if there was something else entirely.
Thus, at the moment my issue seems to be to fulfill the "at least one time-valid plant growth produces SEEDS items whose material is EDIBLE_RAW or EDIBLE_COOKED" criterion. I can find the growth, but its material isn't edible, nor does it refer to any seed material. The only lead I have is the
Hm, changing the code to use the .has_seed flag and get the material from
I've ended up with this code (it ignores the edible seed criterion that doesn't seem to work, and adds a little feedback about things that are out of season, since we need to check the time internally anyway):
// (un)designate matching plants for gathering/cutting
#include <set>
#include "Core.h"
#include "Console.h"
#include "Export.h"
#include "PluginManager.h"
#include "DataDefs.h"
#include "TileTypes.h"
#include "df/map_block.h"
#include "df/plant.h"
#include "df/plant_growth.h"
#include "df/plant_raw.h"
#include "df/tile_dig_designation.h"
#include "df/world.h"
#include "modules/Designations.h"
#include "modules/Maps.h"
#include "modules/Materials.h"
using std::string;
using std::vector;
using std::set;
using namespace DFHack;
using namespace df::enums;
DFHACK_PLUGIN("getplants");
REQUIRE_GLOBAL(world);
REQUIRE_GLOBAL(cur_year_tick);
enum class selectability {
Selectable,
Grass,
Nonselectable,
OutOfSeason,
Unselected
};
//selectability selectablePlant(color_ostream &out, const df::plant_raw *plant)
selectability selectablePlant(const df::plant_raw *plant)
{
const DFHack::MaterialInfo basic_mat = DFHack::MaterialInfo(plant->material_defs.type_basic_mat, plant->material_defs.idx_basic_mat);
bool outOfSeason = false;
if (plant->flags.is_set(plant_raw_flags::TREE))
{
// out.print("%s is a selectable tree\n", plant->id.c_str());
return selectability::Selectable;
}
else if (plant->flags.is_set(plant_raw_flags::GRASS))
{
// out.print("%s is a non selectable Grass\n", plant->id.c_str());
return selectability::Grass;
}
if (basic_mat.material->flags.is_set(material_flags::EDIBLE_RAW) ||
basic_mat.material->flags.is_set(material_flags::EDIBLE_COOKED))
{
// out.print("%s is a edible\n", plant->id.c_str());
return selectability::Selectable;
}
if (plant->flags.is_set(plant_raw_flags::THREAD) ||
plant->flags.is_set(plant_raw_flags::MILL) ||
plant->flags.is_set(plant_raw_flags::EXTRACT_VIAL) ||
plant->flags.is_set(plant_raw_flags::EXTRACT_BARREL) ||
plant->flags.is_set(plant_raw_flags::EXTRACT_STILL_VIAL))
{
// out.print("%s is thread/mill/extract\n", plant->id.c_str());
return selectability::Selectable;
}
if (basic_mat.material->reaction_product.id.size() > 0 ||
basic_mat.material->reaction_class.size() > 0)
{
// out.print("%s has a reaction\n", plant->id.c_str());
return selectability::Selectable;
}
for (auto i = 0; i < plant->growths.size(); i++)
{
if (plant->growths[i]->item_type == df::item_type::PLANT_GROWTH)
{
const DFHack::MaterialInfo growth_mat = DFHack::MaterialInfo(plant->growths[i]->mat_type, plant->growths[i]->mat_index);
if (growth_mat.material->flags.is_set(material_flags::STOCKPILE_PLANT_GROWTH))
{
if (*cur_year_tick >= plant->growths[i]->timing_1 &&
(plant->growths[i]->timing_2 == -1 ||
*cur_year_tick <= plant->growths[i]->timing_2))
{
// out.print("%s has a stockpile growth\n", plant->id.c_str());
return selectability::Selectable;
}
else
{
outOfSeason = true;
}
}
/* else if (plant->growths[i]->behavior.bits.has_seed) // This code designates beans, etc. when DF doesn't, but plant gatherers still fail to collect anything.
{
const DFHack::MaterialInfo seed_mat = DFHack::MaterialInfo(plant->material_defs.type_seed, plant->material_defs.idx_seed);
if (seed_mat.material->flags.is_set(material_flags::EDIBLE_RAW) ||
seed_mat.material->flags.is_set(material_flags::EDIBLE_COOKED))
{
if (*cur_year_tick >= plant->growths[i]->timing_1 &&
(plant->growths[i]->timing_2 == -1 ||
*cur_year_tick <= plant->growths[i]->timing_2))
{
return selectability::Selectable;
}
else
{
outOfSeason = true;
}
}
} */
}
}
if (outOfSeason)
{
// out.print("%s has an out of season growth\n", plant->id.c_str());
return selectability::OutOfSeason;
}
else
{
// out.printerr("%s cannot be gathered\n", plant->id.c_str());
return selectability::Nonselectable;
}
}
command_result df_getplants (color_ostream &out, vector <string> & parameters)
{
string plantMatStr = "";
std::vector<selectability> plantSelections;
set<string> plantNames;
bool deselect = false, exclude = false, treesonly = false, shrubsonly = false, all = false;
int count = 0;
plantSelections.resize(world->raws.plants.all.size());
for (auto i = 0; i < plantSelections.size(); i++)
{
plantSelections[i] = selectability::Unselected;
}
bool anyPlantsSelected = false;
for (size_t i = 0; i < parameters.size(); i++)
{
if(parameters[i] == "help" || parameters[i] == "?")
return CR_WRONG_USAGE;
else if(parameters[i] == "-t")
treesonly = true;
else if(parameters[i] == "-s")
shrubsonly = true;
else if(parameters[i] == "-c")
deselect = true;
else if(parameters[i] == "-x")
exclude = true;
else if(parameters[i] == "-a")
all = true;
else
plantNames.insert(parameters[i]);
}
if (treesonly && shrubsonly)
{
out.printerr("Cannot specify both -t and -s at the same time!\n");
return CR_WRONG_USAGE;
}
if (all && exclude)
{
out.printerr("Cannot specify both -a and -x at the same time!\n");
return CR_WRONG_USAGE;
}
if (all && plantNames.size())
{
out.printerr("Cannot specify -a along with plant IDs!\n");
return CR_WRONG_USAGE;
}
CoreSuspender suspend;
for (size_t i = 0; i < world->raws.plants.all.size(); i++)
{
df::plant_raw *plant = world->raws.plants.all[i];
if (all)
{
// plantSelections[i] = selectablePlant(out, plant);
plantSelections[i] = selectablePlant(plant);
}
else if (plantNames.find(plant->id) != plantNames.end())
{
plantNames.erase(plant->id);
// plantSelections[i] = selectablePlant(out, plant);
plantSelections[i] = selectablePlant(plant);
switch (plantSelections[i])
{
case selectability::Grass:
{
out.printerr("%s is a Grass, and those can not be gathered\n", plant->id.c_str());
break;
}
case selectability::Nonselectable:
{
out.printerr("%s does not have any parts that can be gathered\n", plant->id.c_str());
break;
}
case selectability::OutOfSeason:
{
out.printerr("%s is out of season, with nothing that can be gathered now\n", plant->id.c_str());
break;
}
case selectability::Selectable:
break;
case selectability::Unselected:
break; // We won't get to this option
}
}
}
if (plantNames.size() > 0)
{
out.printerr("Invalid plant ID(s):");
for (set<string>::const_iterator it = plantNames.begin(); it != plantNames.end(); it++)
out.printerr(" %s", it->c_str());
out.printerr("\n");
return CR_FAILURE;
}
for (auto i = 0; i < plantSelections.size(); i++)
{
if (plantSelections[i] == selectability::OutOfSeason ||
plantSelections[i] == selectability::Selectable)
{
anyPlantsSelected = true;
break;
}
}
if (!anyPlantsSelected)
{
out.print("Valid plant IDs:\n");
for (size_t i = 0; i < world->raws.plants.all.size(); i++)
{
df::plant_raw *plant = world->raws.plants.all[i];
// switch (selectablePlant(out, plant))
switch (selectablePlant(plant))
{
case selectability::Grass:
case selectability::Nonselectable:
continue;
case selectability::OutOfSeason:
{
out.print("* (shrub) %s - %s is out of season\n", plant->id.c_str(), plant->name.c_str());
break;
}
case selectability::Selectable:
{
out.print("* (%s) %s - %s\n", plant->flags.is_set(plant_raw_flags::TREE) ? "tree" : "shrub", plant->id.c_str(), plant->name.c_str());
break;
}
case selectability::Unselected: // Should never get this alternative
break;
}
}
return CR_OK;
}
count = 0;
for (size_t i = 0; i < world->plants.all.size(); i++)
{
const df::plant *plant = world->plants.all[i];
df::map_block *cur = Maps::getTileBlock(plant->pos);
bool dirty = false;
int x = plant->pos.x % 16;
int y = plant->pos.y % 16;
if (plantSelections[plant->material] == selectability::OutOfSeason ||
plantSelections[plant->material] == selectability::Selectable)
{
if (exclude ||
plantSelections[plant->material] == selectability::OutOfSeason)
continue;
}
else
{
if (!exclude)
continue;
}
df::tiletype_shape shape = tileShape(cur->tiletype[x][y]);
df::tiletype_material material = tileMaterial(cur->tiletype[x][y]);
df::tiletype_special special = tileSpecial(cur->tiletype[x][y]);
if (plant->flags.bits.is_shrub && (treesonly || !(shape == tiletype_shape::SHRUB && special != tiletype_special::DEAD)))
continue;
if (!plant->flags.bits.is_shrub && (shrubsonly || !(material == tiletype_material::TREE)))
continue;
if (cur->designation[x][y].bits.hidden)
continue;
if (deselect && Designations::unmarkPlant(plant))
{
++count;
}
if (!deselect && Designations::markPlant(plant))
{
++count;
}
}
if (count)
out.print("Updated %d plant designations.\n", count);
return CR_OK;
}
DFhackCExport command_result plugin_init ( color_ostream &out, vector <PluginCommand> &commands)
{
commands.push_back(PluginCommand(
"getplants", "Cut down trees or gather shrubs by ID",
df_getplants, false,
" Specify the types of trees to cut down and/or shrubs to gather by their\n"
" plant IDs, separated by spaces.\n"
"Options:\n"
" -t - Select trees only (exclude shrubs)\n"
" -s - Select shrubs only (exclude trees)\n"
" -c - Clear designations instead of setting them\n"
" -x - Apply selected action to all plants except those specified\n"
" -a - Select every type of plant (obeys -t/-s)\n"
"Specifying both -t and -s will have no effect.\n"
"If no plant IDs are specified, all valid plant IDs will be listed.\n"
));
return CR_OK;
}
DFhackCExport command_result plugin_shutdown ( color_ostream &out )
{
return CR_OK;
}
Edit 2: I ran a script checking the item_type of all growths, and SEEDS does indeed exist. However, all of them are nuts (Macadamia, Almond, Ginko, Hazel, Pecan, Walnut, Oak, Candlenut), and so won't apply to any vanilla shrubs (but could apply to modded raws, of course, and thus ought to be checked).
It seems the bug regarding beans etc. is number 0006940 (a rare instance where a bug tracker search actually found things related to the search term ["bean" in this case] instead of page after page of completely unrelated reports that don't seem to contain the search term anywhere). Beans seem to not work for farming either, if I read the report correctly.
Edit 3: Well, I just found that DF does know to distinguish present growths from absent ones. DF refrained from designating one instance of Bitter Melon Vines, while designating all other Bitter Melons (at least one of which has Bitter Melon Leaves and Bitter Melon in addition to the Vines). Thus, another thing to look for.
I've also tried to figure out how DF determines where seeds are supposed to come from (i.e. which plant part has to be collected in order to get seeds from it). Most plants have SEED_MAT reactions, some (trees) have growths of the item_type SEEDS, and some have growths with behavior.has_seed, but then there is a bunch that has none of those, and at least some of those can't get the seeds from the STRUCTURAL_MAT, as it's inedible. The trouble shrubs (a number of trees also end up in this category), regardless of which parts are edible, are CABBAGE, CELERY, CHICORY, GARDEN_CRESS, GARLIC, LEEK, LETTUCE, ONION, RHUBARB, SPINACH, TARO, LESSER_YAM, LONG_YAM, PURPLE_YAM, MUSHROOM_CUP_DIMPLE, WEED_BLADE, and ROOT_HIDE. Some of these would need to get the seeds from their underground parts, as the above ground ones can't be used. The reason I'm looking at this is to try to add a "-f" switch ("farming", as "-s" is already taken) to specifically designate shrubs to get seeds for farming.
Edit 4: I've looked at LETTUCE, which produces both an edible structure and an edible leaf, and seen that dorfs eating the leaves (in an embark) do not leave seeds behind, while those who eat the structure part do. I moved on to SPINACH, and found that the leaf (the only edible part) does not produce any seed. Thus, it seems it shouldn't be possible to grow those except from imported seeds. Most forms of Yam is edible only when cooked, so, again, it seems they should be possible to grow only from imported seeds. My conclusion is that the trouble group above implicitly produces seeds from their structural part, and if there is no valid process (milling, brewing, etc.) for that part and it can't be eaten raw, you can't grow it sustainably.
I've made no progress regarding detection of growths. Tile sets (Phoebus, in my case) can display that a tree has been picked partially, and DF can show that there's fallen fruit on the ground, but I haven't managed to find any map or plant data that shows any of: growths being present, picked, or dropped.
@quietust Does your ability to look at the DF code allow you to figure out where we might need to look for growths that can be picked/gathered? The plant raws provides the timing for when growths may be present, but after they're picked the plants they grew on still remain if the plants themselves (the structure part) are not useful (e.g. bitter melon), DF is clearly capable of displaying the fruit/nuts on trees, removing them from display as they're picked, and DF also refuses to designate already picked shrubs for picking, so the info is clearly there, somewhere. I suspect the growths are stored in some unmapped structure, although it's obviously not possible to rule out a failure on my part to find something "hiding" in plain sight.
In version 0.47.04 on 64-bit Windows, the function for designating Shrub tiles for plant gathering is located at address 0x140DE87C0. My analysis above is based entirely on that function, but I omitted some details that I wasn't able to figure out at the time - specifically, how the game decides that growths are "time-valid".
The time validity is based on cur_year_tick plus/minus a random value (whose seed is based on the X and Y coordinates). I'm not exactly certain about the math involved, but that particular code is at 0x140DE89D1 if you want to try to analyze it in IDA or Ghidra.
There's another check I previously overlooked that involves records inside world.world_data.object_data
, specifically the lists unk_c0
/unk_d0
/unk_e0
(all int16, likely containing X/Y/Z coordinates) and unk_f0
/unk_100
/unk_110
(all int32, with the 2nd being related to growth Density and the 3rd being a Year). These could very well be how it keeps track of which tiles have been harvested.
Thanks, quietust. I'll try to see if I can get anything useful out of the structures you've pointed out. object_data is currently a dark corner of the structures, so anything hiding there would be hard to find without this kind of hint.
I'll most likely not delve into code analysis, though, but it's definitely useful to know there is randomness involved (which is visible in game play, as you can see that trees in particular change different tiles at different times over a comparatively short period).
Edit: It looks like the Id field is the world MLT coordinates of an MLT, unk_c0/unk_d0 the x/y coordinates within that tile, with unk_e0 being some weird coordinate with 100 = surface (on my flat embark anyway), and 101/102 being up in trees (the cursor z coordinates stop at 48 or something like that, at the top of the air, so it's not the normal z coordinate). unk_f0 seems to be the subtype (or growth index, if you like). Picking a horned melon resulted in an entry, while a strawberry plant did not (in the first case the structure is left behind, while in the second the whole plant is taken).
Edit2: I don't think the addresses specified are those within the DF exe, but rather addresses of a debugging session, as they're far too large to fit within the 19 MB of the exe, so they're probably of little use without a known base address.
Those addresses are indeed within the program's address space, assuming the default base address of 0x140000000.
Thanks. IDA did not display whatever base address it might be using, using a base of 0, and I'm not sure it remains the same between different OS'. At least the pointers end up pointing to vastly different address ranges between DF invocations, but that might be for other reasons.
I've done a bit more digging into the code for the "random" time variance on each tile, and it seems to be based on the following formula: variance = ((435522653 - (((Y_coord + 3) * X_coord + 5) * ((Y_coord + 7) * Y_coord * 400181475 + 289700012))) & 0x3FFFFFFF) % 2000
.
As far as I can tell, the X and Y coordinates are relative to your entire embark, not the map_block
in which the plants are located. No, I don't know how that works in Adventurer mode, where map_block
s are continuously created and destroyed around you.
Also, no, I don't know where any of those big numbers came from or what they mean, but Ghidra managed to come up with the exact same equation so I'm pretty sure it's correct. That variance gets added to cur_year_tick
(mod 403200), and the resulting timestamp is compared to the growth timing values to see if they're valid.
If you use getplants to designate shrubs for harvest, it will do so even if there is nothing currently there to harvest. This occurs when the only usable components of the plants are growths.
Steps to reproduce: 1: Embark on a site with bilberrys 2: getplants -s BILBERRY 3: The dwarves will go around 'harvesting' bilberries. The shrubs will be destroyed. No bilberry fruits are actually collected.