From be40d55e64e2a5fd1f211e845a2a81bdbc0c6184 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 9 Jan 2023 17:31:49 -0800 Subject: [PATCH] update seedwatch --- docs/plugins/seedwatch.rst | 41 ++- library/include/LuaTools.h | 8 + library/include/modules/Kitchen.h | 30 +- library/modules/Kitchen.cpp | 78 ---- plugins/CMakeLists.txt | 2 +- plugins/lua/seedwatch.lua | 67 ++++ plugins/seedwatch.cpp | 588 ++++++++++++++++++------------ 7 files changed, 452 insertions(+), 362 deletions(-) create mode 100644 plugins/lua/seedwatch.lua diff --git a/docs/plugins/seedwatch.rst b/docs/plugins/seedwatch.rst index b41f3a977..12a728c51 100644 --- a/docs/plugins/seedwatch.rst +++ b/docs/plugins/seedwatch.rst @@ -5,42 +5,47 @@ seedwatch :summary: Manages seed and plant cooking based on seed stock levels. :tags: fort auto plants -Each seed type can be assigned a target. If the number of seeds of that type -falls below that target, then the plants and seeds of that type will be excluded -from cookery. If the number rises above the target + 20, then cooking will be -allowed. +Unlike brewing and other kinds of processing, cooking plants does not produce +a usable seed. By default, all plants are allowed to be cooked. This often leads +to the situation where dwarves have no seeds left to plant crops with because +they cooked all the relevant plants. Seedwatch protects you from this problem. -The plugin needs a fortress to be loaded and will deactivate automatically -otherwise. You have to reactivate with ``enable seedwatch`` after you load a -fort. +Each seed type can be assigned a target stock amount. If the number of seeds of +that type falls below that target, then the plants and seeds of that type will +be protected from cookery. If the number rises above the target + 20, then +cooking will be allowed again. Usage ----- ``enable seedwatch`` - Start managing seed and plant cooking. By default, no types are watched. - You have to add them with further ``seedwatch`` commands. + Start managing seed and plant cooking. By default, all types are watched + with a target of ``30``, but you can adjust the list or even + ``seedwatch clear`` it and start your own if you like. +``seedwatch [status]`` + Display whether seedwatch is enabled and prints out the watch list, along + with the current seed counts. ``seedwatch `` Adds the specified type to the watchlist (if it's not already there) and sets the target number of seeds to the specified number. You can pass the keyword ``all`` instead of a specific type to set the target for all types. -``seedwatch `` - Removes the specified type from the watch list. + If you set the target to ``0``, it removes the specified type from the + watch list. ``seedwatch clear`` - Clears all types from the watch list. -``seedwatch info`` - Display whether seedwatch is enabled and prints out the watch list. + Clears all types from the watch list. Same as ``seedwatch all 0``. -To print out a list of all plant types, you can run this command:: +To see a list of all plant types that you might want to set targets for, you can +run this command:: devel/query --table df.global.world.raws.plants.all --search ^id --maxdepth 1 Examples -------- -``seedwatch all 30`` - Adds all seeds to the watch list and sets the targets to 30. +``enable seedwatch`` + Adds all seeds to the watch list, sets the targets to 30, and starts + monitoring your seed stock levels. ``seedwatch MUSHROOM_HELMET_PLUMP 50`` Add Plump Helmets to the watch list and sets the target to 50. -``seedwatch MUSHROOM_HELMET_PLUMP`` +``seedwatch MUSHROOM_HELMET_PLUMP 0`` removes Plump Helmets from the watch list. diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index 9e1901f03..ca9aac788 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -30,6 +30,7 @@ distribution. #include #include #include +#include #include "df/interfacest.h" @@ -378,6 +379,13 @@ namespace DFHack {namespace Lua { TableInsert(L, entry.first, entry.second); } + template + void Push(lua_State *L, const std::unordered_map &pmap) { + lua_createtable(L, 0, pmap.size()); + for (auto &entry : pmap) + TableInsert(L, entry.first, entry.second); + } + DFHACK_EXPORT void CheckPen(lua_State *L, Screen::Pen *pen, int index, bool allow_nil = false, bool allow_color = true); DFHACK_EXPORT bool IsCoreContext(lua_State *state); diff --git a/library/include/modules/Kitchen.h b/library/include/modules/Kitchen.h index 3fde8edf8..f2708c844 100644 --- a/library/include/modules/Kitchen.h +++ b/library/include/modules/Kitchen.h @@ -27,11 +27,6 @@ distribution. * kitchen settings */ #include "Export.h" -#include "Module.h" -#include "Types.h" -#include "VersionInfo.h" -#include "Core.h" -#include "modules/Items.h" #include "df/kitchen_exc_type.h" /** @@ -43,38 +38,21 @@ namespace DFHack { namespace Kitchen { +// print the exclusion list, with the material index also translated into its token (for organics) - for debug really +DFHACK_EXPORT void debug_print(color_ostream &out); + /** * Kitchen exclusions manipulator. Currently geared towards plants and seeds. * \ingroup grp_modules * \ingroup grp_kitchen */ -// print the exclusion list, with the material index also translated into its token (for organics) - for debug really -DFHACK_EXPORT void debug_print(color_ostream &out); - // remove this plant from the exclusion list if it is in it DFHACK_EXPORT void allowPlantSeedCookery(int32_t plant_id); // add this plant to the exclusion list, if it is not already in it DFHACK_EXPORT void denyPlantSeedCookery(int32_t plant_id); -// fills a map with info from the limit info storage entries in the exclusion list -DFHACK_EXPORT void fillWatchMap(std::map& watchMap); - -// Finds the index of a limit info storage entry. Returns -1 if not found. -DFHACK_EXPORT int findLimit(int32_t plant_id); - -// removes a limit info storage entry from the exclusion list if it's present -DFHACK_EXPORT bool removeLimit(int32_t plant_id); - -// add a limit info storage item to the exclusion list, or alters an existing one -DFHACK_EXPORT bool setLimit(int32_t plant_id, int16_t limit); - -// clears all limit info storage items from the exclusion list -DFHACK_EXPORT void clearLimits(); - -DFHACK_EXPORT std::size_t size(); - // Finds the index of a kitchen exclusion in plotinfo.kitchen.exc_types. Returns -1 if not found. DFHACK_EXPORT int findExclusion(df::kitchen_exc_type type, df::item_type item_type, int16_t item_subtype, @@ -90,5 +68,7 @@ DFHACK_EXPORT bool removeExclusion(df::kitchen_exc_type type, df::item_type item_type, int16_t item_subtype, int16_t mat_type, int32_t mat_index); +DFHACK_EXPORT std::size_t size(); + } } diff --git a/library/modules/Kitchen.cpp b/library/modules/Kitchen.cpp index 65e47f528..5bbc2ee7c 100644 --- a/library/modules/Kitchen.cpp +++ b/library/modules/Kitchen.cpp @@ -78,84 +78,6 @@ void Kitchen::denyPlantSeedCookery(int32_t plant_id) type->material_defs.idx[plant_material_def::basic_mat]); } -void Kitchen::fillWatchMap(std::map& watchMap) -{ - watchMap.clear(); - for (std::size_t i = 0; i < size(); ++i) - { - if (plotinfo->kitchen.item_subtypes[i] == SEEDLIMIT_ITEMTYPE && - plotinfo->kitchen.item_subtypes[i] == SEEDLIMIT_ITEMSUBTYPE && - plotinfo->kitchen.exc_types[i] == SEEDLIMIT_EXCTYPE) - { - watchMap[plotinfo->kitchen.mat_indices[i]] = plotinfo->kitchen.mat_types[i]; - } - } -} - -int Kitchen::findLimit(int32_t plant_id) -{ - for (size_t i = 0; i < size(); ++i) - { - if (plotinfo->kitchen.item_types[i] == SEEDLIMIT_ITEMTYPE && - plotinfo->kitchen.item_subtypes[i] == SEEDLIMIT_ITEMSUBTYPE && - plotinfo->kitchen.mat_indices[i] == plant_id && - plotinfo->kitchen.exc_types[i] == SEEDLIMIT_EXCTYPE) - { - return int(i); - } - } - return -1; -} - -bool Kitchen::removeLimit(int32_t plant_id) -{ - int i = findLimit(plant_id); - if (i < 0) - return false; - - plotinfo->kitchen.item_types.erase(plotinfo->kitchen.item_types.begin() + i); - plotinfo->kitchen.item_subtypes.erase(plotinfo->kitchen.item_subtypes.begin() + i); - plotinfo->kitchen.mat_types.erase(plotinfo->kitchen.mat_types.begin() + i); - plotinfo->kitchen.mat_indices.erase(plotinfo->kitchen.mat_indices.begin() + i); - plotinfo->kitchen.exc_types.erase(plotinfo->kitchen.exc_types.begin() + i); - return true; -} - -bool Kitchen::setLimit(int32_t plant_id, int16_t limit) -{ - if (limit > SEEDLIMIT_MAX) - limit = SEEDLIMIT_MAX; - - int i = findLimit(plant_id); - if (i < 0) - { - plotinfo->kitchen.item_types.push_back(SEEDLIMIT_ITEMTYPE); - plotinfo->kitchen.item_subtypes.push_back(SEEDLIMIT_ITEMSUBTYPE); - plotinfo->kitchen.mat_types.push_back(limit); - plotinfo->kitchen.mat_indices.push_back(plant_id); - plotinfo->kitchen.exc_types.push_back(SEEDLIMIT_EXCTYPE); - } - else - { - plotinfo->kitchen.mat_types[i] = limit; - } - return true; -} - -void Kitchen::clearLimits() -{ - for (size_t i = 0; i < size(); ++i) - { - if (plotinfo->kitchen.item_types[i] == SEEDLIMIT_ITEMTYPE && - plotinfo->kitchen.item_subtypes[i] == SEEDLIMIT_ITEMSUBTYPE && - plotinfo->kitchen.exc_types[i] == SEEDLIMIT_EXCTYPE) - { - removeLimit(plotinfo->kitchen.mat_indices[i]); - --i; - } - } -} - size_t Kitchen::size() { return plotinfo->kitchen.item_types.size(); diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 41b3ad542..d5f35fb21 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -149,7 +149,7 @@ add_subdirectory(remotefortressreader) #add_subdirectory(rendermax) dfhack_plugin(reveal reveal.cpp LINK_LIBRARIES lua) #dfhack_plugin(search search.cpp) -dfhack_plugin(seedwatch seedwatch.cpp) +dfhack_plugin(seedwatch seedwatch.cpp LINK_LIBRARIES lua) dfhack_plugin(showmood showmood.cpp) #dfhack_plugin(siege-engine siege-engine.cpp LINK_LIBRARIES lua) #dfhack_plugin(sort sort.cpp LINK_LIBRARIES lua) diff --git a/plugins/lua/seedwatch.lua b/plugins/lua/seedwatch.lua new file mode 100644 index 000000000..a72c65d47 --- /dev/null +++ b/plugins/lua/seedwatch.lua @@ -0,0 +1,67 @@ +local _ENV = mkmodule('plugins.seedwatch') + +local argparse = require('argparse') + +local function process_args(opts, args) + if args[1] == 'help' then + opts.help = true + return + end + + return argparse.processArgsGetopt(args, { + {'h', 'help', handler=function() opts.help = true end}, + }) +end + +local function print_status() + print(('seedwatch is %s'):format(isEnabled() and "enabled" or "disabled")) + print() + print('usable seed counts and current targets:') + local watch_map, seed_counts = seedwatch_getData() + local sum = 0 + local plants = df.global.world.raws.plants.all + for k,v in pairs(seed_counts) do + print((' %4d/%d %s'):format(v, watch_map[k] or 0, plants[k].id)) + sum = sum + v + end + print() + print(('total usable seeds: %d'):format(sum)) +end + +local function set_target(name, num) + if not name or #name == 0 then + qerror('must specify "all" or plant name') + end + + num = tonumber(num) + num = num and math.floor(num) or nil + if not num or num < 0 then + qerror('target must be a non-negative integer') + end + + seedwatch_setTarget(name, num) +end + +function parse_commandline(...) + local args, opts = {...}, {} + local positionals = process_args(opts, args) + + if opts.help then + return false + end + + local command = positionals[1] + if not command or command == 'status' then + print_status() + elseif command == 'clear' then + set_target('all', 0) + elseif positionals[2] and positionals[3] then + set_target(positionals[2], positionals[3]) + else + return false + end + + return true +end + +return _ENV diff --git a/plugins/seedwatch.cpp b/plugins/seedwatch.cpp index 13ed8e66e..c6482b1ac 100644 --- a/plugins/seedwatch.cpp +++ b/plugins/seedwatch.cpp @@ -4,218 +4,113 @@ // With thanks to peterix for DFHack and Quietust for information // http://www.bay12forums.com/smf/index.php?topic=91166.msg2605147#msg2605147 -#include -#include -#include -#include "Console.h" -#include "Core.h" -#include "Export.h" +#include "Debug.h" +#include "LuaTools.h" #include "PluginManager.h" -#include "modules/World.h" -#include "modules/Materials.h" +#include "TileTypes.h" + #include "modules/Kitchen.h" -#include "VersionInfo.h" -#include "df/world.h" -#include "df/plant_raw.h" +#include "modules/Maps.h" +#include "modules/Persistence.h" +#include "modules/Units.h" +#include "modules/World.h" + #include "df/item_flags.h" #include "df/items_other_id.h" +#include "df/plant_raw.h" +#include "df/world.h" + +#include -using namespace std; using namespace DFHack; using namespace df::enums; +using std::map; +using std::string; +using std::unordered_map; +using std::vector; + DFHACK_PLUGIN("seedwatch"); -DFHACK_PLUGIN_IS_ENABLED(running); // whether seedwatch is counting the seeds or not +DFHACK_PLUGIN_IS_ENABLED(is_enabled); REQUIRE_GLOBAL(world); -const int buffer = 20; // seed number buffer - 20 is reasonable +namespace DFHack { + // for configuration-related logging + DBG_DECLARE(seedwatch, config, DebugCategory::LINFO); + // for logging during the periodic scan + DBG_DECLARE(seedwatch, cycle, DebugCategory::LINFO); +} // abbreviations for the standard plants -map abbreviations; - -bool ignoreSeeds(df::item_flags& f) // seeds with the following flags should not be counted -{ - return - f.bits.dump || - f.bits.forbid || - f.bits.garbage_collect || - f.bits.hidden || - f.bits.hostile || - f.bits.on_fire || - f.bits.rotten || - f.bits.trader || - f.bits.in_building || - f.bits.in_job; -}; +static unordered_map abbreviations; +static map world_plant_ids; +static const int DEFAULT_TARGET = 30; +static const int TARGET_BUFFER = 20; // seed number buffer; 20 is reasonable -// searches abbreviations, returns expansion if so, returns original if not -string searchAbbreviations(string in) -{ - if(abbreviations.count(in) > 0) - { - return abbreviations[in]; - } - else - { - return in; - } +static const string CONFIG_KEY = string(plugin_name) + "/config"; +static const string SEED_CONFIG_KEY_PREFIX = string(plugin_name) + "/seed/"; +static PersistentDataItem config; +static unordered_map watched_seeds; + +enum ConfigValues { + CONFIG_IS_ENABLED = 0, }; -DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) -{ - if(enable == true) - { - if(Core::getInstance().isWorldLoaded()) - { - running = true; - out.print("seedwatch supervision started.\n"); - } else { - out.printerr( - "This plugin needs a fortress to be loaded and will deactivate automatically otherwise.\n" - "Activate with 'seedwatch start' after you load the game.\n" - ); - } - } else { - running = false; - out.print("seedwatch supervision stopped.\n"); - } +enum SeedConfigValues { + SEED_CONFIG_ID = 0, + SEED_CONFIG_TARGET = 1, +}; - return CR_OK; +static int get_config_val(PersistentDataItem &c, int index) { + if (!c.isValid()) + return -1; + return c.ival(index); +} +static bool get_config_bool(PersistentDataItem &c, int index) { + return get_config_val(c, index) == 1; +} +static void set_config_val(PersistentDataItem &c, int index, int value) { + if (c.isValid()) + c.ival(index) = value; +} +static void set_config_bool(PersistentDataItem &c, int index, bool value) { + set_config_val(c, index, value ? 1 : 0); } -command_result df_seedwatch(color_ostream &out, vector& parameters) -{ - CoreSuspender suspend; - - map plantIDs; - for(size_t i = 0; i < world->raws.plants.all.size(); ++i) - { - auto & plant = world->raws.plants.all[i]; - if (plant->material_defs.type[plant_material_def::seed] != -1) - plantIDs[plant->id] = i; - } - - t_gamemodes gm; - World::ReadGameMode(gm);// FIXME: check return value - - // if game mode isn't fortress mode - if(gm.g_mode != game_mode::DWARF || !World::isFortressMode(gm.g_type)) - { - // just print the help - return CR_WRONG_USAGE; - } +static PersistentDataItem & ensure_seed_config(color_ostream &out, int id) { + if (watched_seeds.count(id)) + return watched_seeds[id]; + string keyname = SEED_CONFIG_KEY_PREFIX + int_to_string(id); + DEBUG(config,out).print("creating new persistent key for seed type %d\n", id); + watched_seeds.emplace(id, World::GetPersistentData(keyname, NULL)); + return watched_seeds[id]; +} +static void remove_seed_config(color_ostream &out, int id) { + if (!watched_seeds.count(id)) + return; + DEBUG(config,out).print("removing persistent key for seed type %d\n", id); + World::DeletePersistentData(watched_seeds[id]); + watched_seeds.erase(id); +} - string par; - int limit; - switch(parameters.size()) - { - case 0: - return CR_WRONG_USAGE; - - case 1: - par = parameters[0]; - if ((par == "help") || (par == "?")) - { - return CR_WRONG_USAGE; - } - else if(par == "start") - { - plugin_enable(out, true); +static const int32_t CYCLE_TICKS = 1200; +static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle - } - else if(par == "stop") - { - plugin_enable(out, false); - } - else if(par == "clear") - { - Kitchen::clearLimits(); - out.print("seedwatch watchlist cleared\n"); - } - else if(par == "info") - { - out.print("seedwatch Info:\n"); - if(running) - { - out.print("seedwatch is supervising. Use 'disable seedwatch' to stop supervision.\n"); - } - else - { - out.print("seedwatch is not supervising. Use 'enable seedwatch' to start supervision.\n"); - } - map watchMap; - Kitchen::fillWatchMap(watchMap); - if(watchMap.empty()) - { - out.print("The watch list is empty.\n"); - } - else - { - out.print("The watch list is:\n"); - for(auto i = watchMap.begin(); i != watchMap.end(); ++i) - { - out.print("%s : %u\n", world->raws.plants.all[i->first]->id.c_str(), i->second); - } - } - } - else if(par == "debug") - { - map watchMap; - Kitchen::fillWatchMap(watchMap); - Kitchen::debug_print(out); - } - else - { - string token = searchAbbreviations(par); - if(plantIDs.count(token) > 0) - { - Kitchen::removeLimit(plantIDs[token]); - out.print("%s is not being watched\n", token.c_str()); - } - else - { - out.print("%s has not been found as a material.\n", token.c_str()); - } - } - break; - case 2: - limit = atoi(parameters[1].c_str()); - if(limit < 0) limit = 0; - if(parameters[0] == "all") - { - for(auto & entry : plantIDs) - Kitchen::setLimit(entry.second, limit); - } - else - { - string token = searchAbbreviations(parameters[0]); - if(plantIDs.count(token) > 0) - { - Kitchen::setLimit(plantIDs[token], limit); - out.print("%s is being watched.\n", token.c_str()); - } - else - { - out.print("%s has not been found as a material.\n", token.c_str()); - } - } - break; - default: - return CR_WRONG_USAGE; - break; - } +static command_result do_command(color_ostream &out, vector ¶meters); +static void do_cycle(color_ostream &out, int32_t *num_enabled_seeds, int32_t *num_disabled_seeds); +static void seedwatch_setTarget(color_ostream &out, string name, int32_t num); - return CR_OK; -} +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { + DEBUG(config,out).print("initializing %s\n", plugin_name); -DFhackCExport command_result plugin_init(color_ostream &out, vector& commands) -{ + // provide a configuration interface for the plugin commands.push_back(PluginCommand( - "seedwatch", - "Toggles seed cooking based on quantity available.", - df_seedwatch)); - // fill in the abbreviations map, with abbreviations for the standard plants + plugin_name, + "Automatically toggle seed cooking based on quantity available.", + do_command)); + + // fill in the abbreviations map abbreviations["bs"] = "SLIVER_BARB"; abbreviations["bt"] = "TUBER_BLOATED"; abbreviations["bw"] = "WEED_BLADE"; @@ -237,72 +132,285 @@ DFhackCExport command_result plugin_init(color_ostream &out, vector seedCount; // the number of seeds - - // count all seeds and plants by RAW material - for(size_t i = 0; i < world->items.other[items_other_id::SEEDS].size(); ++i) - { - df::item *item = world->items.other[items_other_id::SEEDS][i]; - MaterialInfo mat(item); - if (!mat.isPlant()) - continue; - if (!ignoreSeeds(item->flags)) - ++seedCount[mat.plant->index]; - } +DFhackCExport command_result plugin_shutdown (color_ostream &out) { + DEBUG(config,out).print("shutting down %s\n", plugin_name); + + return CR_OK; +} + +DFhackCExport command_result plugin_load_data (color_ostream &out) { + world_plant_ids.clear(); + for (size_t i = 0; i < world->raws.plants.all.size(); ++i) { + auto & plant = world->raws.plants.all[i]; + if (plant->material_defs.type[plant_material_def::seed] != -1) + world_plant_ids[plant->id] = i; + } + + config = World::GetPersistentData(CONFIG_KEY); + + if (!config.isValid()) { + DEBUG(config,out).print("no config found in this save; initializing\n"); + config = World::AddPersistentData(CONFIG_KEY); + set_config_bool(config, CONFIG_IS_ENABLED, is_enabled); + } + + is_enabled = get_config_bool(config, CONFIG_IS_ENABLED); + DEBUG(config,out).print("loading persisted enabled state: %s\n", + is_enabled ? "true" : "false"); + watched_seeds.clear(); + vector seed_configs; + World::GetPersistentData(&seed_configs, SEED_CONFIG_KEY_PREFIX, true); + const size_t num_seed_configs = seed_configs.size(); + for (size_t idx = 0; idx < num_seed_configs; ++idx) { + auto &c = seed_configs[idx]; + watched_seeds.emplace(get_config_val(c, SEED_CONFIG_ID), c); + } - map watchMap; - Kitchen::fillWatchMap(watchMap); - for(auto i = watchMap.begin(); i != watchMap.end(); ++i) - { - if(seedCount[i->first] <= i->second) - { - Kitchen::denyPlantSeedCookery(i->first); - } - else if(i->second + buffer < seedCount[i->first]) - { - Kitchen::allowPlantSeedCookery(i->first); - } + return CR_OK; +} + +DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) { + if (event == DFHack::SC_WORLD_UNLOADED) { + if (is_enabled) { + DEBUG(config,out).print("world unloaded; disabling %s\n", + plugin_name); + is_enabled = false; } } return CR_OK; } -DFhackCExport command_result plugin_shutdown(Core* pCore) -{ +DFhackCExport command_result plugin_onupdate(color_ostream &out) { + if (is_enabled && world->frame_counter - cycle_timestamp >= CYCLE_TICKS) { + int32_t num_enabled_seeds, num_disabled_seeds; + do_cycle(out, &num_enabled_seeds, &num_disabled_seeds); + if (0 < num_enabled_seeds) + out.print("%s: enabled %d seed types for cooking\n", + plugin_name, num_enabled_seeds); + if (0 < num_disabled_seeds) + out.print("%s: protected %d seed types from cooking\n", + plugin_name, num_disabled_seeds); + } return CR_OK; } + +static bool call_seedwatch_lua(color_ostream *out, const char *fn_name, + int nargs = 0, int nres = 0, + Lua::LuaLambda && args_lambda = Lua::DEFAULT_LUA_LAMBDA, + Lua::LuaLambda && res_lambda = Lua::DEFAULT_LUA_LAMBDA) { + CoreSuspender guard; + + auto L = Lua::Core::State; + Lua::StackUnwinder top(L); + + if (!out) + out = &Core::getInstance().getConsole(); + + DEBUG(config,*out).print("calling %s lua function: '%s'\n", plugin_name, fn_name); + + return Lua::CallLuaModuleFunction(*out, L, "plugins.seedwatch", fn_name, + nargs, nres, + std::forward(args_lambda), + std::forward(res_lambda)); +} + +static command_result do_command(color_ostream &out, vector ¶meters) { + CoreSuspender suspend; + + if (!Core::getInstance().isWorldLoaded()) { + out.printerr("Cannot run %s without a loaded world.\n", plugin_name); + return CR_FAILURE; + } + + bool show_help = false; + if (!call_seedwatch_lua(&out, "parse_commandline", parameters.size(), 1, + [&](lua_State *L) { + for (const string ¶m : parameters) + Lua::Push(L, param); + }, + [&](lua_State *L) { + show_help = !lua_toboolean(L, -1); + })) { + return CR_FAILURE; + } + + return show_help ? CR_WRONG_USAGE : CR_OK; +} + +///////////////////////////////////////////////////// +// cycle logic +// + +struct BadFlags { + uint32_t whole; + + // TODO: maybe don't filter out seeds that are in_building. that would + // allow us to count seeds that are in workshops. are there any negative + // consequences? + BadFlags() { + df::item_flags flags; + #define F(x) 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); + F(in_job); F(owned); F(in_chest); F(removed); + F(encased); F(spider_web); + #undef F + whole = flags.whole; + } +}; + +static bool is_accessible_item(const df::coord &pos, const vector &citizens) { + for (auto &unit : citizens) { + if (Maps::canWalkBetween(unit->pos, pos)) + return true; + } + return false; +} + +static void scan_seeds(color_ostream &out, unordered_map *accessible_counts, + unordered_map *inaccessible_counts = NULL) { + static const BadFlags bad_flags; + + vector citizens; + Units::getCitizens(citizens); + + for (auto &item : world->items.other[items_other_id::SEEDS]) { + MaterialInfo mat(item); + if (!mat.isPlant()) + continue; + if ((bad_flags.whole & item->flags.whole) || !is_accessible_item(item->pos, citizens)) { + if (inaccessible_counts) + ++(*inaccessible_counts)[mat.plant->index]; + } else { + if (accessible_counts) + ++(*accessible_counts)[mat.plant->index]; + } + } +} + +static void do_cycle(color_ostream &out, int32_t *num_enabled_seed_types, int32_t *num_disabled_seed_types) { + DEBUG(cycle,out).print("running %s cycle\n", plugin_name); + + // mark that we have recently run + cycle_timestamp = world->frame_counter; + + if (num_enabled_seed_types) + *num_enabled_seed_types = 0; + if (num_disabled_seed_types) + *num_disabled_seed_types = 0; + + unordered_map accessible_counts; + scan_seeds(out, &accessible_counts); + + for (auto &entry : watched_seeds) { + int32_t id = entry.first; + int32_t target = get_config_val(entry.second, SEED_CONFIG_TARGET); + if (accessible_counts[id] <= target) { + DEBUG(cycle,out).print("disabling seed mat: %d\n", id); + if (num_disabled_seed_types) + ++*num_disabled_seed_types; + Kitchen::denyPlantSeedCookery(id); + } else if (target + TARGET_BUFFER < accessible_counts[id]) { + DEBUG(cycle,out).print("enabling seed mat: %d\n", id); + if (num_enabled_seed_types) + ++*num_enabled_seed_types; + Kitchen::allowPlantSeedCookery(id); + } + } +} + +///////////////////////////////////////////////////// +// Lua API +// core will already be suspended when coming in through here +// + +static void set_target(color_ostream &out, int32_t id, int32_t target) { + if (target == 0) + remove_seed_config(out, id); + else { + PersistentDataItem &c = ensure_seed_config(out, id); + set_config_val(c, SEED_CONFIG_TARGET, target); + } +} + +// searches abbreviations, returns expansion if so, returns original if not +static string searchAbbreviations(string in) { + if(abbreviations.count(in) > 0) + return abbreviations[in]; + return in; +}; + +static void seedwatch_setTarget(color_ostream &out, string name, int32_t num) { + DEBUG(config,out).print("entering seedwatch_setTarget\n"); + + if (num < 0) { + out.printerr("target must be at least 0\n"); + return; + } + + if (name == "all") { + for (auto &entry : world_plant_ids) { + set_target(out, entry.second, num); + } + return; + } + + string token = searchAbbreviations(name); + if (!world_plant_ids.count(token)) { + out.printerr("%s has not been found as a material.\n", token.c_str()); + return; + } + + set_target(out, world_plant_ids[token], num); +} + +static int seedwatch_getData(lua_State *L) { + color_ostream *out = Lua::GetOutput(L); + if (!out) + out = &Core::getInstance().getConsole(); + DEBUG(config,*out).print("entering seedwatch_getData\n"); + unordered_map watch_map, accessible_counts; + scan_seeds(*out, &accessible_counts); + for (auto &entry : watched_seeds) { + watch_map.emplace(entry.first, get_config_val(entry.second, SEED_CONFIG_TARGET)); + } + Lua::Push(L, watch_map); + Lua::Push(L, accessible_counts); + return 2; +} + +DFHACK_PLUGIN_LUA_FUNCTIONS { + DFHACK_LUA_FUNCTION(seedwatch_setTarget), + DFHACK_LUA_END +}; + +DFHACK_PLUGIN_LUA_COMMANDS { + DFHACK_LUA_COMMAND(seedwatch_getData), + DFHACK_LUA_END +};