From 813d8d912b3a73cc1f4ed19d2b4c0fb6da5704b3 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Aug 2018 09:16:52 -0500 Subject: [PATCH 1/7] Convert autofarm from ruby to C++ Reimplement the Ruby autofarm script as a C++ plugin --- plugins/devel/CMakeLists.txt | 1 + plugins/devel/autofarm.cpp | 406 +++++++++++++++++++++++++++++++++++ 2 files changed, 407 insertions(+) create mode 100644 plugins/devel/autofarm.cpp diff --git a/plugins/devel/CMakeLists.txt b/plugins/devel/CMakeLists.txt index 4fb4b5cf5..be2bdac98 100644 --- a/plugins/devel/CMakeLists.txt +++ b/plugins/devel/CMakeLists.txt @@ -3,6 +3,7 @@ DFHACK_PLUGIN(vectors vectors.cpp) endif() ADD_DEFINITIONS(-DDEV_PLUGIN) +DFHACK_PLUGIN(autofarm autofarm.cpp) DFHACK_PLUGIN(buildprobe buildprobe.cpp) DFHACK_PLUGIN(color-dfhack-text color-dfhack-text.cpp) DFHACK_PLUGIN(counters counters.cpp) diff --git a/plugins/devel/autofarm.cpp b/plugins/devel/autofarm.cpp new file mode 100644 index 000000000..8a79d3f61 --- /dev/null +++ b/plugins/devel/autofarm.cpp @@ -0,0 +1,406 @@ +#include "Core.h" +#include "Console.h" +#include "Export.h" +#include "PluginManager.h" + +#include "DataDefs.h" +#include "df/world.h" +#include "df/ui.h" +#include "df/building_type.h" +#include "df/building_farmplotst.h" +#include "df/buildings_other_id.h" +#include "df/global_objects.h" +#include "df/item.h" +#include "df/item_plantst.h" +#include "df/items_other_id.h" +#include "df/unit.h" +#include "df/building.h" +#include "df/plant_raw.h" +#include "df/plant_raw_flags.h" +#include "df/biome_type.h" +#include "modules/Items.h" +#include "modules/Maps.h" +#include "modules/World.h" + +using std::vector; +using std::string; +using std::map; +using std::set; +using std::endl; +using namespace DFHack; +using namespace df::enums; + +using df::global::world; +using df::global::ui; + +static command_result autofarm(color_ostream &out, vector & parameters); + +DFHACK_PLUGIN("autofarm"); + +DFHACK_PLUGIN_IS_ENABLED(enabled); + +static void process(color_ostream &out) +{ + CoreSuspender suspend; + +} + +const char *tagline = "Automatically handle crop selection in farm plots based on current plant stocks."; +const char *usage = ( + "autofarm enable\n" + "autofarm default 30\n" + "autofarm threshold 150 helmet_plump tail_pig\n" + ); + +class AutoFarm { + +private: + map thresholds; + int defaultThreshold = 50; + + map lastCounts; + +public: + void initialize() + { + thresholds.clear(); + defaultThreshold = 50; + + lastCounts.clear(); + } + + void setThreshold(int id, int val) + { + thresholds[id] = val; + } + + int getThreshold(int id) + { + return (thresholds.count(id) > 0) ? thresholds[id] : defaultThreshold; + } + + void setDefault(int val) + { + defaultThreshold = val; + } + +private: + const df::plant_raw_flags seasons[4] = { df::plant_raw_flags::SPRING, df::plant_raw_flags::SUMMER, df::plant_raw_flags::AUTUMN, df::plant_raw_flags::WINTER }; + +public: + + bool is_plantable(df::plant_raw* plant) + { + bool has_seed = plant->flags.is_set(df::plant_raw_flags::SEED); + bool is_tree = plant->flags.is_set(df::plant_raw_flags::TREE); + + int8_t season = *df::global::cur_season; + int harvest = (*df::global::cur_season_tick) + plant->growdur * 10; + bool can_plant = has_seed && !is_tree && plant->flags.is_set(seasons[season]); + while (can_plant && harvest >= 10080) { + season = (season + 1) % 4; + harvest -= 10080; + can_plant = can_plant && plant->flags.is_set(seasons[season]); + } + + return can_plant; + } + +private: + map> plantable_plants; + + const map biomeFlagMap = { + { df::plant_raw_flags::BIOME_MOUNTAIN, df::biome_type::MOUNTAIN }, + { df::plant_raw_flags::BIOME_GLACIER, df::biome_type::GLACIER }, + { df::plant_raw_flags::BIOME_TUNDRA, df::biome_type::TUNDRA }, + { df::plant_raw_flags::BIOME_SWAMP_TEMPERATE_FRESHWATER, df::biome_type::SWAMP_TEMPERATE_FRESHWATER }, + { df::plant_raw_flags::BIOME_SWAMP_TEMPERATE_SALTWATER, df::biome_type::SWAMP_TEMPERATE_SALTWATER }, + { df::plant_raw_flags::BIOME_MARSH_TEMPERATE_FRESHWATER, df::biome_type::MARSH_TEMPERATE_FRESHWATER }, + { df::plant_raw_flags::BIOME_MARSH_TEMPERATE_SALTWATER, df::biome_type::MARSH_TEMPERATE_SALTWATER }, + { df::plant_raw_flags::BIOME_SWAMP_TROPICAL_FRESHWATER, df::biome_type::SWAMP_TROPICAL_FRESHWATER }, + { df::plant_raw_flags::BIOME_SWAMP_TROPICAL_SALTWATER, df::biome_type::SWAMP_TROPICAL_SALTWATER }, + { df::plant_raw_flags::BIOME_SWAMP_MANGROVE, df::biome_type::SWAMP_MANGROVE }, + { df::plant_raw_flags::BIOME_MARSH_TROPICAL_FRESHWATER, df::biome_type::MARSH_TROPICAL_FRESHWATER }, + { df::plant_raw_flags::BIOME_MARSH_TROPICAL_SALTWATER, df::biome_type::MARSH_TROPICAL_SALTWATER }, + { df::plant_raw_flags::BIOME_FOREST_TAIGA, df::biome_type::FOREST_TAIGA }, + { df::plant_raw_flags::BIOME_FOREST_TEMPERATE_CONIFER, df::biome_type::FOREST_TEMPERATE_CONIFER }, + { df::plant_raw_flags::BIOME_FOREST_TEMPERATE_BROADLEAF, df::biome_type::FOREST_TEMPERATE_BROADLEAF }, + { df::plant_raw_flags::BIOME_FOREST_TROPICAL_CONIFER, df::biome_type::FOREST_TROPICAL_CONIFER }, + { df::plant_raw_flags::BIOME_FOREST_TROPICAL_DRY_BROADLEAF, df::biome_type::FOREST_TROPICAL_DRY_BROADLEAF }, + { df::plant_raw_flags::BIOME_FOREST_TROPICAL_MOIST_BROADLEAF, df::biome_type::FOREST_TROPICAL_MOIST_BROADLEAF }, + { df::plant_raw_flags::BIOME_GRASSLAND_TEMPERATE, df::biome_type::GRASSLAND_TEMPERATE }, + { df::plant_raw_flags::BIOME_SAVANNA_TEMPERATE, df::biome_type::SAVANNA_TEMPERATE }, + { df::plant_raw_flags::BIOME_SHRUBLAND_TEMPERATE, df::biome_type::SHRUBLAND_TEMPERATE }, + { df::plant_raw_flags::BIOME_GRASSLAND_TROPICAL, df::biome_type::GRASSLAND_TROPICAL }, + { df::plant_raw_flags::BIOME_SAVANNA_TROPICAL, df::biome_type::SAVANNA_TROPICAL }, + { df::plant_raw_flags::BIOME_SHRUBLAND_TROPICAL, df::biome_type::SHRUBLAND_TROPICAL }, + { df::plant_raw_flags::BIOME_DESERT_BADLAND, df::biome_type::DESERT_BADLAND }, + { df::plant_raw_flags::BIOME_DESERT_ROCK, df::biome_type::DESERT_ROCK }, + { df::plant_raw_flags::BIOME_DESERT_SAND, df::biome_type::DESERT_SAND }, + { df::plant_raw_flags::BIOME_OCEAN_TROPICAL, df::biome_type::OCEAN_TROPICAL }, + { df::plant_raw_flags::BIOME_OCEAN_TEMPERATE, df::biome_type::OCEAN_TEMPERATE }, + { df::plant_raw_flags::BIOME_OCEAN_ARCTIC, df::biome_type::OCEAN_ARCTIC }, + { df::plant_raw_flags::BIOME_POOL_TEMPERATE_FRESHWATER, df::biome_type::POOL_TEMPERATE_FRESHWATER }, + { df::plant_raw_flags::BIOME_POOL_TEMPERATE_BRACKISHWATER, df::biome_type::POOL_TEMPERATE_BRACKISHWATER }, + { df::plant_raw_flags::BIOME_POOL_TEMPERATE_SALTWATER, df::biome_type::POOL_TEMPERATE_SALTWATER }, + { df::plant_raw_flags::BIOME_POOL_TROPICAL_FRESHWATER, df::biome_type::POOL_TROPICAL_FRESHWATER }, + { df::plant_raw_flags::BIOME_POOL_TROPICAL_BRACKISHWATER, df::biome_type::POOL_TROPICAL_BRACKISHWATER }, + { df::plant_raw_flags::BIOME_POOL_TROPICAL_SALTWATER, df::biome_type::POOL_TROPICAL_SALTWATER }, + { df::plant_raw_flags::BIOME_LAKE_TEMPERATE_FRESHWATER, df::biome_type::LAKE_TEMPERATE_FRESHWATER }, + { df::plant_raw_flags::BIOME_LAKE_TEMPERATE_BRACKISHWATER, df::biome_type::LAKE_TEMPERATE_BRACKISHWATER }, + { df::plant_raw_flags::BIOME_LAKE_TEMPERATE_SALTWATER, df::biome_type::LAKE_TEMPERATE_SALTWATER }, + { df::plant_raw_flags::BIOME_LAKE_TROPICAL_FRESHWATER, df::biome_type::LAKE_TROPICAL_FRESHWATER }, + { df::plant_raw_flags::BIOME_LAKE_TROPICAL_BRACKISHWATER, df::biome_type::LAKE_TROPICAL_BRACKISHWATER }, + { df::plant_raw_flags::BIOME_LAKE_TROPICAL_SALTWATER, df::biome_type::LAKE_TROPICAL_SALTWATER }, + { df::plant_raw_flags::BIOME_RIVER_TEMPERATE_FRESHWATER, df::biome_type::RIVER_TEMPERATE_FRESHWATER }, + { df::plant_raw_flags::BIOME_RIVER_TEMPERATE_BRACKISHWATER, df::biome_type::RIVER_TEMPERATE_BRACKISHWATER }, + { df::plant_raw_flags::BIOME_RIVER_TEMPERATE_SALTWATER, df::biome_type::RIVER_TEMPERATE_SALTWATER }, + { df::plant_raw_flags::BIOME_RIVER_TROPICAL_FRESHWATER, df::biome_type::RIVER_TROPICAL_FRESHWATER }, + { df::plant_raw_flags::BIOME_RIVER_TROPICAL_BRACKISHWATER, df::biome_type::RIVER_TROPICAL_BRACKISHWATER }, + { df::plant_raw_flags::BIOME_RIVER_TROPICAL_SALTWATER, df::biome_type::RIVER_TROPICAL_SALTWATER }, + { df::plant_raw_flags::BIOME_SUBTERRANEAN_WATER, df::biome_type::SUBTERRANEAN_WATER }, + { df::plant_raw_flags::BIOME_SUBTERRANEAN_CHASM, df::biome_type::SUBTERRANEAN_CHASM }, + { df::plant_raw_flags::BIOME_SUBTERRANEAN_LAVA, df::biome_type::SUBTERRANEAN_LAVA } + }; + + +public: + void find_plantable_plants() + { + plantable_plants.clear(); + + map counts; + + df::item_flags bad_flags; + bad_flags.whole = 0; + +#define F(x) bad_flags.bits.x = true; + F(dump); F(forbid); F(garbage_collect); + F(hostile); F(on_fire); F(rotten); F(trader); + F(in_building); F(construction); F(artifact); +#undef F + + for (auto ii : world->items.other[df::items_other_id::SEEDS]) + { + df::item_plantst* i = (df::item_plantst*)ii; + if ((i->flags.whole & bad_flags.whole) == 0) + counts[i->mat_index] += i->stack_size; + + } + + for (auto ci : counts) + { + if (df::global::ui->tasks.discovered_plants[ci.first]) + { + df::plant_raw* plant = world->raws.plants.all[ci.first]; + if (is_plantable(plant)) + for (auto flagmap : biomeFlagMap) + if (plant->flags.is_set(flagmap.first)) + plantable_plants[plant->index].insert(flagmap.second); + } + } + } + + void set_farms(color_ostream& out, vector plants, vector farms) + { + if (farms.empty()) + return; + + if (plants.empty()) + plants = vector{ -1 }; + + int season = *df::global::cur_season; + + for (int idx = 0; idx < farms.size(); idx++) + { + df::building_farmplotst* farm = farms[idx]; + int o = farm->plant_id[season]; + int n = plants[idx % plants.size()]; + if (n != o) + { + farm->plant_id[season] = plants[idx % plants.size()]; + out << "autofarm: changing farm #" << farm->id << + " from " << ((o == -1) ? "NONE" : world->raws.plants.all[o]->name) << + " to " << ((n == -1) ? "NONE" : world->raws.plants.all[n]->name) << endl; + } + } + } + + void process(color_ostream& out) + { + if (!enabled) + return; + + find_plantable_plants(); + + lastCounts.clear(); + + df::item_flags bad_flags; + bad_flags.whole = 0; + +#define F(x) bad_flags.bits.x = true; + F(dump); F(forbid); F(garbage_collect); + F(hostile); F(on_fire); F(rotten); F(trader); + F(in_building); F(construction); F(artifact); +#undef F + + for (auto ii : world->items.other[df::items_other_id::PLANT]) + { + df::item_plantst* i = (df::item_plantst*)ii; + if ((i->flags.whole & bad_flags.whole) == 0 && + plantable_plants.count(i->mat_index) > 0) + { + lastCounts[i->mat_index] += i->stack_size; + } + } + + map> plants; + plants.clear(); + + for (auto plantable : plantable_plants) + { + df::plant_raw* plant = world->raws.plants.all[plantable.first]; + if (lastCounts[plant->index] < getThreshold(plant->index)) + for (auto biome : plantable.second) + { + plants[biome].push_back(plant->index); + } + } + + map> farms; + farms.clear(); + + for (auto bb : world->buildings.other[df::buildings_other_id::FARM_PLOT]) + { + df::building_farmplotst* farm = (df::building_farmplotst*) bb; + if (farm->flags.bits.exists) + { + df::biome_type biome; + if (Maps::getTileDesignation(bb->centerx, bb->centery, bb->z)->bits.subterranean) + biome = biome_type::SUBTERRANEAN_WATER; + else { + df::coord2d region(Maps::getTileBiomeRgn(df::coord(bb->centerx, bb->centery, bb->z))); + biome = Maps::GetBiomeType(region.x, region.y); + } + farms[biome].push_back(farm); + } + } + + for (auto ff : farms) + { + set_farms(out, plants[ff.first], ff.second); + } + } + + void status(color_ostream& out) + { + out << (enabled ? "Running." : "Stopped.") << endl; + for (auto lc : lastCounts) + { + auto plant = world->raws.plants.all[lc.first]; + out << plant->id << " limit " << getThreshold(lc.first) << " current " << lc.second << endl; + } + + for (auto th : thresholds) + { + if (lastCounts[th.first] > 0) + continue; + auto plant = world->raws.plants.all[th.first]; + out << plant->id << " limit " << getThreshold(th.first) << " current 0" << endl; + } + out << "Default: " << defaultThreshold << endl; + } + +}; + +static AutoFarm* autofarmInstance; + + +DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) +{ + if (world && ui) { + commands.push_back( + PluginCommand("autofarm", tagline, + autofarm, false, usage + ) + ); + } + autofarmInstance = new AutoFarm(); + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown ( color_ostream &out ) +{ + delete autofarmInstance; + + return CR_OK; +} + +DFhackCExport command_result plugin_onupdate(color_ostream &out) +{ + if (!autofarmInstance) + return CR_OK; + + if (!Maps::IsValid()) + return CR_OK; + + if (DFHack::World::ReadPauseState()) + return CR_OK; + + if (world->frame_counter % 50 != 0) // Check every hour + return CR_OK; + + autofarmInstance->process(out); + + return CR_OK; +} + +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) +{ + enabled = enable; + return CR_OK; +} + +static command_result autofarm(color_ostream &out, vector & parameters) +{ + CoreSuspender suspend; + + if (parameters.size() == 1 && parameters[0] == "enable") + plugin_enable(out, true); + else if (parameters.size() == 1 && parameters[0] == "disable") + plugin_enable(out, false); + else if (parameters.size() == 2 && parameters[0] == "default") + autofarmInstance->setDefault(atoi(parameters[1].c_str())); + else if (parameters.size() >= 3 && parameters[0] == "threshold") + { + int val = atoi(parameters[1].c_str()); + for (int i = 2; i < parameters.size(); i++) + { + string id = parameters[i]; + transform(id.begin(), id.end(), id.begin(), ::toupper); + + bool ok = false; + for (auto plant : world->raws.plants.all) + { + if (plant->flags.is_set(df::plant_raw_flags::SEED) && (plant->id == id)) + { + autofarmInstance->setThreshold(plant->index, val); + ok = true; + break; + } + } + if (!ok) + { + out << "Cannot find plant with id " << id << endl; + return CR_WRONG_USAGE; + } + } + } + else if (parameters.size() == 0 || parameters.size() == 1 && parameters[0] == "status") + autofarmInstance->status(out); + else + return CR_WRONG_USAGE; + + return CR_OK; +} + From 3802412d688262bcdeb4dd7239c45667e5730b00 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 18 Aug 2018 14:57:26 -0500 Subject: [PATCH 2/7] autofarm: suspend while processing --- plugins/devel/autofarm.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/plugins/devel/autofarm.cpp b/plugins/devel/autofarm.cpp index 8a79d3f61..aaee2764e 100644 --- a/plugins/devel/autofarm.cpp +++ b/plugins/devel/autofarm.cpp @@ -39,12 +39,6 @@ DFHACK_PLUGIN("autofarm"); DFHACK_PLUGIN_IS_ENABLED(enabled); -static void process(color_ostream &out) -{ - CoreSuspender suspend; - -} - const char *tagline = "Automatically handle crop selection in farm plots based on current plant stocks."; const char *usage = ( "autofarm enable\n" @@ -350,7 +344,10 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) if (world->frame_counter % 50 != 0) // Check every hour return CR_OK; - autofarmInstance->process(out); + { + CoreSuspender suspend; + autofarmInstance->process(out); + } return CR_OK; } From 6afb76d41c400f4b6045910f113c855d49b57b66 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 27 Aug 2018 22:29:14 -0500 Subject: [PATCH 3/7] autofarm: make crop assignment more stable --- plugins/devel/autofarm.cpp | 61 ++++++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/plugins/devel/autofarm.cpp b/plugins/devel/autofarm.cpp index aaee2764e..0fc02728b 100644 --- a/plugins/devel/autofarm.cpp +++ b/plugins/devel/autofarm.cpp @@ -22,10 +22,13 @@ #include "modules/Maps.h" #include "modules/World.h" +#include + using std::vector; using std::string; using std::map; using std::set; +using std::queue; using std::endl; using namespace DFHack; using namespace df::enums; @@ -195,27 +198,53 @@ public: } } - void set_farms(color_ostream& out, vector plants, vector farms) + void set_farms(color_ostream& out, set plants, vector farms) { - if (farms.empty()) - return; - - if (plants.empty()) - plants = vector{ -1 }; + // this algorithm attempts to change as few farms as possible, while ensuring that + // the number of farms planting each eligible plant is "as equal as possible" + + if (farms.empty() || plants.empty()) + return; // do nothing if there are no farms or no plantable plants int season = *df::global::cur_season; - for (int idx = 0; idx < farms.size(); idx++) + int min = farms.size() / plants.size(); // the number of farms that should plant each eligible plant, rounded down + int extra = farms.size() - min * plants.size(); // the remainder that cannot be evenly divided + + map counters; + counters.empty(); + + queue toChange; + toChange.empty(); + + for (auto farm : farms) { - df::building_farmplotst* farm = farms[idx]; int o = farm->plant_id[season]; - int n = plants[idx % plants.size()]; - if (n != o) + if (plants.count(o)==0 || counters[o] > min || (counters[o] == min && extra == 0)) + toChange.push(farm); // this farm is an excess instance for the plant it is currently planting + else { - farm->plant_id[season] = plants[idx % plants.size()]; + if (counters[o] == min) + extra--; // allocate off one of the remainder farms + counters[o]++; + } + } + + for (auto n : plants) + { + int c = counters[n]; + while (toChange.size() > 0 && (c < min || (c == min && extra > 0))) + { + // pick one of the excess farms and change it to plant this plant + df::building_farmplotst* farm = toChange.front(); + int o = farm->plant_id[season]; + farm->plant_id[season] = n; out << "autofarm: changing farm #" << farm->id << - " from " << ((o == -1) ? "NONE" : world->raws.plants.all[o]->name) << + " from " << ((o == -1) ? "NONE" : world->raws.plants.all[o]->name) << " to " << ((n == -1) ? "NONE" : world->raws.plants.all[n]->name) << endl; + toChange.pop(); + if (c++ == min) + extra--; } } } @@ -248,7 +277,7 @@ public: } } - map> plants; + map> plants; plants.clear(); for (auto plantable : plantable_plants) @@ -257,7 +286,7 @@ public: if (lastCounts[plant->index] < getThreshold(plant->index)) for (auto biome : plantable.second) { - plants[biome].push_back(plant->index); + plants[biome].insert(plant->index); } } @@ -362,7 +391,9 @@ static command_result autofarm(color_ostream &out, vector & parameters) { CoreSuspender suspend; - if (parameters.size() == 1 && parameters[0] == "enable") + if (parameters.size() == 1 && parameters[0] == "runonce") + autofarmInstance->process(out); + else if (parameters.size() == 1 && parameters[0] == "enable") plugin_enable(out, true); else if (parameters.size() == 1 && parameters[0] == "disable") plugin_enable(out, false); From 4abd410b1b415d74417a91454ae18006104d36f5 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 16 Aug 2018 09:16:52 -0500 Subject: [PATCH 4/7] Convert autofarm from ruby to C++ Reimplement the Ruby autofarm script as a C++ plugin --- plugins/devel/CMakeLists.txt | 1 + plugins/devel/autofarm.cpp | 406 +++++++++++++++++++++++++++++++++++ 2 files changed, 407 insertions(+) create mode 100644 plugins/devel/autofarm.cpp diff --git a/plugins/devel/CMakeLists.txt b/plugins/devel/CMakeLists.txt index 245bc518c..a431fcaae 100644 --- a/plugins/devel/CMakeLists.txt +++ b/plugins/devel/CMakeLists.txt @@ -3,6 +3,7 @@ DFHACK_PLUGIN(vectors vectors.cpp) endif() ADD_DEFINITIONS(-DDEV_PLUGIN) +DFHACK_PLUGIN(autofarm autofarm.cpp) DFHACK_PLUGIN(buildprobe buildprobe.cpp) DFHACK_PLUGIN(color-dfhack-text color-dfhack-text.cpp) DFHACK_PLUGIN(counters counters.cpp) diff --git a/plugins/devel/autofarm.cpp b/plugins/devel/autofarm.cpp new file mode 100644 index 000000000..8a79d3f61 --- /dev/null +++ b/plugins/devel/autofarm.cpp @@ -0,0 +1,406 @@ +#include "Core.h" +#include "Console.h" +#include "Export.h" +#include "PluginManager.h" + +#include "DataDefs.h" +#include "df/world.h" +#include "df/ui.h" +#include "df/building_type.h" +#include "df/building_farmplotst.h" +#include "df/buildings_other_id.h" +#include "df/global_objects.h" +#include "df/item.h" +#include "df/item_plantst.h" +#include "df/items_other_id.h" +#include "df/unit.h" +#include "df/building.h" +#include "df/plant_raw.h" +#include "df/plant_raw_flags.h" +#include "df/biome_type.h" +#include "modules/Items.h" +#include "modules/Maps.h" +#include "modules/World.h" + +using std::vector; +using std::string; +using std::map; +using std::set; +using std::endl; +using namespace DFHack; +using namespace df::enums; + +using df::global::world; +using df::global::ui; + +static command_result autofarm(color_ostream &out, vector & parameters); + +DFHACK_PLUGIN("autofarm"); + +DFHACK_PLUGIN_IS_ENABLED(enabled); + +static void process(color_ostream &out) +{ + CoreSuspender suspend; + +} + +const char *tagline = "Automatically handle crop selection in farm plots based on current plant stocks."; +const char *usage = ( + "autofarm enable\n" + "autofarm default 30\n" + "autofarm threshold 150 helmet_plump tail_pig\n" + ); + +class AutoFarm { + +private: + map thresholds; + int defaultThreshold = 50; + + map lastCounts; + +public: + void initialize() + { + thresholds.clear(); + defaultThreshold = 50; + + lastCounts.clear(); + } + + void setThreshold(int id, int val) + { + thresholds[id] = val; + } + + int getThreshold(int id) + { + return (thresholds.count(id) > 0) ? thresholds[id] : defaultThreshold; + } + + void setDefault(int val) + { + defaultThreshold = val; + } + +private: + const df::plant_raw_flags seasons[4] = { df::plant_raw_flags::SPRING, df::plant_raw_flags::SUMMER, df::plant_raw_flags::AUTUMN, df::plant_raw_flags::WINTER }; + +public: + + bool is_plantable(df::plant_raw* plant) + { + bool has_seed = plant->flags.is_set(df::plant_raw_flags::SEED); + bool is_tree = plant->flags.is_set(df::plant_raw_flags::TREE); + + int8_t season = *df::global::cur_season; + int harvest = (*df::global::cur_season_tick) + plant->growdur * 10; + bool can_plant = has_seed && !is_tree && plant->flags.is_set(seasons[season]); + while (can_plant && harvest >= 10080) { + season = (season + 1) % 4; + harvest -= 10080; + can_plant = can_plant && plant->flags.is_set(seasons[season]); + } + + return can_plant; + } + +private: + map> plantable_plants; + + const map biomeFlagMap = { + { df::plant_raw_flags::BIOME_MOUNTAIN, df::biome_type::MOUNTAIN }, + { df::plant_raw_flags::BIOME_GLACIER, df::biome_type::GLACIER }, + { df::plant_raw_flags::BIOME_TUNDRA, df::biome_type::TUNDRA }, + { df::plant_raw_flags::BIOME_SWAMP_TEMPERATE_FRESHWATER, df::biome_type::SWAMP_TEMPERATE_FRESHWATER }, + { df::plant_raw_flags::BIOME_SWAMP_TEMPERATE_SALTWATER, df::biome_type::SWAMP_TEMPERATE_SALTWATER }, + { df::plant_raw_flags::BIOME_MARSH_TEMPERATE_FRESHWATER, df::biome_type::MARSH_TEMPERATE_FRESHWATER }, + { df::plant_raw_flags::BIOME_MARSH_TEMPERATE_SALTWATER, df::biome_type::MARSH_TEMPERATE_SALTWATER }, + { df::plant_raw_flags::BIOME_SWAMP_TROPICAL_FRESHWATER, df::biome_type::SWAMP_TROPICAL_FRESHWATER }, + { df::plant_raw_flags::BIOME_SWAMP_TROPICAL_SALTWATER, df::biome_type::SWAMP_TROPICAL_SALTWATER }, + { df::plant_raw_flags::BIOME_SWAMP_MANGROVE, df::biome_type::SWAMP_MANGROVE }, + { df::plant_raw_flags::BIOME_MARSH_TROPICAL_FRESHWATER, df::biome_type::MARSH_TROPICAL_FRESHWATER }, + { df::plant_raw_flags::BIOME_MARSH_TROPICAL_SALTWATER, df::biome_type::MARSH_TROPICAL_SALTWATER }, + { df::plant_raw_flags::BIOME_FOREST_TAIGA, df::biome_type::FOREST_TAIGA }, + { df::plant_raw_flags::BIOME_FOREST_TEMPERATE_CONIFER, df::biome_type::FOREST_TEMPERATE_CONIFER }, + { df::plant_raw_flags::BIOME_FOREST_TEMPERATE_BROADLEAF, df::biome_type::FOREST_TEMPERATE_BROADLEAF }, + { df::plant_raw_flags::BIOME_FOREST_TROPICAL_CONIFER, df::biome_type::FOREST_TROPICAL_CONIFER }, + { df::plant_raw_flags::BIOME_FOREST_TROPICAL_DRY_BROADLEAF, df::biome_type::FOREST_TROPICAL_DRY_BROADLEAF }, + { df::plant_raw_flags::BIOME_FOREST_TROPICAL_MOIST_BROADLEAF, df::biome_type::FOREST_TROPICAL_MOIST_BROADLEAF }, + { df::plant_raw_flags::BIOME_GRASSLAND_TEMPERATE, df::biome_type::GRASSLAND_TEMPERATE }, + { df::plant_raw_flags::BIOME_SAVANNA_TEMPERATE, df::biome_type::SAVANNA_TEMPERATE }, + { df::plant_raw_flags::BIOME_SHRUBLAND_TEMPERATE, df::biome_type::SHRUBLAND_TEMPERATE }, + { df::plant_raw_flags::BIOME_GRASSLAND_TROPICAL, df::biome_type::GRASSLAND_TROPICAL }, + { df::plant_raw_flags::BIOME_SAVANNA_TROPICAL, df::biome_type::SAVANNA_TROPICAL }, + { df::plant_raw_flags::BIOME_SHRUBLAND_TROPICAL, df::biome_type::SHRUBLAND_TROPICAL }, + { df::plant_raw_flags::BIOME_DESERT_BADLAND, df::biome_type::DESERT_BADLAND }, + { df::plant_raw_flags::BIOME_DESERT_ROCK, df::biome_type::DESERT_ROCK }, + { df::plant_raw_flags::BIOME_DESERT_SAND, df::biome_type::DESERT_SAND }, + { df::plant_raw_flags::BIOME_OCEAN_TROPICAL, df::biome_type::OCEAN_TROPICAL }, + { df::plant_raw_flags::BIOME_OCEAN_TEMPERATE, df::biome_type::OCEAN_TEMPERATE }, + { df::plant_raw_flags::BIOME_OCEAN_ARCTIC, df::biome_type::OCEAN_ARCTIC }, + { df::plant_raw_flags::BIOME_POOL_TEMPERATE_FRESHWATER, df::biome_type::POOL_TEMPERATE_FRESHWATER }, + { df::plant_raw_flags::BIOME_POOL_TEMPERATE_BRACKISHWATER, df::biome_type::POOL_TEMPERATE_BRACKISHWATER }, + { df::plant_raw_flags::BIOME_POOL_TEMPERATE_SALTWATER, df::biome_type::POOL_TEMPERATE_SALTWATER }, + { df::plant_raw_flags::BIOME_POOL_TROPICAL_FRESHWATER, df::biome_type::POOL_TROPICAL_FRESHWATER }, + { df::plant_raw_flags::BIOME_POOL_TROPICAL_BRACKISHWATER, df::biome_type::POOL_TROPICAL_BRACKISHWATER }, + { df::plant_raw_flags::BIOME_POOL_TROPICAL_SALTWATER, df::biome_type::POOL_TROPICAL_SALTWATER }, + { df::plant_raw_flags::BIOME_LAKE_TEMPERATE_FRESHWATER, df::biome_type::LAKE_TEMPERATE_FRESHWATER }, + { df::plant_raw_flags::BIOME_LAKE_TEMPERATE_BRACKISHWATER, df::biome_type::LAKE_TEMPERATE_BRACKISHWATER }, + { df::plant_raw_flags::BIOME_LAKE_TEMPERATE_SALTWATER, df::biome_type::LAKE_TEMPERATE_SALTWATER }, + { df::plant_raw_flags::BIOME_LAKE_TROPICAL_FRESHWATER, df::biome_type::LAKE_TROPICAL_FRESHWATER }, + { df::plant_raw_flags::BIOME_LAKE_TROPICAL_BRACKISHWATER, df::biome_type::LAKE_TROPICAL_BRACKISHWATER }, + { df::plant_raw_flags::BIOME_LAKE_TROPICAL_SALTWATER, df::biome_type::LAKE_TROPICAL_SALTWATER }, + { df::plant_raw_flags::BIOME_RIVER_TEMPERATE_FRESHWATER, df::biome_type::RIVER_TEMPERATE_FRESHWATER }, + { df::plant_raw_flags::BIOME_RIVER_TEMPERATE_BRACKISHWATER, df::biome_type::RIVER_TEMPERATE_BRACKISHWATER }, + { df::plant_raw_flags::BIOME_RIVER_TEMPERATE_SALTWATER, df::biome_type::RIVER_TEMPERATE_SALTWATER }, + { df::plant_raw_flags::BIOME_RIVER_TROPICAL_FRESHWATER, df::biome_type::RIVER_TROPICAL_FRESHWATER }, + { df::plant_raw_flags::BIOME_RIVER_TROPICAL_BRACKISHWATER, df::biome_type::RIVER_TROPICAL_BRACKISHWATER }, + { df::plant_raw_flags::BIOME_RIVER_TROPICAL_SALTWATER, df::biome_type::RIVER_TROPICAL_SALTWATER }, + { df::plant_raw_flags::BIOME_SUBTERRANEAN_WATER, df::biome_type::SUBTERRANEAN_WATER }, + { df::plant_raw_flags::BIOME_SUBTERRANEAN_CHASM, df::biome_type::SUBTERRANEAN_CHASM }, + { df::plant_raw_flags::BIOME_SUBTERRANEAN_LAVA, df::biome_type::SUBTERRANEAN_LAVA } + }; + + +public: + void find_plantable_plants() + { + plantable_plants.clear(); + + map counts; + + df::item_flags bad_flags; + bad_flags.whole = 0; + +#define F(x) bad_flags.bits.x = true; + F(dump); F(forbid); F(garbage_collect); + F(hostile); F(on_fire); F(rotten); F(trader); + F(in_building); F(construction); F(artifact); +#undef F + + for (auto ii : world->items.other[df::items_other_id::SEEDS]) + { + df::item_plantst* i = (df::item_plantst*)ii; + if ((i->flags.whole & bad_flags.whole) == 0) + counts[i->mat_index] += i->stack_size; + + } + + for (auto ci : counts) + { + if (df::global::ui->tasks.discovered_plants[ci.first]) + { + df::plant_raw* plant = world->raws.plants.all[ci.first]; + if (is_plantable(plant)) + for (auto flagmap : biomeFlagMap) + if (plant->flags.is_set(flagmap.first)) + plantable_plants[plant->index].insert(flagmap.second); + } + } + } + + void set_farms(color_ostream& out, vector plants, vector farms) + { + if (farms.empty()) + return; + + if (plants.empty()) + plants = vector{ -1 }; + + int season = *df::global::cur_season; + + for (int idx = 0; idx < farms.size(); idx++) + { + df::building_farmplotst* farm = farms[idx]; + int o = farm->plant_id[season]; + int n = plants[idx % plants.size()]; + if (n != o) + { + farm->plant_id[season] = plants[idx % plants.size()]; + out << "autofarm: changing farm #" << farm->id << + " from " << ((o == -1) ? "NONE" : world->raws.plants.all[o]->name) << + " to " << ((n == -1) ? "NONE" : world->raws.plants.all[n]->name) << endl; + } + } + } + + void process(color_ostream& out) + { + if (!enabled) + return; + + find_plantable_plants(); + + lastCounts.clear(); + + df::item_flags bad_flags; + bad_flags.whole = 0; + +#define F(x) bad_flags.bits.x = true; + F(dump); F(forbid); F(garbage_collect); + F(hostile); F(on_fire); F(rotten); F(trader); + F(in_building); F(construction); F(artifact); +#undef F + + for (auto ii : world->items.other[df::items_other_id::PLANT]) + { + df::item_plantst* i = (df::item_plantst*)ii; + if ((i->flags.whole & bad_flags.whole) == 0 && + plantable_plants.count(i->mat_index) > 0) + { + lastCounts[i->mat_index] += i->stack_size; + } + } + + map> plants; + plants.clear(); + + for (auto plantable : plantable_plants) + { + df::plant_raw* plant = world->raws.plants.all[plantable.first]; + if (lastCounts[plant->index] < getThreshold(plant->index)) + for (auto biome : plantable.second) + { + plants[biome].push_back(plant->index); + } + } + + map> farms; + farms.clear(); + + for (auto bb : world->buildings.other[df::buildings_other_id::FARM_PLOT]) + { + df::building_farmplotst* farm = (df::building_farmplotst*) bb; + if (farm->flags.bits.exists) + { + df::biome_type biome; + if (Maps::getTileDesignation(bb->centerx, bb->centery, bb->z)->bits.subterranean) + biome = biome_type::SUBTERRANEAN_WATER; + else { + df::coord2d region(Maps::getTileBiomeRgn(df::coord(bb->centerx, bb->centery, bb->z))); + biome = Maps::GetBiomeType(region.x, region.y); + } + farms[biome].push_back(farm); + } + } + + for (auto ff : farms) + { + set_farms(out, plants[ff.first], ff.second); + } + } + + void status(color_ostream& out) + { + out << (enabled ? "Running." : "Stopped.") << endl; + for (auto lc : lastCounts) + { + auto plant = world->raws.plants.all[lc.first]; + out << plant->id << " limit " << getThreshold(lc.first) << " current " << lc.second << endl; + } + + for (auto th : thresholds) + { + if (lastCounts[th.first] > 0) + continue; + auto plant = world->raws.plants.all[th.first]; + out << plant->id << " limit " << getThreshold(th.first) << " current 0" << endl; + } + out << "Default: " << defaultThreshold << endl; + } + +}; + +static AutoFarm* autofarmInstance; + + +DFhackCExport command_result plugin_init (color_ostream &out, std::vector &commands) +{ + if (world && ui) { + commands.push_back( + PluginCommand("autofarm", tagline, + autofarm, false, usage + ) + ); + } + autofarmInstance = new AutoFarm(); + return CR_OK; +} + +DFhackCExport command_result plugin_shutdown ( color_ostream &out ) +{ + delete autofarmInstance; + + return CR_OK; +} + +DFhackCExport command_result plugin_onupdate(color_ostream &out) +{ + if (!autofarmInstance) + return CR_OK; + + if (!Maps::IsValid()) + return CR_OK; + + if (DFHack::World::ReadPauseState()) + return CR_OK; + + if (world->frame_counter % 50 != 0) // Check every hour + return CR_OK; + + autofarmInstance->process(out); + + return CR_OK; +} + +DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) +{ + enabled = enable; + return CR_OK; +} + +static command_result autofarm(color_ostream &out, vector & parameters) +{ + CoreSuspender suspend; + + if (parameters.size() == 1 && parameters[0] == "enable") + plugin_enable(out, true); + else if (parameters.size() == 1 && parameters[0] == "disable") + plugin_enable(out, false); + else if (parameters.size() == 2 && parameters[0] == "default") + autofarmInstance->setDefault(atoi(parameters[1].c_str())); + else if (parameters.size() >= 3 && parameters[0] == "threshold") + { + int val = atoi(parameters[1].c_str()); + for (int i = 2; i < parameters.size(); i++) + { + string id = parameters[i]; + transform(id.begin(), id.end(), id.begin(), ::toupper); + + bool ok = false; + for (auto plant : world->raws.plants.all) + { + if (plant->flags.is_set(df::plant_raw_flags::SEED) && (plant->id == id)) + { + autofarmInstance->setThreshold(plant->index, val); + ok = true; + break; + } + } + if (!ok) + { + out << "Cannot find plant with id " << id << endl; + return CR_WRONG_USAGE; + } + } + } + else if (parameters.size() == 0 || parameters.size() == 1 && parameters[0] == "status") + autofarmInstance->status(out); + else + return CR_WRONG_USAGE; + + return CR_OK; +} + From 7c8c6546c7c4a6d6f3613d30303a46962791b863 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 18 Aug 2018 14:57:26 -0500 Subject: [PATCH 5/7] autofarm: suspend while processing --- plugins/devel/autofarm.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/plugins/devel/autofarm.cpp b/plugins/devel/autofarm.cpp index 8a79d3f61..aaee2764e 100644 --- a/plugins/devel/autofarm.cpp +++ b/plugins/devel/autofarm.cpp @@ -39,12 +39,6 @@ DFHACK_PLUGIN("autofarm"); DFHACK_PLUGIN_IS_ENABLED(enabled); -static void process(color_ostream &out) -{ - CoreSuspender suspend; - -} - const char *tagline = "Automatically handle crop selection in farm plots based on current plant stocks."; const char *usage = ( "autofarm enable\n" @@ -350,7 +344,10 @@ DFhackCExport command_result plugin_onupdate(color_ostream &out) if (world->frame_counter % 50 != 0) // Check every hour return CR_OK; - autofarmInstance->process(out); + { + CoreSuspender suspend; + autofarmInstance->process(out); + } return CR_OK; } From 5d92026bbe66dd5f34c2de897bf86d721b1801f1 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Mon, 27 Aug 2018 22:29:14 -0500 Subject: [PATCH 6/7] autofarm: make crop assignment more stable --- plugins/devel/autofarm.cpp | 61 ++++++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/plugins/devel/autofarm.cpp b/plugins/devel/autofarm.cpp index aaee2764e..0fc02728b 100644 --- a/plugins/devel/autofarm.cpp +++ b/plugins/devel/autofarm.cpp @@ -22,10 +22,13 @@ #include "modules/Maps.h" #include "modules/World.h" +#include + using std::vector; using std::string; using std::map; using std::set; +using std::queue; using std::endl; using namespace DFHack; using namespace df::enums; @@ -195,27 +198,53 @@ public: } } - void set_farms(color_ostream& out, vector plants, vector farms) + void set_farms(color_ostream& out, set plants, vector farms) { - if (farms.empty()) - return; - - if (plants.empty()) - plants = vector{ -1 }; + // this algorithm attempts to change as few farms as possible, while ensuring that + // the number of farms planting each eligible plant is "as equal as possible" + + if (farms.empty() || plants.empty()) + return; // do nothing if there are no farms or no plantable plants int season = *df::global::cur_season; - for (int idx = 0; idx < farms.size(); idx++) + int min = farms.size() / plants.size(); // the number of farms that should plant each eligible plant, rounded down + int extra = farms.size() - min * plants.size(); // the remainder that cannot be evenly divided + + map counters; + counters.empty(); + + queue toChange; + toChange.empty(); + + for (auto farm : farms) { - df::building_farmplotst* farm = farms[idx]; int o = farm->plant_id[season]; - int n = plants[idx % plants.size()]; - if (n != o) + if (plants.count(o)==0 || counters[o] > min || (counters[o] == min && extra == 0)) + toChange.push(farm); // this farm is an excess instance for the plant it is currently planting + else { - farm->plant_id[season] = plants[idx % plants.size()]; + if (counters[o] == min) + extra--; // allocate off one of the remainder farms + counters[o]++; + } + } + + for (auto n : plants) + { + int c = counters[n]; + while (toChange.size() > 0 && (c < min || (c == min && extra > 0))) + { + // pick one of the excess farms and change it to plant this plant + df::building_farmplotst* farm = toChange.front(); + int o = farm->plant_id[season]; + farm->plant_id[season] = n; out << "autofarm: changing farm #" << farm->id << - " from " << ((o == -1) ? "NONE" : world->raws.plants.all[o]->name) << + " from " << ((o == -1) ? "NONE" : world->raws.plants.all[o]->name) << " to " << ((n == -1) ? "NONE" : world->raws.plants.all[n]->name) << endl; + toChange.pop(); + if (c++ == min) + extra--; } } } @@ -248,7 +277,7 @@ public: } } - map> plants; + map> plants; plants.clear(); for (auto plantable : plantable_plants) @@ -257,7 +286,7 @@ public: if (lastCounts[plant->index] < getThreshold(plant->index)) for (auto biome : plantable.second) { - plants[biome].push_back(plant->index); + plants[biome].insert(plant->index); } } @@ -362,7 +391,9 @@ static command_result autofarm(color_ostream &out, vector & parameters) { CoreSuspender suspend; - if (parameters.size() == 1 && parameters[0] == "enable") + if (parameters.size() == 1 && parameters[0] == "runonce") + autofarmInstance->process(out); + else if (parameters.size() == 1 && parameters[0] == "enable") plugin_enable(out, true); else if (parameters.size() == 1 && parameters[0] == "disable") plugin_enable(out, false); From dfafafdf9b1489eade5139ecd37dc99a0ecfa90e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 21 Nov 2019 18:58:06 -0600 Subject: [PATCH 7/7] autofarm.cpp: whitespace, complexity Clean up autofarm.cpp --- plugins/devel/autofarm.cpp | 64 +++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/plugins/devel/autofarm.cpp b/plugins/devel/autofarm.cpp index 0fc02728b..991dfd222 100644 --- a/plugins/devel/autofarm.cpp +++ b/plugins/devel/autofarm.cpp @@ -50,7 +50,6 @@ const char *usage = ( ); class AutoFarm { - private: map thresholds; int defaultThreshold = 50; @@ -85,7 +84,6 @@ private: const df::plant_raw_flags seasons[4] = { df::plant_raw_flags::SPRING, df::plant_raw_flags::SUMMER, df::plant_raw_flags::AUTUMN, df::plant_raw_flags::WINTER }; public: - bool is_plantable(df::plant_raw* plant) { bool has_seed = plant->flags.is_set(df::plant_raw_flags::SEED); @@ -182,7 +180,6 @@ public: df::item_plantst* i = (df::item_plantst*)ii; if ((i->flags.whole & bad_flags.whole) == 0) counts[i->mat_index] += i->stack_size; - } for (auto ci : counts) @@ -200,9 +197,9 @@ public: void set_farms(color_ostream& out, set plants, vector farms) { - // this algorithm attempts to change as few farms as possible, while ensuring that - // the number of farms planting each eligible plant is "as equal as possible" - + // this algorithm attempts to change as few farms as possible, while ensuring that + // the number of farms planting each eligible plant is "as equal as possible" + if (farms.empty() || plants.empty()) return; // do nothing if there are no farms or no plantable plants @@ -224,7 +221,7 @@ public: toChange.push(farm); // this farm is an excess instance for the plant it is currently planting else { - if (counters[o] == min) + if (counters[o] == min) extra--; // allocate off one of the remainder farms counters[o]++; } @@ -333,7 +330,6 @@ public: } out << "Default: " << defaultThreshold << endl; } - }; static AutoFarm* autofarmInstance; @@ -387,6 +383,33 @@ DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) return CR_OK; } +static command_result setThresholds(color_ostream& out, vector & parameters) +{ + int val = atoi(parameters[1].c_str()); + for (int i = 2; i < parameters.size(); i++) + { + string id = parameters[i]; + transform(id.begin(), id.end(), id.begin(), ::toupper); + + bool ok = false; + for (auto plant : world->raws.plants.all) + { + if (plant->flags.is_set(df::plant_raw_flags::SEED) && (plant->id == id)) + { + autofarmInstance->setThreshold(plant->index, val); + ok = true; + break; + } + } + if (!ok) + { + out << "Cannot find plant with id " << id << endl; + return CR_WRONG_USAGE; + } + } + return CR_OK; +} + static command_result autofarm(color_ostream &out, vector & parameters) { CoreSuspender suspend; @@ -400,30 +423,7 @@ static command_result autofarm(color_ostream &out, vector & parameters) else if (parameters.size() == 2 && parameters[0] == "default") autofarmInstance->setDefault(atoi(parameters[1].c_str())); else if (parameters.size() >= 3 && parameters[0] == "threshold") - { - int val = atoi(parameters[1].c_str()); - for (int i = 2; i < parameters.size(); i++) - { - string id = parameters[i]; - transform(id.begin(), id.end(), id.begin(), ::toupper); - - bool ok = false; - for (auto plant : world->raws.plants.all) - { - if (plant->flags.is_set(df::plant_raw_flags::SEED) && (plant->id == id)) - { - autofarmInstance->setThreshold(plant->index, val); - ok = true; - break; - } - } - if (!ok) - { - out << "Cannot find plant with id " << id << endl; - return CR_WRONG_USAGE; - } - } - } + return setThresholds(out, parameters); else if (parameters.size() == 0 || parameters.size() == 1 && parameters[0] == "status") autofarmInstance->status(out); else