// Dynamically enables and disables cooking restrictions for plants and seeds // in order to limit the number of seeds available for each crop type // With thanks to peterix for DFHack and Quietust for information // http://www.bay12forums.com/smf/index.php?topic=91166.msg2605147#msg2605147 #include "Debug.h" #include "LuaTools.h" #include "PluginManager.h" #include "TileTypes.h" #include "modules/Items.h" #include "modules/Kitchen.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 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(is_enabled); REQUIRE_GLOBAL(world); 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 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 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, }; enum SeedConfigValues { SEED_CONFIG_ID = 0, SEED_CONFIG_TARGET = 1, }; 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); } 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)); set_config_val(watched_seeds[id], SEED_CONFIG_ID, id); 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); } // this validation removes configuration data from versions prior to 50.09-r3 // it can be removed once saves from 50.09 are no longer loadable static bool validate_seed_config(color_ostream& out, PersistentDataItem c) { int seed_id = get_config_val(c, SEED_CONFIG_ID); auto plant = binsearch_in_vector(world->raws.plants.all, &df::plant_raw::index, seed_id); bool valid = (!plant->flags.is_set(df::enums::plant_raw_flags::TREE)); if (!valid) { DEBUG(config, out).print("invalid configuration for %s discarded\n", plant->id.c_str()); } return valid; } static const int32_t CYCLE_TICKS = 1200; static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle static command_result do_command(color_ostream &out, vector ¶meters); static void do_cycle(color_ostream &out, int32_t *num_enabled_seeds = NULL, int32_t *num_disabled_seeds = NULL); static void seedwatch_setTarget(color_ostream &out, string name, int32_t num); DFhackCExport command_result plugin_init(color_ostream &out, std::vector &commands) { DEBUG(config,out).print("initializing %s\n", plugin_name); // provide a configuration interface for the plugin commands.push_back(PluginCommand( 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"; abbreviations["cw"] = "GRASS_WHEAT_CAVE"; abbreviations["dc"] = "MUSHROOM_CUP_DIMPLE"; abbreviations["fb"] = "BERRIES_FISHER"; abbreviations["hr"] = "ROOT_HIDE"; abbreviations["kb"] = "BULB_KOBOLD"; abbreviations["lg"] = "GRASS_LONGLAND"; abbreviations["mr"] = "ROOT_MUCK"; abbreviations["pb"] = "BERRIES_PRICKLE"; abbreviations["ph"] = "MUSHROOM_HELMET_PLUMP"; abbreviations["pt"] = "GRASS_TAIL_PIG"; abbreviations["qb"] = "BUSH_QUARRY"; abbreviations["rr"] = "REED_ROPE"; abbreviations["rw"] = "WEED_RAT"; abbreviations["sb"] = "BERRY_SUN"; abbreviations["sp"] = "POD_SWEET"; abbreviations["vh"] = "HERB_VALLEY"; abbreviations["ws"] = "BERRIES_STRAW"; abbreviations["wv"] = "VINE_WHIP"; return CR_OK; } DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) { if (!Core::getInstance().isWorldLoaded()) { out.printerr("Cannot enable %s without a loaded world.\n", plugin_name); return CR_FAILURE; } if (enable != is_enabled) { is_enabled = enable; DEBUG(config,out).print("%s from the API; persisting\n", is_enabled ? "enabled" : "disabled"); set_config_bool(config, CONFIG_IS_ENABLED, is_enabled); if (enable) do_cycle(out); } else { DEBUG(config,out).print("%s from the API, but already %s; no action\n", is_enabled ? "enabled" : "disabled", is_enabled ? "enabled" : "disabled"); } return CR_OK; } 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) { cycle_timestamp = 0; 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 && !plant->flags.is_set(df::enums::plant_raw_flags::TREE)) world_plant_ids[plant->id] = i; } 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]; if (validate_seed_config(out, c)) watched_seeds.emplace(get_config_val(c, SEED_CONFIG_ID), c); } 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); seedwatch_setTarget(out, "all", DEFAULT_TARGET); } is_enabled = get_config_bool(config, CONFIG_IS_ENABLED); DEBUG(config,out).print("loading persisted enabled state: %s\n", is_enabled ? "true" : "false"); 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_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; 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(df::item *item, const vector &citizens) { const df::coord pos = Items::getPosition(item); for (auto &unit : citizens) { if (Maps::canWalkBetween(Units::getPosition(unit), 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.plant->index < 0 || !mat.isPlant()) continue; if ((bad_flags.whole & item->flags.whole) || !is_accessible_item(item, 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; if (id < 0 || (size_t)id >= world->raws.plants.all.size()) continue; int32_t target = get_config_val(entry.second, SEED_CONFIG_TARGET); if (accessible_counts[id] <= target && (Kitchen::isPlantCookeryAllowed(id) || Kitchen::isSeedCookeryAllowed(id))) { 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] && (!Kitchen::isPlantCookeryAllowed(id) || !Kitchen::isSeedCookeryAllowed(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 { if (id < 0 || (size_t)id >= world->raws.plants.all.size()) { WARN(config,out).print( "cannot set target for unknown plant id: %d\n", id); return; } 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 };